@teardown/metro-config 2.0.44

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/dist/bun.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Bun-specific Metro configuration utilities.
3
+ *
4
+ * Handles blocking of .bun directories which can cause Metro resolution issues.
5
+ */
6
+ import type { ResolveRequestFn } from "./types";
7
+ /**
8
+ * Block pattern for .bun directories.
9
+ * Metro's blockList prevents processing of matched paths.
10
+ */
11
+ export declare const BUN_BLOCK_PATTERN: RegExp;
12
+ /**
13
+ * Create a custom resolver that filters out .bun paths during module resolution.
14
+ *
15
+ * Metro's blockList only prevents processing after resolution, not during.
16
+ * This resolver intercepts resolution and redirects away from .bun paths.
17
+ */
18
+ export declare function createBunAwareResolver(projectRoot: string, defaultResolveRequest: ResolveRequestFn): ResolveRequestFn;
19
+ /**
20
+ * Get blockList patterns for Metro config.
21
+ * Includes .bun directory blocking.
22
+ */
23
+ export declare function getBlockList(existingBlockList?: RegExp | RegExp[]): RegExp[];
24
+ //# sourceMappingURL=bun.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bun.d.ts","sourceRoot":"","sources":["../src/bun.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAGhD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAAwB,CAAC;AAEvD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,qBAAqB,EAAE,gBAAgB,GAAG,gBAAgB,CAwBrH;AAmCD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,CAQ5E"}
package/dist/bun.js ADDED
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ /**
3
+ * Bun-specific Metro configuration utilities.
4
+ *
5
+ * Handles blocking of .bun directories which can cause Metro resolution issues.
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.BUN_BLOCK_PATTERN = void 0;
12
+ exports.createBunAwareResolver = createBunAwareResolver;
13
+ exports.getBlockList = getBlockList;
14
+ const node_fs_1 = require("node:fs");
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ const workspace_1 = require("./workspace");
17
+ /**
18
+ * Block pattern for .bun directories.
19
+ * Metro's blockList prevents processing of matched paths.
20
+ */
21
+ exports.BUN_BLOCK_PATTERN = /.*[/\\]\.bun[/\\].*/;
22
+ /**
23
+ * Create a custom resolver that filters out .bun paths during module resolution.
24
+ *
25
+ * Metro's blockList only prevents processing after resolution, not during.
26
+ * This resolver intercepts resolution and redirects away from .bun paths.
27
+ */
28
+ function createBunAwareResolver(projectRoot, defaultResolveRequest) {
29
+ const modulesPaths = (0, workspace_1.getModulesPaths)(projectRoot);
30
+ return (context, moduleName, platform) => {
31
+ // Use Metro's default resolution
32
+ const result = defaultResolveRequest(context, moduleName, platform);
33
+ // If resolved path contains .bun, try to find alternative
34
+ if (result?.filePath?.includes("/.bun/")) {
35
+ for (const modulesPath of modulesPaths) {
36
+ if (modulesPath.includes("/.bun/"))
37
+ continue;
38
+ const packagePath = node_path_1.default.join(modulesPath, moduleName);
39
+ if ((0, node_fs_1.existsSync)(packagePath)) {
40
+ const mainFile = resolvePackageMain(packagePath);
41
+ if (mainFile) {
42
+ return { type: "sourceFile", filePath: mainFile };
43
+ }
44
+ }
45
+ }
46
+ }
47
+ return result;
48
+ };
49
+ }
50
+ /**
51
+ * Resolve the main entry point of a package.
52
+ */
53
+ function resolvePackageMain(packagePath) {
54
+ const packageJsonPath = node_path_1.default.join(packagePath, "package.json");
55
+ if ((0, node_fs_1.existsSync)(packageJsonPath)) {
56
+ try {
57
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)(packageJsonPath, "utf8"));
58
+ const main = pkg.main || pkg.module || "index.js";
59
+ const mainPath = node_path_1.default.join(packagePath, main);
60
+ if ((0, node_fs_1.existsSync)(mainPath)) {
61
+ return mainPath;
62
+ }
63
+ // Try with .js extension
64
+ const mainPathJs = mainPath + ".js";
65
+ if ((0, node_fs_1.existsSync)(mainPathJs)) {
66
+ return mainPathJs;
67
+ }
68
+ }
69
+ catch {
70
+ // Ignore parse errors
71
+ }
72
+ }
73
+ // Fallback to index.js
74
+ const indexPath = node_path_1.default.join(packagePath, "index.js");
75
+ if ((0, node_fs_1.existsSync)(indexPath)) {
76
+ return indexPath;
77
+ }
78
+ return null;
79
+ }
80
+ /**
81
+ * Get blockList patterns for Metro config.
82
+ * Includes .bun directory blocking.
83
+ */
84
+ function getBlockList(existingBlockList) {
85
+ const blockListArray = Array.isArray(existingBlockList)
86
+ ? existingBlockList
87
+ : existingBlockList
88
+ ? [existingBlockList]
89
+ : [];
90
+ return [...blockListArray, exports.BUN_BLOCK_PATTERN];
91
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @teardown/metro-config
3
+ *
4
+ * Metro configuration wrapper that provides:
5
+ * - Rust-powered transforms via Facetpack (36x faster than Babel)
6
+ * - Automatic monorepo/workspace detection and configuration
7
+ * - Bun-specific handling (blocks .bun directories)
8
+ *
9
+ * @example
10
+ * ```js
11
+ * // metro.config.js
12
+ * const { withTeardown } = require('@teardown/metro-config')
13
+ * const { getDefaultConfig } = require('@react-native/metro-config')
14
+ *
15
+ * module.exports = withTeardown(getDefaultConfig(__dirname))
16
+ * ```
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+ import { getStoredOptions } from "@ecrindigital/facetpack";
21
+ import type { MetroConfig, TeardownMetroOptions } from "./types";
22
+ /**
23
+ * Wraps a Metro configuration with Teardown's enhancements.
24
+ *
25
+ * This function applies:
26
+ * - Facetpack for Rust-powered transforms (36x faster)
27
+ * - Automatic monorepo detection and watch folder configuration
28
+ * - Bun-specific handling (.bun directory blocking)
29
+ * - Proper nodeModulesPaths for monorepo resolution
30
+ *
31
+ * @param config - Base Metro configuration
32
+ * @param options - Optional configuration options
33
+ * @returns Enhanced Metro configuration
34
+ *
35
+ * @example
36
+ * ```js
37
+ * // Basic usage - all monorepo handling is automatic
38
+ * const { withTeardown } = require('@teardown/metro-config')
39
+ * const { getDefaultConfig } = require('@react-native/metro-config')
40
+ *
41
+ * module.exports = withTeardown(getDefaultConfig(__dirname))
42
+ * ```
43
+ *
44
+ * @example
45
+ * ```js
46
+ * // With options
47
+ * const { withTeardown } = require('@teardown/metro-config')
48
+ * const { getDefaultConfig } = require('@react-native/metro-config')
49
+ *
50
+ * module.exports = withTeardown(getDefaultConfig(__dirname), {
51
+ * verbose: true,
52
+ * projectRoot: __dirname, // explicitly set project root
53
+ * })
54
+ * ```
55
+ */
56
+ export declare function withTeardown(config: MetroConfig, options?: TeardownMetroOptions): MetroConfig;
57
+ export { getStoredOptions };
58
+ export { BUN_BLOCK_PATTERN, getBlockList } from "./bun";
59
+ export { getMetroServerRoot, getModulesPaths, getRelativeProjectRoot, getWatchFolders, getWorkspaceRoot, isInMonorepo, } from "./workspace";
60
+ export type { TeardownMetroOptions, MetroConfig };
61
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,gBAAgB,EAAiB,MAAM,yBAAyB,CAAC;AAE1E,OAAO,KAAK,EAAE,WAAW,EAAoB,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAUnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,oBAAyB,GAAG,WAAW,CAoFjG;AAGD,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAG5B,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAExD,OAAO,EACN,kBAAkB,EAClB,eAAe,EACf,sBAAsB,EACtB,eAAe,EACf,gBAAgB,EAChB,YAAY,GACZ,MAAM,aAAa,CAAC;AAGrB,YAAY,EAAE,oBAAoB,EAAE,WAAW,EAAE,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ /**
3
+ * @teardown/metro-config
4
+ *
5
+ * Metro configuration wrapper that provides:
6
+ * - Rust-powered transforms via Facetpack (36x faster than Babel)
7
+ * - Automatic monorepo/workspace detection and configuration
8
+ * - Bun-specific handling (blocks .bun directories)
9
+ *
10
+ * @example
11
+ * ```js
12
+ * // metro.config.js
13
+ * const { withTeardown } = require('@teardown/metro-config')
14
+ * const { getDefaultConfig } = require('@react-native/metro-config')
15
+ *
16
+ * module.exports = withTeardown(getDefaultConfig(__dirname))
17
+ * ```
18
+ *
19
+ * @packageDocumentation
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.isInMonorepo = exports.getWorkspaceRoot = exports.getWatchFolders = exports.getRelativeProjectRoot = exports.getModulesPaths = exports.getMetroServerRoot = exports.getBlockList = exports.BUN_BLOCK_PATTERN = exports.getStoredOptions = void 0;
23
+ exports.withTeardown = withTeardown;
24
+ const facetpack_1 = require("@ecrindigital/facetpack");
25
+ Object.defineProperty(exports, "getStoredOptions", { enumerable: true, get: function () { return facetpack_1.getStoredOptions; } });
26
+ const bun_1 = require("./bun");
27
+ const workspace_1 = require("./workspace");
28
+ /**
29
+ * Wraps a Metro configuration with Teardown's enhancements.
30
+ *
31
+ * This function applies:
32
+ * - Facetpack for Rust-powered transforms (36x faster)
33
+ * - Automatic monorepo detection and watch folder configuration
34
+ * - Bun-specific handling (.bun directory blocking)
35
+ * - Proper nodeModulesPaths for monorepo resolution
36
+ *
37
+ * @param config - Base Metro configuration
38
+ * @param options - Optional configuration options
39
+ * @returns Enhanced Metro configuration
40
+ *
41
+ * @example
42
+ * ```js
43
+ * // Basic usage - all monorepo handling is automatic
44
+ * const { withTeardown } = require('@teardown/metro-config')
45
+ * const { getDefaultConfig } = require('@react-native/metro-config')
46
+ *
47
+ * module.exports = withTeardown(getDefaultConfig(__dirname))
48
+ * ```
49
+ *
50
+ * @example
51
+ * ```js
52
+ * // With options
53
+ * const { withTeardown } = require('@teardown/metro-config')
54
+ * const { getDefaultConfig } = require('@react-native/metro-config')
55
+ *
56
+ * module.exports = withTeardown(getDefaultConfig(__dirname), {
57
+ * verbose: true,
58
+ * projectRoot: __dirname, // explicitly set project root
59
+ * })
60
+ * ```
61
+ */
62
+ function withTeardown(config, options = {}) {
63
+ const { verbose, projectRoot: explicitProjectRoot, ...facetpackOptions } = options;
64
+ // Determine project root
65
+ const projectRoot = explicitProjectRoot || config.projectRoot || process.cwd();
66
+ if (verbose) {
67
+ console.log("[teardown/metro-config] Applying Teardown configuration...");
68
+ console.log(`[teardown/metro-config] Project root: ${projectRoot}`);
69
+ }
70
+ // Check if in monorepo
71
+ const inMonorepo = (0, workspace_1.isInMonorepo)(projectRoot);
72
+ if (verbose && inMonorepo) {
73
+ const workspaceRoot = (0, workspace_1.getWorkspaceRoot)(projectRoot);
74
+ console.log(`[teardown/metro-config] Monorepo detected: ${workspaceRoot}`);
75
+ }
76
+ // Get monorepo-aware watch folders
77
+ const additionalWatchFolders = (0, workspace_1.getWatchFolders)(projectRoot);
78
+ const existingWatchFolders = config.watchFolders || [];
79
+ // Get monorepo-aware node modules paths
80
+ const modulesPaths = (0, workspace_1.getModulesPaths)(projectRoot);
81
+ const existingNodeModulesPaths = config.resolver?.nodeModulesPaths || [];
82
+ // Get block list with .bun blocking
83
+ const blockList = (0, bun_1.getBlockList)(config.resolver?.blockList);
84
+ // Create Bun-aware resolver
85
+ const defaultResolveRequest = config.resolver?.resolveRequest ||
86
+ ((context, moduleName, platform) => context.resolveRequest(context, moduleName, platform));
87
+ const bunAwareResolver = (0, bun_1.createBunAwareResolver)(projectRoot, defaultResolveRequest);
88
+ // Build enhanced config
89
+ const enhancedConfig = {
90
+ ...config,
91
+ projectRoot,
92
+ watchFolders: [...new Set([...existingWatchFolders, ...additionalWatchFolders])],
93
+ resolver: {
94
+ ...config.resolver,
95
+ blockList,
96
+ nodeModulesPaths: [...new Set([...modulesPaths, ...existingNodeModulesPaths])],
97
+ resolveRequest: bunAwareResolver,
98
+ },
99
+ };
100
+ // Set unstable_serverRoot for monorepo web support
101
+ if (inMonorepo) {
102
+ const serverRoot = (0, workspace_1.getMetroServerRoot)(projectRoot);
103
+ enhancedConfig.unstable_serverRoot = serverRoot;
104
+ // Add relative project root to transformer cache key for cache collision prevention
105
+ const relativeRoot = (0, workspace_1.getRelativeProjectRoot)(projectRoot);
106
+ if (relativeRoot) {
107
+ const existingCacheKey = config.transformer?.customTransformOptions || {};
108
+ enhancedConfig.transformer = {
109
+ ...config.transformer,
110
+ customTransformOptions: {
111
+ ...existingCacheKey,
112
+ _teardownRelativeProjectRoot: relativeRoot,
113
+ },
114
+ };
115
+ }
116
+ }
117
+ if (verbose) {
118
+ const watchFoldersCount = enhancedConfig.watchFolders?.length || 0;
119
+ const nodeModulesPathsCount = enhancedConfig.resolver?.nodeModulesPaths?.length || 0;
120
+ console.log(`[teardown/metro-config] Watch folders: ${watchFoldersCount}`);
121
+ console.log(`[teardown/metro-config] Node modules paths: ${nodeModulesPathsCount}`);
122
+ }
123
+ // Apply Facetpack for Rust-powered transforms
124
+ const finalConfig = (0, facetpack_1.withFacetpack)(enhancedConfig, facetpackOptions);
125
+ if (verbose) {
126
+ console.log("[teardown/metro-config] Metro config enhanced successfully");
127
+ }
128
+ return finalConfig;
129
+ }
130
+ // Re-export Bun utilities
131
+ const bun_2 = require("./bun");
132
+ Object.defineProperty(exports, "BUN_BLOCK_PATTERN", { enumerable: true, get: function () { return bun_2.BUN_BLOCK_PATTERN; } });
133
+ Object.defineProperty(exports, "getBlockList", { enumerable: true, get: function () { return bun_2.getBlockList; } });
134
+ // Re-export workspace utilities for advanced use cases
135
+ const workspace_2 = require("./workspace");
136
+ Object.defineProperty(exports, "getMetroServerRoot", { enumerable: true, get: function () { return workspace_2.getMetroServerRoot; } });
137
+ Object.defineProperty(exports, "getModulesPaths", { enumerable: true, get: function () { return workspace_2.getModulesPaths; } });
138
+ Object.defineProperty(exports, "getRelativeProjectRoot", { enumerable: true, get: function () { return workspace_2.getRelativeProjectRoot; } });
139
+ Object.defineProperty(exports, "getWatchFolders", { enumerable: true, get: function () { return workspace_2.getWatchFolders; } });
140
+ Object.defineProperty(exports, "getWorkspaceRoot", { enumerable: true, get: function () { return workspace_2.getWorkspaceRoot; } });
141
+ Object.defineProperty(exports, "isInMonorepo", { enumerable: true, get: function () { return workspace_2.isInMonorepo; } });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Type definitions for @teardown/metro-config
3
+ */
4
+ import type { MetroConfig as FacetpackMetroConfig } from "@ecrindigital/facetpack";
5
+ /**
6
+ * Extended Metro configuration type with proper resolver and transformer types
7
+ */
8
+ export interface MetroConfig extends FacetpackMetroConfig {
9
+ projectRoot?: string;
10
+ watchFolders?: string[];
11
+ resolver?: {
12
+ blockList?: RegExp | RegExp[];
13
+ nodeModulesPaths?: string[];
14
+ resolveRequest?: ResolveRequestFn;
15
+ [key: string]: unknown;
16
+ };
17
+ transformer?: {
18
+ customTransformOptions?: Record<string, unknown>;
19
+ [key: string]: unknown;
20
+ };
21
+ [key: string]: unknown;
22
+ }
23
+ /**
24
+ * Metro resolve request function type
25
+ */
26
+ export type ResolveRequestFn = (context: {
27
+ resolveRequest: ResolveRequestFn;
28
+ }, moduleName: string, platform: string | null) => {
29
+ type: string;
30
+ filePath: string;
31
+ } | null;
32
+ /**
33
+ * Teardown-specific Metro configuration options
34
+ */
35
+ export interface TeardownMetroOptions {
36
+ /**
37
+ * Enable verbose logging for debugging
38
+ * @default false
39
+ */
40
+ verbose?: boolean;
41
+ /**
42
+ * Explicit project root path.
43
+ * If not provided, uses config.projectRoot or process.cwd().
44
+ */
45
+ projectRoot?: string;
46
+ }
47
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,IAAI,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAEnF;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,oBAAoB;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE;QACV,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAC9B,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;QAC5B,cAAc,CAAC,EAAE,gBAAgB,CAAC;QAClC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACvB,CAAC;IACF,WAAW,CAAC,EAAE;QACb,sBAAsB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjD,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACvB,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC9B,OAAO,EAAE;IAAE,cAAc,EAAE,gBAAgB,CAAA;CAAE,EAC7C,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,KACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB"}
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /**
3
+ * Type definitions for @teardown/metro-config
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Workspace and monorepo detection utilities for Metro configuration.
3
+ *
4
+ * Uses `resolve-workspace-root` to detect workspace roots across:
5
+ * - Yarn workspaces
6
+ * - npm workspaces
7
+ * - pnpm workspaces
8
+ * - Bun workspaces
9
+ */
10
+ /**
11
+ * Glob all package.json paths in a workspace.
12
+ * Matches Expo's implementation pattern.
13
+ *
14
+ * @param workspaceRoot Root file path for the workspace
15
+ * @param linkedPackages List of folders that contain linked node modules, ex: `['packages/*', 'apps/*']`
16
+ * @returns List of valid package.json file paths
17
+ */
18
+ export declare function globAllPackageJsonPaths(workspaceRoot: string, linkedPackages: string[]): string[];
19
+ /**
20
+ * Resolve all workspace package.json paths.
21
+ *
22
+ * @param workspaceRoot root file path for a workspace.
23
+ * @returns list of package.json file paths that are linked to the workspace.
24
+ */
25
+ export declare function resolveAllWorkspacePackageJsonPaths(workspaceRoot: string): string[];
26
+ /**
27
+ * Resolve the workspace root directory.
28
+ * Returns the project root if not in a workspace.
29
+ */
30
+ export declare function getWorkspaceRoot(projectRoot: string): string;
31
+ /**
32
+ * Get workspace glob patterns from the workspace root.
33
+ */
34
+ export declare function getWorkspaceGlobs(workspaceRoot: string): string[];
35
+ /**
36
+ * Check if the project is in a monorepo (workspace root differs from project root).
37
+ */
38
+ export declare function isInMonorepo(projectRoot: string): boolean;
39
+ /**
40
+ * Get the Metro server root for monorepo support.
41
+ * This should be set as `unstable_serverRoot` in Metro config.
42
+ */
43
+ export declare function getMetroServerRoot(projectRoot: string): string;
44
+ /**
45
+ * Get watch folders for Metro in a monorepo setup.
46
+ *
47
+ * Returns:
48
+ * - Empty array if not in a monorepo
49
+ * - Workspace root node_modules + all workspace package directories if in monorepo
50
+ */
51
+ export declare function getWatchFolders(projectRoot: string): string[];
52
+ /**
53
+ * Get node module paths for Metro resolver in a monorepo setup.
54
+ *
55
+ * Returns paths in priority order:
56
+ * 1. Project's local node_modules
57
+ * 2. Workspace root node_modules (if in monorepo)
58
+ */
59
+ export declare function getModulesPaths(projectRoot: string): string[];
60
+ /**
61
+ * Get the relative project root from workspace root.
62
+ * Used for cache key generation to prevent collisions in monorepos.
63
+ */
64
+ export declare function getRelativeProjectRoot(projectRoot: string): string;
65
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA0BH;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAWjG;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,EAAE,CAQnF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAY5D;AAiCD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,EAAE,CAwBjE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAM9D;AASD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAe7D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAa7D;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAMlE"}
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ /**
3
+ * Workspace and monorepo detection utilities for Metro configuration.
4
+ *
5
+ * Uses `resolve-workspace-root` to detect workspace roots across:
6
+ * - Yarn workspaces
7
+ * - npm workspaces
8
+ * - pnpm workspaces
9
+ * - Bun workspaces
10
+ */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.globAllPackageJsonPaths = globAllPackageJsonPaths;
16
+ exports.resolveAllWorkspacePackageJsonPaths = resolveAllWorkspacePackageJsonPaths;
17
+ exports.getWorkspaceRoot = getWorkspaceRoot;
18
+ exports.getWorkspaceGlobs = getWorkspaceGlobs;
19
+ exports.isInMonorepo = isInMonorepo;
20
+ exports.getMetroServerRoot = getMetroServerRoot;
21
+ exports.getWatchFolders = getWatchFolders;
22
+ exports.getModulesPaths = getModulesPaths;
23
+ exports.getRelativeProjectRoot = getRelativeProjectRoot;
24
+ const node_fs_1 = require("node:fs");
25
+ const node_path_1 = __importDefault(require("node:path"));
26
+ const glob_1 = require("glob");
27
+ /**
28
+ * Read and parse a JSON file, returning null if invalid.
29
+ */
30
+ function readJsonFile(filePath) {
31
+ const file = (0, node_fs_1.readFileSync)(filePath, "utf8");
32
+ return JSON.parse(file);
33
+ }
34
+ /**
35
+ * Check if a file is a valid JSON file.
36
+ */
37
+ function isValidJsonFile(filePath) {
38
+ try {
39
+ readJsonFile(filePath);
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ /**
47
+ * Glob all package.json paths in a workspace.
48
+ * Matches Expo's implementation pattern.
49
+ *
50
+ * @param workspaceRoot Root file path for the workspace
51
+ * @param linkedPackages List of folders that contain linked node modules, ex: `['packages/*', 'apps/*']`
52
+ * @returns List of valid package.json file paths
53
+ */
54
+ function globAllPackageJsonPaths(workspaceRoot, linkedPackages) {
55
+ return linkedPackages
56
+ .flatMap((pattern) => {
57
+ // Globs should only contain `/` as separator, even on Windows.
58
+ return (0, glob_1.globSync)(node_path_1.default.posix.join(pattern, "package.json").replace(/\\/g, "/"), {
59
+ cwd: workspaceRoot,
60
+ absolute: true,
61
+ ignore: ["**/@(Carthage|Pods|node_modules)/**"],
62
+ }).filter((pkgPath) => isValidJsonFile(pkgPath));
63
+ })
64
+ .map((p) => node_path_1.default.join(p));
65
+ }
66
+ /**
67
+ * Resolve all workspace package.json paths.
68
+ *
69
+ * @param workspaceRoot root file path for a workspace.
70
+ * @returns list of package.json file paths that are linked to the workspace.
71
+ */
72
+ function resolveAllWorkspacePackageJsonPaths(workspaceRoot) {
73
+ try {
74
+ const workspaceGlobs = getWorkspaceGlobs(workspaceRoot);
75
+ if (!workspaceGlobs?.length)
76
+ return [];
77
+ return globAllPackageJsonPaths(workspaceRoot, workspaceGlobs);
78
+ }
79
+ catch {
80
+ return [];
81
+ }
82
+ }
83
+ /**
84
+ * Resolve the workspace root directory.
85
+ * Returns the project root if not in a workspace.
86
+ */
87
+ function getWorkspaceRoot(projectRoot) {
88
+ try {
89
+ // Dynamic import to handle the package
90
+ const { resolveWorkspaceRoot } = require("resolve-workspace-root");
91
+ const workspaceRoot = resolveWorkspaceRoot(projectRoot);
92
+ return workspaceRoot || projectRoot;
93
+ }
94
+ catch {
95
+ // Fallback: check for turbo.jsonc or package.json with workspaces
96
+ return findMonorepoRootFallback(projectRoot);
97
+ }
98
+ }
99
+ /**
100
+ * Fallback monorepo root detection for when resolve-workspace-root fails.
101
+ * Checks for turbo.jsonc or package.json with workspaces field.
102
+ */
103
+ function findMonorepoRootFallback(startDir) {
104
+ let current = startDir;
105
+ while (current !== node_path_1.default.dirname(current)) {
106
+ const turboConfig = node_path_1.default.join(current, "turbo.jsonc");
107
+ const turboJson = node_path_1.default.join(current, "turbo.json");
108
+ const packageJson = node_path_1.default.join(current, "package.json");
109
+ if ((0, node_fs_1.existsSync)(turboConfig) || (0, node_fs_1.existsSync)(turboJson)) {
110
+ return current;
111
+ }
112
+ if ((0, node_fs_1.existsSync)(packageJson)) {
113
+ try {
114
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)(packageJson, "utf8"));
115
+ if (pkg.workspaces) {
116
+ return current;
117
+ }
118
+ }
119
+ catch {
120
+ // Ignore parse errors
121
+ }
122
+ }
123
+ current = node_path_1.default.dirname(current);
124
+ }
125
+ return startDir;
126
+ }
127
+ /**
128
+ * Get workspace glob patterns from the workspace root.
129
+ */
130
+ function getWorkspaceGlobs(workspaceRoot) {
131
+ try {
132
+ const { getWorkspaceGlobs: getGlobs } = require("resolve-workspace-root");
133
+ return getGlobs(workspaceRoot) ?? [];
134
+ }
135
+ catch {
136
+ // Fallback: try to read from package.json
137
+ const packageJsonPath = node_path_1.default.join(workspaceRoot, "package.json");
138
+ if ((0, node_fs_1.existsSync)(packageJsonPath)) {
139
+ try {
140
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)(packageJsonPath, "utf8"));
141
+ if (Array.isArray(pkg.workspaces)) {
142
+ return pkg.workspaces;
143
+ }
144
+ if (pkg.workspaces?.packages) {
145
+ return pkg.workspaces.packages;
146
+ }
147
+ }
148
+ catch {
149
+ // Ignore parse errors
150
+ }
151
+ }
152
+ return [];
153
+ }
154
+ }
155
+ /**
156
+ * Check if the project is in a monorepo (workspace root differs from project root).
157
+ */
158
+ function isInMonorepo(projectRoot) {
159
+ const workspaceRoot = getWorkspaceRoot(projectRoot);
160
+ return workspaceRoot !== projectRoot;
161
+ }
162
+ /**
163
+ * Get the Metro server root for monorepo support.
164
+ * This should be set as `unstable_serverRoot` in Metro config.
165
+ */
166
+ function getMetroServerRoot(projectRoot) {
167
+ // Can be disabled with environment variable
168
+ if (process.env.TEARDOWN_NO_METRO_WORKSPACE_ROOT) {
169
+ return projectRoot;
170
+ }
171
+ return getWorkspaceRoot(projectRoot);
172
+ }
173
+ /**
174
+ * Helper to get unique items from an array.
175
+ */
176
+ function uniqueItems(items) {
177
+ return [...new Set(items)];
178
+ }
179
+ /**
180
+ * Get watch folders for Metro in a monorepo setup.
181
+ *
182
+ * Returns:
183
+ * - Empty array if not in a monorepo
184
+ * - Workspace root node_modules + all workspace package directories if in monorepo
185
+ */
186
+ function getWatchFolders(projectRoot) {
187
+ const resolvedProjectRoot = node_path_1.default.resolve(projectRoot);
188
+ const workspaceRoot = getMetroServerRoot(resolvedProjectRoot);
189
+ // Rely on default behavior in standard projects.
190
+ if (workspaceRoot === resolvedProjectRoot) {
191
+ return [];
192
+ }
193
+ const packages = resolveAllWorkspacePackageJsonPaths(workspaceRoot);
194
+ if (!packages?.length) {
195
+ return [];
196
+ }
197
+ return uniqueItems([node_path_1.default.join(workspaceRoot, "node_modules"), ...packages.map((pkg) => node_path_1.default.dirname(pkg))]);
198
+ }
199
+ /**
200
+ * Get node module paths for Metro resolver in a monorepo setup.
201
+ *
202
+ * Returns paths in priority order:
203
+ * 1. Project's local node_modules
204
+ * 2. Workspace root node_modules (if in monorepo)
205
+ */
206
+ function getModulesPaths(projectRoot) {
207
+ const paths = [];
208
+ // Only add paths if in a monorepo - minimizes chance of Metro resolver breaking
209
+ const resolvedProjectRoot = node_path_1.default.resolve(projectRoot);
210
+ const workspaceRoot = getMetroServerRoot(resolvedProjectRoot);
211
+ if (workspaceRoot !== resolvedProjectRoot) {
212
+ paths.push(node_path_1.default.resolve(projectRoot, "node_modules"));
213
+ paths.push(node_path_1.default.resolve(workspaceRoot, "node_modules"));
214
+ }
215
+ return paths;
216
+ }
217
+ /**
218
+ * Get the relative project root from workspace root.
219
+ * Used for cache key generation to prevent collisions in monorepos.
220
+ */
221
+ function getRelativeProjectRoot(projectRoot) {
222
+ const workspaceRoot = getWorkspaceRoot(projectRoot);
223
+ if (workspaceRoot === projectRoot) {
224
+ return "";
225
+ }
226
+ return node_path_1.default.relative(workspaceRoot, projectRoot);
227
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@teardown/metro-config",
3
+ "version": "2.0.44",
4
+ "description": "Metro configuration for Teardown - Rust-powered transforms via Facetpack",
5
+ "private": false,
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "require": "./dist/index.js",
15
+ "import": "./dist/index.js",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "scripts": {
21
+ "typecheck": "bun x tsgo --noEmit --project ./tsconfig.json",
22
+ "build": "bun x tsgo --project ./tsconfig.json",
23
+ "dev": "bun x tsgo --watch --project ./tsconfig.json"
24
+ },
25
+ "peerDependencies": {
26
+ "@ecrindigital/facetpack": "^0.2.0",
27
+ "metro": "*",
28
+ "metro-config": "*"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "metro": {
32
+ "optional": true
33
+ },
34
+ "metro-config": {
35
+ "optional": true
36
+ }
37
+ },
38
+ "dependencies": {
39
+ "glob": "^10.0.0",
40
+ "resolve-workspace-root": "^2.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@teardown/tsconfig": "2.0.44",
44
+ "@types/bun": "1.3.5",
45
+ "typescript": "5.9.3"
46
+ },
47
+ "keywords": [
48
+ "react-native",
49
+ "metro",
50
+ "facetpack",
51
+ "bundler",
52
+ "rust",
53
+ "oxc"
54
+ ]
55
+ }
package/src/bun.ts ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Bun-specific Metro configuration utilities.
3
+ *
4
+ * Handles blocking of .bun directories which can cause Metro resolution issues.
5
+ */
6
+
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import path from "node:path";
9
+ import type { ResolveRequestFn } from "./types";
10
+ import { getModulesPaths } from "./workspace";
11
+
12
+ /**
13
+ * Block pattern for .bun directories.
14
+ * Metro's blockList prevents processing of matched paths.
15
+ */
16
+ export const BUN_BLOCK_PATTERN = /.*[/\\]\.bun[/\\].*/;
17
+
18
+ /**
19
+ * Create a custom resolver that filters out .bun paths during module resolution.
20
+ *
21
+ * Metro's blockList only prevents processing after resolution, not during.
22
+ * This resolver intercepts resolution and redirects away from .bun paths.
23
+ */
24
+ export function createBunAwareResolver(projectRoot: string, defaultResolveRequest: ResolveRequestFn): ResolveRequestFn {
25
+ const modulesPaths = getModulesPaths(projectRoot);
26
+
27
+ return (context: { resolveRequest: ResolveRequestFn }, moduleName: string, platform: string | null) => {
28
+ // Use Metro's default resolution
29
+ const result = defaultResolveRequest(context, moduleName, platform);
30
+
31
+ // If resolved path contains .bun, try to find alternative
32
+ if (result?.filePath?.includes("/.bun/")) {
33
+ for (const modulesPath of modulesPaths) {
34
+ if (modulesPath.includes("/.bun/")) continue;
35
+
36
+ const packagePath = path.join(modulesPath, moduleName);
37
+ if (existsSync(packagePath)) {
38
+ const mainFile = resolvePackageMain(packagePath);
39
+ if (mainFile) {
40
+ return { type: "sourceFile", filePath: mainFile };
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ return result;
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Resolve the main entry point of a package.
52
+ */
53
+ function resolvePackageMain(packagePath: string): string | null {
54
+ const packageJsonPath = path.join(packagePath, "package.json");
55
+ if (existsSync(packageJsonPath)) {
56
+ try {
57
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
58
+ const main = pkg.main || pkg.module || "index.js";
59
+ const mainPath = path.join(packagePath, main);
60
+
61
+ if (existsSync(mainPath)) {
62
+ return mainPath;
63
+ }
64
+ // Try with .js extension
65
+ const mainPathJs = mainPath + ".js";
66
+ if (existsSync(mainPathJs)) {
67
+ return mainPathJs;
68
+ }
69
+ } catch {
70
+ // Ignore parse errors
71
+ }
72
+ }
73
+
74
+ // Fallback to index.js
75
+ const indexPath = path.join(packagePath, "index.js");
76
+ if (existsSync(indexPath)) {
77
+ return indexPath;
78
+ }
79
+
80
+ return null;
81
+ }
82
+
83
+ /**
84
+ * Get blockList patterns for Metro config.
85
+ * Includes .bun directory blocking.
86
+ */
87
+ export function getBlockList(existingBlockList?: RegExp | RegExp[]): RegExp[] {
88
+ const blockListArray = Array.isArray(existingBlockList)
89
+ ? existingBlockList
90
+ : existingBlockList
91
+ ? [existingBlockList]
92
+ : [];
93
+
94
+ return [...blockListArray, BUN_BLOCK_PATTERN];
95
+ }
package/src/index.ts ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * @teardown/metro-config
3
+ *
4
+ * Metro configuration wrapper that provides:
5
+ * - Rust-powered transforms via Facetpack (36x faster than Babel)
6
+ * - Automatic monorepo/workspace detection and configuration
7
+ * - Bun-specific handling (blocks .bun directories)
8
+ *
9
+ * @example
10
+ * ```js
11
+ * // metro.config.js
12
+ * const { withTeardown } = require('@teardown/metro-config')
13
+ * const { getDefaultConfig } = require('@react-native/metro-config')
14
+ *
15
+ * module.exports = withTeardown(getDefaultConfig(__dirname))
16
+ * ```
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+
21
+ import { getStoredOptions, withFacetpack } from "@ecrindigital/facetpack";
22
+ import { createBunAwareResolver, getBlockList } from "./bun";
23
+ import type { MetroConfig, ResolveRequestFn, TeardownMetroOptions } from "./types";
24
+ import {
25
+ getMetroServerRoot,
26
+ getModulesPaths,
27
+ getRelativeProjectRoot,
28
+ getWatchFolders,
29
+ getWorkspaceRoot,
30
+ isInMonorepo,
31
+ } from "./workspace";
32
+
33
+ /**
34
+ * Wraps a Metro configuration with Teardown's enhancements.
35
+ *
36
+ * This function applies:
37
+ * - Facetpack for Rust-powered transforms (36x faster)
38
+ * - Automatic monorepo detection and watch folder configuration
39
+ * - Bun-specific handling (.bun directory blocking)
40
+ * - Proper nodeModulesPaths for monorepo resolution
41
+ *
42
+ * @param config - Base Metro configuration
43
+ * @param options - Optional configuration options
44
+ * @returns Enhanced Metro configuration
45
+ *
46
+ * @example
47
+ * ```js
48
+ * // Basic usage - all monorepo handling is automatic
49
+ * const { withTeardown } = require('@teardown/metro-config')
50
+ * const { getDefaultConfig } = require('@react-native/metro-config')
51
+ *
52
+ * module.exports = withTeardown(getDefaultConfig(__dirname))
53
+ * ```
54
+ *
55
+ * @example
56
+ * ```js
57
+ * // With options
58
+ * const { withTeardown } = require('@teardown/metro-config')
59
+ * const { getDefaultConfig } = require('@react-native/metro-config')
60
+ *
61
+ * module.exports = withTeardown(getDefaultConfig(__dirname), {
62
+ * verbose: true,
63
+ * projectRoot: __dirname, // explicitly set project root
64
+ * })
65
+ * ```
66
+ */
67
+ export function withTeardown(config: MetroConfig, options: TeardownMetroOptions = {}): MetroConfig {
68
+ const { verbose, projectRoot: explicitProjectRoot, ...facetpackOptions } = options;
69
+
70
+ // Determine project root
71
+ const projectRoot = explicitProjectRoot || (config.projectRoot as string | undefined) || process.cwd();
72
+
73
+ if (verbose) {
74
+ console.log("[teardown/metro-config] Applying Teardown configuration...");
75
+ console.log(`[teardown/metro-config] Project root: ${projectRoot}`);
76
+ }
77
+
78
+ // Check if in monorepo
79
+ const inMonorepo = isInMonorepo(projectRoot);
80
+ if (verbose && inMonorepo) {
81
+ const workspaceRoot = getWorkspaceRoot(projectRoot);
82
+ console.log(`[teardown/metro-config] Monorepo detected: ${workspaceRoot}`);
83
+ }
84
+
85
+ // Get monorepo-aware watch folders
86
+ const additionalWatchFolders = getWatchFolders(projectRoot);
87
+ const existingWatchFolders = (config.watchFolders as string[] | undefined) || [];
88
+
89
+ // Get monorepo-aware node modules paths
90
+ const modulesPaths = getModulesPaths(projectRoot);
91
+ const existingNodeModulesPaths = (config.resolver?.nodeModulesPaths as string[] | undefined) || [];
92
+
93
+ // Get block list with .bun blocking
94
+ const blockList = getBlockList(config.resolver?.blockList as RegExp | RegExp[] | undefined);
95
+
96
+ // Create Bun-aware resolver
97
+ const defaultResolveRequest: ResolveRequestFn =
98
+ (config.resolver?.resolveRequest as ResolveRequestFn | undefined) ||
99
+ ((context, moduleName, platform) => context.resolveRequest(context, moduleName, platform));
100
+
101
+ const bunAwareResolver = createBunAwareResolver(projectRoot, defaultResolveRequest);
102
+
103
+ // Build enhanced config
104
+ const enhancedConfig: MetroConfig = {
105
+ ...config,
106
+ projectRoot,
107
+ watchFolders: [...new Set([...existingWatchFolders, ...additionalWatchFolders])],
108
+ resolver: {
109
+ ...config.resolver,
110
+ blockList,
111
+ nodeModulesPaths: [...new Set([...modulesPaths, ...existingNodeModulesPaths])],
112
+ resolveRequest: bunAwareResolver,
113
+ },
114
+ };
115
+
116
+ // Set unstable_serverRoot for monorepo web support
117
+ if (inMonorepo) {
118
+ const serverRoot = getMetroServerRoot(projectRoot);
119
+ (enhancedConfig as Record<string, unknown>).unstable_serverRoot = serverRoot;
120
+
121
+ // Add relative project root to transformer cache key for cache collision prevention
122
+ const relativeRoot = getRelativeProjectRoot(projectRoot);
123
+ if (relativeRoot) {
124
+ const existingCacheKey =
125
+ (config.transformer as Record<string, unknown> | undefined)?.customTransformOptions || {};
126
+ enhancedConfig.transformer = {
127
+ ...config.transformer,
128
+ customTransformOptions: {
129
+ ...(existingCacheKey as Record<string, unknown>),
130
+ _teardownRelativeProjectRoot: relativeRoot,
131
+ },
132
+ };
133
+ }
134
+ }
135
+
136
+ if (verbose) {
137
+ const watchFoldersCount = (enhancedConfig.watchFolders as string[] | undefined)?.length || 0;
138
+ const nodeModulesPathsCount = (enhancedConfig.resolver?.nodeModulesPaths as string[] | undefined)?.length || 0;
139
+ console.log(`[teardown/metro-config] Watch folders: ${watchFoldersCount}`);
140
+ console.log(`[teardown/metro-config] Node modules paths: ${nodeModulesPathsCount}`);
141
+ }
142
+
143
+ // Apply Facetpack for Rust-powered transforms
144
+ const finalConfig = withFacetpack(enhancedConfig, facetpackOptions);
145
+
146
+ if (verbose) {
147
+ console.log("[teardown/metro-config] Metro config enhanced successfully");
148
+ }
149
+
150
+ return finalConfig;
151
+ }
152
+
153
+ // Re-export useful utilities from Facetpack
154
+ export { getStoredOptions };
155
+
156
+ // Re-export Bun utilities
157
+ export { BUN_BLOCK_PATTERN, getBlockList } from "./bun";
158
+ // Re-export workspace utilities for advanced use cases
159
+ export {
160
+ getMetroServerRoot,
161
+ getModulesPaths,
162
+ getRelativeProjectRoot,
163
+ getWatchFolders,
164
+ getWorkspaceRoot,
165
+ isInMonorepo,
166
+ } from "./workspace";
167
+
168
+ // Re-export types
169
+ export type { TeardownMetroOptions, MetroConfig };
package/src/types.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Type definitions for @teardown/metro-config
3
+ */
4
+
5
+ import type { MetroConfig as FacetpackMetroConfig } from "@ecrindigital/facetpack";
6
+
7
+ /**
8
+ * Extended Metro configuration type with proper resolver and transformer types
9
+ */
10
+ export interface MetroConfig extends FacetpackMetroConfig {
11
+ projectRoot?: string;
12
+ watchFolders?: string[];
13
+ resolver?: {
14
+ blockList?: RegExp | RegExp[];
15
+ nodeModulesPaths?: string[];
16
+ resolveRequest?: ResolveRequestFn;
17
+ [key: string]: unknown;
18
+ };
19
+ transformer?: {
20
+ customTransformOptions?: Record<string, unknown>;
21
+ [key: string]: unknown;
22
+ };
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ /**
27
+ * Metro resolve request function type
28
+ */
29
+ export type ResolveRequestFn = (
30
+ context: { resolveRequest: ResolveRequestFn },
31
+ moduleName: string,
32
+ platform: string | null
33
+ ) => { type: string; filePath: string } | null;
34
+
35
+ /**
36
+ * Teardown-specific Metro configuration options
37
+ */
38
+ export interface TeardownMetroOptions {
39
+ /**
40
+ * Enable verbose logging for debugging
41
+ * @default false
42
+ */
43
+ verbose?: boolean;
44
+
45
+ /**
46
+ * Explicit project root path.
47
+ * If not provided, uses config.projectRoot or process.cwd().
48
+ */
49
+ projectRoot?: string;
50
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Workspace and monorepo detection utilities for Metro configuration.
3
+ *
4
+ * Uses `resolve-workspace-root` to detect workspace roots across:
5
+ * - Yarn workspaces
6
+ * - npm workspaces
7
+ * - pnpm workspaces
8
+ * - Bun workspaces
9
+ */
10
+
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import path from "node:path";
13
+ import { globSync } from "glob";
14
+
15
+ /**
16
+ * Read and parse a JSON file, returning null if invalid.
17
+ */
18
+ function readJsonFile(filePath: string): unknown {
19
+ const file = readFileSync(filePath, "utf8");
20
+ return JSON.parse(file);
21
+ }
22
+
23
+ /**
24
+ * Check if a file is a valid JSON file.
25
+ */
26
+ function isValidJsonFile(filePath: string): boolean {
27
+ try {
28
+ readJsonFile(filePath);
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Glob all package.json paths in a workspace.
37
+ * Matches Expo's implementation pattern.
38
+ *
39
+ * @param workspaceRoot Root file path for the workspace
40
+ * @param linkedPackages List of folders that contain linked node modules, ex: `['packages/*', 'apps/*']`
41
+ * @returns List of valid package.json file paths
42
+ */
43
+ export function globAllPackageJsonPaths(workspaceRoot: string, linkedPackages: string[]): string[] {
44
+ return linkedPackages
45
+ .flatMap((pattern) => {
46
+ // Globs should only contain `/` as separator, even on Windows.
47
+ return globSync(path.posix.join(pattern, "package.json").replace(/\\/g, "/"), {
48
+ cwd: workspaceRoot,
49
+ absolute: true,
50
+ ignore: ["**/@(Carthage|Pods|node_modules)/**"],
51
+ }).filter((pkgPath) => isValidJsonFile(pkgPath));
52
+ })
53
+ .map((p) => path.join(p));
54
+ }
55
+
56
+ /**
57
+ * Resolve all workspace package.json paths.
58
+ *
59
+ * @param workspaceRoot root file path for a workspace.
60
+ * @returns list of package.json file paths that are linked to the workspace.
61
+ */
62
+ export function resolveAllWorkspacePackageJsonPaths(workspaceRoot: string): string[] {
63
+ try {
64
+ const workspaceGlobs = getWorkspaceGlobs(workspaceRoot);
65
+ if (!workspaceGlobs?.length) return [];
66
+ return globAllPackageJsonPaths(workspaceRoot, workspaceGlobs);
67
+ } catch {
68
+ return [];
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Resolve the workspace root directory.
74
+ * Returns the project root if not in a workspace.
75
+ */
76
+ export function getWorkspaceRoot(projectRoot: string): string {
77
+ try {
78
+ // Dynamic import to handle the package
79
+ const { resolveWorkspaceRoot } = require("resolve-workspace-root") as {
80
+ resolveWorkspaceRoot: (cwd: string) => string | null;
81
+ };
82
+ const workspaceRoot = resolveWorkspaceRoot(projectRoot);
83
+ return workspaceRoot || projectRoot;
84
+ } catch {
85
+ // Fallback: check for turbo.jsonc or package.json with workspaces
86
+ return findMonorepoRootFallback(projectRoot);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Fallback monorepo root detection for when resolve-workspace-root fails.
92
+ * Checks for turbo.jsonc or package.json with workspaces field.
93
+ */
94
+ function findMonorepoRootFallback(startDir: string): string {
95
+ let current = startDir;
96
+ while (current !== path.dirname(current)) {
97
+ const turboConfig = path.join(current, "turbo.jsonc");
98
+ const turboJson = path.join(current, "turbo.json");
99
+ const packageJson = path.join(current, "package.json");
100
+
101
+ if (existsSync(turboConfig) || existsSync(turboJson)) {
102
+ return current;
103
+ }
104
+
105
+ if (existsSync(packageJson)) {
106
+ try {
107
+ const pkg = JSON.parse(readFileSync(packageJson, "utf8"));
108
+ if (pkg.workspaces) {
109
+ return current;
110
+ }
111
+ } catch {
112
+ // Ignore parse errors
113
+ }
114
+ }
115
+
116
+ current = path.dirname(current);
117
+ }
118
+ return startDir;
119
+ }
120
+
121
+ /**
122
+ * Get workspace glob patterns from the workspace root.
123
+ */
124
+ export function getWorkspaceGlobs(workspaceRoot: string): string[] {
125
+ try {
126
+ const { getWorkspaceGlobs: getGlobs } = require("resolve-workspace-root") as {
127
+ getWorkspaceGlobs: (cwd: string) => string[] | null;
128
+ };
129
+ return getGlobs(workspaceRoot) ?? [];
130
+ } catch {
131
+ // Fallback: try to read from package.json
132
+ const packageJsonPath = path.join(workspaceRoot, "package.json");
133
+ if (existsSync(packageJsonPath)) {
134
+ try {
135
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
136
+ if (Array.isArray(pkg.workspaces)) {
137
+ return pkg.workspaces;
138
+ }
139
+ if (pkg.workspaces?.packages) {
140
+ return pkg.workspaces.packages;
141
+ }
142
+ } catch {
143
+ // Ignore parse errors
144
+ }
145
+ }
146
+ return [];
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Check if the project is in a monorepo (workspace root differs from project root).
152
+ */
153
+ export function isInMonorepo(projectRoot: string): boolean {
154
+ const workspaceRoot = getWorkspaceRoot(projectRoot);
155
+ return workspaceRoot !== projectRoot;
156
+ }
157
+
158
+ /**
159
+ * Get the Metro server root for monorepo support.
160
+ * This should be set as `unstable_serverRoot` in Metro config.
161
+ */
162
+ export function getMetroServerRoot(projectRoot: string): string {
163
+ // Can be disabled with environment variable
164
+ if (process.env.TEARDOWN_NO_METRO_WORKSPACE_ROOT) {
165
+ return projectRoot;
166
+ }
167
+ return getWorkspaceRoot(projectRoot);
168
+ }
169
+
170
+ /**
171
+ * Helper to get unique items from an array.
172
+ */
173
+ function uniqueItems(items: string[]): string[] {
174
+ return [...new Set(items)];
175
+ }
176
+
177
+ /**
178
+ * Get watch folders for Metro in a monorepo setup.
179
+ *
180
+ * Returns:
181
+ * - Empty array if not in a monorepo
182
+ * - Workspace root node_modules + all workspace package directories if in monorepo
183
+ */
184
+ export function getWatchFolders(projectRoot: string): string[] {
185
+ const resolvedProjectRoot = path.resolve(projectRoot);
186
+ const workspaceRoot = getMetroServerRoot(resolvedProjectRoot);
187
+
188
+ // Rely on default behavior in standard projects.
189
+ if (workspaceRoot === resolvedProjectRoot) {
190
+ return [];
191
+ }
192
+
193
+ const packages = resolveAllWorkspacePackageJsonPaths(workspaceRoot);
194
+ if (!packages?.length) {
195
+ return [];
196
+ }
197
+
198
+ return uniqueItems([path.join(workspaceRoot, "node_modules"), ...packages.map((pkg) => path.dirname(pkg))]);
199
+ }
200
+
201
+ /**
202
+ * Get node module paths for Metro resolver in a monorepo setup.
203
+ *
204
+ * Returns paths in priority order:
205
+ * 1. Project's local node_modules
206
+ * 2. Workspace root node_modules (if in monorepo)
207
+ */
208
+ export function getModulesPaths(projectRoot: string): string[] {
209
+ const paths: string[] = [];
210
+
211
+ // Only add paths if in a monorepo - minimizes chance of Metro resolver breaking
212
+ const resolvedProjectRoot = path.resolve(projectRoot);
213
+ const workspaceRoot = getMetroServerRoot(resolvedProjectRoot);
214
+
215
+ if (workspaceRoot !== resolvedProjectRoot) {
216
+ paths.push(path.resolve(projectRoot, "node_modules"));
217
+ paths.push(path.resolve(workspaceRoot, "node_modules"));
218
+ }
219
+
220
+ return paths;
221
+ }
222
+
223
+ /**
224
+ * Get the relative project root from workspace root.
225
+ * Used for cache key generation to prevent collisions in monorepos.
226
+ */
227
+ export function getRelativeProjectRoot(projectRoot: string): string {
228
+ const workspaceRoot = getWorkspaceRoot(projectRoot);
229
+ if (workspaceRoot === projectRoot) {
230
+ return "";
231
+ }
232
+ return path.relative(workspaceRoot, projectRoot);
233
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "@teardown/tsconfig/tsconfig.bun.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "module": "CommonJS",
7
+ "moduleResolution": "node",
8
+ "esModuleInterop": true,
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "emitDeclarationOnly": false,
12
+ "allowImportingTsExtensions": false,
13
+ "verbatimModuleSyntax": false
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }