@ttsc/metro 0.18.4 → 0.19.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 CHANGED
@@ -41,6 +41,8 @@ module.exports = withTtsc(getDefaultConfig(__dirname));
41
41
 
42
42
  `withTtsc` sets `transformer.babelTransformerPath` and leaves the rest of your config untouched. It auto-detects the upstream transformer to delegate to (`@expo/metro-config/babel-transformer` for Expo, then `@react-native/metro-babel-transformer`, then the legacy `metro-react-native-babel-transformer`).
43
43
 
44
+ Auto-detection only skips a candidate whose entry point is genuinely **not available** — the package is not installed, or it is installed but the requested subpath is not exported (Expo/React Native version skew). A candidate that _does_ resolve but fails while loading — a top-level throw, an incompatible runtime ABI, or a missing peer/transitive dependency — surfaces its original error (as the `cause` of a `@ttsc/metro` wrapper) instead of being treated as absent. This stops a broken Expo/React Native install from silently falling through to the wrong transformer, and stops an explicit `upstreamTransformer` failure from being reported as if the module did not exist.
45
+
44
46
  ## Configuration
45
47
 
46
48
  By default `@ttsc/metro` finds the nearest `tsconfig.json` from the file being transformed and runs the plugins configured there: the standard `ttsc` model. If that is the config you want, `withTtsc(getDefaultConfig(__dirname))` is enough.
@@ -33,14 +33,33 @@ const UPSTREAM_CANDIDATES = [
33
33
  */
34
34
  function resolveUpstreamTransformer(customPath, load = tryRequire) {
35
35
  if (customPath !== undefined && customPath.length !== 0) {
36
- const upstream = load(customPath);
36
+ let upstream;
37
+ try {
38
+ upstream = load(customPath);
39
+ }
40
+ catch (cause) {
41
+ // The module resolves but failed while initializing (a top-level throw,
42
+ // a missing peer/transitive dependency, or a runtime-ABI rejection).
43
+ // Preserve the original diagnostic instead of masking it as absence.
44
+ throw new Error(`[@ttsc/metro] Failed to load the configured upstream transformer "${customPath}": ${errorMessage(cause)}`, { cause });
45
+ }
37
46
  if (upstream === undefined) {
38
47
  throw new Error(`[@ttsc/metro] Could not load the configured upstream transformer: ${customPath}`);
39
48
  }
40
49
  return upstream;
41
50
  }
42
51
  for (const candidate of UPSTREAM_CANDIDATES) {
43
- const upstream = load(candidate);
52
+ let upstream;
53
+ try {
54
+ upstream = load(candidate);
55
+ }
56
+ catch (cause) {
57
+ // A candidate that resolves but throws while initializing is a broken
58
+ // installation of the active stack, not an absent optional peer. Surface
59
+ // it rather than silently falling through to a candidate that does not
60
+ // match this project.
61
+ throw new Error(`[@ttsc/metro] The upstream Metro transformer "${candidate}" is installed but failed to initialize: ${errorMessage(cause)}`, { cause });
62
+ }
44
63
  if (upstream !== undefined) {
45
64
  return upstream;
46
65
  }
@@ -50,13 +69,66 @@ function resolveUpstreamTransformer(customPath, load = tryRequire) {
50
69
  "(React Native), or set the `upstreamTransformer` option to an explicit " +
51
70
  "module path.");
52
71
  }
72
+ /**
73
+ * Load an upstream transformer module, separating genuine absence from a broken
74
+ * installation.
75
+ *
76
+ * Resolution and execution are split deliberately. `require.resolve` only walks
77
+ * the module graph for the requested specifier; it never executes third-party
78
+ * code, so a failure there proves the requested candidate itself is not present
79
+ * — reported as `undefined` (absence) so automatic probing continues to the
80
+ * next optional peer. Once resolution succeeds, any error thrown by the actual
81
+ * `require` comes from executing the module body, including a missing peer or
82
+ * transitive dependency; that is a real initialization failure and is rethrown
83
+ * with its original message and stack so the caller can preserve it.
84
+ */
53
85
  function tryRequire(modulePath) {
54
86
  try {
55
- return nodeRequire(modulePath);
87
+ nodeRequire.resolve(modulePath);
88
+ }
89
+ catch (error) {
90
+ if (isCandidateAbsent(error)) {
91
+ return undefined;
92
+ }
93
+ // A resolution error that is not one of the known "entry point absent"
94
+ // codes (e.g. an invalid specifier) is not evidence of a plain absence;
95
+ // surface it rather than silently skipping the candidate.
96
+ throw error;
56
97
  }
57
- catch {
58
- return undefined;
98
+ return nodeRequire(modulePath);
99
+ }
100
+ /**
101
+ * Whether a resolution error means the requested candidate's entry point is not
102
+ * available here — i.e. genuine absence, not a broken initialization.
103
+ *
104
+ * Resolution never executes the module body, so a `require.resolve` failure can
105
+ * only concern the requested specifier, never a transitive import of it. Each
106
+ * recognised code says the same thing about that specifier:
107
+ *
108
+ * - `MODULE_NOT_FOUND` / `ERR_MODULE_NOT_FOUND` — the package or file itself is
109
+ * not installed (CJS and ESM loaders respectively).
110
+ * - `ERR_PACKAGE_PATH_NOT_EXPORTED` — the package is installed but the requested
111
+ * subpath is not exported (or its export target is missing). This matters for
112
+ * the `@expo/metro-config/babel-transformer` candidate, a package subpath:
113
+ * under Expo/React Native version skew a present but non-exporting package
114
+ * must stay non-fatal so auto-detection falls through to the next candidate,
115
+ * exactly as a wholly absent package does.
116
+ *
117
+ * An error thrown later, while the resolved module executes, is a real
118
+ * initialization failure and is never routed here — the caller preserves it.
119
+ */
120
+ function isCandidateAbsent(error) {
121
+ const code = error?.code;
122
+ return (code === "MODULE_NOT_FOUND" ||
123
+ code === "ERR_MODULE_NOT_FOUND" ||
124
+ code === "ERR_PACKAGE_PATH_NOT_EXPORTED");
125
+ }
126
+ /** Best-effort message extraction for wrapping an unknown thrown value. */
127
+ function errorMessage(error) {
128
+ if (error instanceof Error) {
129
+ return error.message;
59
130
  }
131
+ return String(error);
60
132
  }
61
133
 
62
134
  exports.UPSTREAM_CANDIDATES = UPSTREAM_CANDIDATES;
@@ -1 +1 @@
1
- {"version":3,"file":"upstream.js","sources":["../../src/core/upstream.ts"],"sourcesContent":[null],"names":["createRequire"],"mappings":";;;;;AAEA,MAAM,WAAW,GAAGA,yBAAa,CAAC,kQAAe,CAAC;AAqBlD;;;;AAIG;AACI,MAAM,mBAAmB,GAAG;IACjC,sCAAsC;IACtC,uCAAuC;IACvC,sCAAsC;;AAGxC;;;;;;;;;;;;;;;;AAgBG;SACa,0BAA0B,CACxC,UAAmB,EACnB,OAAgE,UAAU,EAAA;IAE1E,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE;AACvD,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC;AACjC,QAAA,IAAI,QAAQ,KAAK,SAAS,EAAE;AAC1B,YAAA,MAAM,IAAI,KAAK,CACb,qEAAqE,UAAU,CAAA,CAAE,CAClF;QACH;AACA,QAAA,OAAO,QAAQ;IACjB;AAEA,IAAA,KAAK,MAAM,SAAS,IAAI,mBAAmB,EAAE;AAC3C,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;AAChC,QAAA,IAAI,QAAQ,KAAK,SAAS,EAAE;AAC1B,YAAA,OAAO,QAAQ;QACjB;IACF;IAEA,MAAM,IAAI,KAAK,CACb,sEAAsE;QACpE,qEAAqE;QACrE,yEAAyE;AACzE,QAAA,cAAc,CACjB;AACH;AAEA,SAAS,UAAU,CAAC,UAAkB,EAAA;AACpC,IAAA,IAAI;AACF,QAAA,OAAO,WAAW,CAAC,UAAU,CAAwB;IACvD;AAAE,IAAA,MAAM;AACN,QAAA,OAAO,SAAS;IAClB;AACF;;;;;"}
1
+ {"version":3,"file":"upstream.js","sources":["../../src/core/upstream.ts"],"sourcesContent":[null],"names":["createRequire"],"mappings":";;;;;AAEA,MAAM,WAAW,GAAGA,yBAAa,CAAC,kQAAe,CAAC;AAqBlD;;;;AAIG;AACI,MAAM,mBAAmB,GAAG;IACjC,sCAAsC;IACtC,uCAAuC;IACvC,sCAAsC;;AAGxC;;;;;;;;;;;;;;;;AAgBG;SACa,0BAA0B,CACxC,UAAmB,EACnB,OAAgE,UAAU,EAAA;IAE1E,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE;AACvD,QAAA,IAAI,QAAyC;AAC7C,QAAA,IAAI;AACF,YAAA,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC;QAC7B;QAAE,OAAO,KAAK,EAAE;;;;AAId,YAAA,MAAM,IAAI,KAAK,CACb,CAAA,kEAAA,EAAqE,UAAU,MAAM,YAAY,CAAC,KAAK,CAAC,EAAE,EAC1G,EAAE,KAAK,EAAE,CACV;QACH;AACA,QAAA,IAAI,QAAQ,KAAK,SAAS,EAAE;AAC1B,YAAA,MAAM,IAAI,KAAK,CACb,qEAAqE,UAAU,CAAA,CAAE,CAClF;QACH;AACA,QAAA,OAAO,QAAQ;IACjB;AAEA,IAAA,KAAK,MAAM,SAAS,IAAI,mBAAmB,EAAE;AAC3C,QAAA,IAAI,QAAyC;AAC7C,QAAA,IAAI;AACF,YAAA,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;QAC5B;QAAE,OAAO,KAAK,EAAE;;;;;AAKd,YAAA,MAAM,IAAI,KAAK,CACb,CAAA,8CAAA,EAAiD,SAAS,4CAA4C,YAAY,CAAC,KAAK,CAAC,EAAE,EAC3H,EAAE,KAAK,EAAE,CACV;QACH;AACA,QAAA,IAAI,QAAQ,KAAK,SAAS,EAAE;AAC1B,YAAA,OAAO,QAAQ;QACjB;IACF;IAEA,MAAM,IAAI,KAAK,CACb,sEAAsE;QACpE,qEAAqE;QACrE,yEAAyE;AACzE,QAAA,cAAc,CACjB;AACH;AAEA;;;;;;;;;;;;AAYG;AACH,SAAS,UAAU,CAAC,UAAkB,EAAA;AACpC,IAAA,IAAI;AACF,QAAA,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC;IACjC;IAAE,OAAO,KAAK,EAAE;AACd,QAAA,IAAI,iBAAiB,CAAC,KAAK,CAAC,EAAE;AAC5B,YAAA,OAAO,SAAS;QAClB;;;;AAIA,QAAA,MAAM,KAAK;IACb;AACA,IAAA,OAAO,WAAW,CAAC,UAAU,CAAwB;AACvD;AAEA;;;;;;;;;;;;;;;;;;;AAmBG;AACH,SAAS,iBAAiB,CAAC,KAAc,EAAA;AACvC,IAAA,MAAM,IAAI,GAAI,KAA+C,EAAE,IAAI;IACnE,QACE,IAAI,KAAK,kBAAkB;AAC3B,QAAA,IAAI,KAAK,sBAAsB;QAC/B,IAAI,KAAK,+BAA+B;AAE5C;AAEA;AACA,SAAS,YAAY,CAAC,KAAc,EAAA;AAClC,IAAA,IAAI,KAAK,YAAY,KAAK,EAAE;QAC1B,OAAO,KAAK,CAAC,OAAO;IACtB;AACA,IAAA,OAAO,MAAM,CAAC,KAAK,CAAC;AACtB;;;;;"}
@@ -30,14 +30,33 @@ const UPSTREAM_CANDIDATES = [
30
30
  */
31
31
  function resolveUpstreamTransformer(customPath, load = tryRequire) {
32
32
  if (customPath !== undefined && customPath.length !== 0) {
33
- const upstream = load(customPath);
33
+ let upstream;
34
+ try {
35
+ upstream = load(customPath);
36
+ }
37
+ catch (cause) {
38
+ // The module resolves but failed while initializing (a top-level throw,
39
+ // a missing peer/transitive dependency, or a runtime-ABI rejection).
40
+ // Preserve the original diagnostic instead of masking it as absence.
41
+ throw new Error(`[@ttsc/metro] Failed to load the configured upstream transformer "${customPath}": ${errorMessage(cause)}`, { cause });
42
+ }
34
43
  if (upstream === undefined) {
35
44
  throw new Error(`[@ttsc/metro] Could not load the configured upstream transformer: ${customPath}`);
36
45
  }
37
46
  return upstream;
38
47
  }
39
48
  for (const candidate of UPSTREAM_CANDIDATES) {
40
- const upstream = load(candidate);
49
+ let upstream;
50
+ try {
51
+ upstream = load(candidate);
52
+ }
53
+ catch (cause) {
54
+ // A candidate that resolves but throws while initializing is a broken
55
+ // installation of the active stack, not an absent optional peer. Surface
56
+ // it rather than silently falling through to a candidate that does not
57
+ // match this project.
58
+ throw new Error(`[@ttsc/metro] The upstream Metro transformer "${candidate}" is installed but failed to initialize: ${errorMessage(cause)}`, { cause });
59
+ }
41
60
  if (upstream !== undefined) {
42
61
  return upstream;
43
62
  }
@@ -47,13 +66,66 @@ function resolveUpstreamTransformer(customPath, load = tryRequire) {
47
66
  "(React Native), or set the `upstreamTransformer` option to an explicit " +
48
67
  "module path.");
49
68
  }
69
+ /**
70
+ * Load an upstream transformer module, separating genuine absence from a broken
71
+ * installation.
72
+ *
73
+ * Resolution and execution are split deliberately. `require.resolve` only walks
74
+ * the module graph for the requested specifier; it never executes third-party
75
+ * code, so a failure there proves the requested candidate itself is not present
76
+ * — reported as `undefined` (absence) so automatic probing continues to the
77
+ * next optional peer. Once resolution succeeds, any error thrown by the actual
78
+ * `require` comes from executing the module body, including a missing peer or
79
+ * transitive dependency; that is a real initialization failure and is rethrown
80
+ * with its original message and stack so the caller can preserve it.
81
+ */
50
82
  function tryRequire(modulePath) {
51
83
  try {
52
- return nodeRequire(modulePath);
84
+ nodeRequire.resolve(modulePath);
85
+ }
86
+ catch (error) {
87
+ if (isCandidateAbsent(error)) {
88
+ return undefined;
89
+ }
90
+ // A resolution error that is not one of the known "entry point absent"
91
+ // codes (e.g. an invalid specifier) is not evidence of a plain absence;
92
+ // surface it rather than silently skipping the candidate.
93
+ throw error;
53
94
  }
54
- catch {
55
- return undefined;
95
+ return nodeRequire(modulePath);
96
+ }
97
+ /**
98
+ * Whether a resolution error means the requested candidate's entry point is not
99
+ * available here — i.e. genuine absence, not a broken initialization.
100
+ *
101
+ * Resolution never executes the module body, so a `require.resolve` failure can
102
+ * only concern the requested specifier, never a transitive import of it. Each
103
+ * recognised code says the same thing about that specifier:
104
+ *
105
+ * - `MODULE_NOT_FOUND` / `ERR_MODULE_NOT_FOUND` — the package or file itself is
106
+ * not installed (CJS and ESM loaders respectively).
107
+ * - `ERR_PACKAGE_PATH_NOT_EXPORTED` — the package is installed but the requested
108
+ * subpath is not exported (or its export target is missing). This matters for
109
+ * the `@expo/metro-config/babel-transformer` candidate, a package subpath:
110
+ * under Expo/React Native version skew a present but non-exporting package
111
+ * must stay non-fatal so auto-detection falls through to the next candidate,
112
+ * exactly as a wholly absent package does.
113
+ *
114
+ * An error thrown later, while the resolved module executes, is a real
115
+ * initialization failure and is never routed here — the caller preserves it.
116
+ */
117
+ function isCandidateAbsent(error) {
118
+ const code = error?.code;
119
+ return (code === "MODULE_NOT_FOUND" ||
120
+ code === "ERR_MODULE_NOT_FOUND" ||
121
+ code === "ERR_PACKAGE_PATH_NOT_EXPORTED");
122
+ }
123
+ /** Best-effort message extraction for wrapping an unknown thrown value. */
124
+ function errorMessage(error) {
125
+ if (error instanceof Error) {
126
+ return error.message;
56
127
  }
128
+ return String(error);
57
129
  }
58
130
 
59
131
  export { UPSTREAM_CANDIDATES, resolveUpstreamTransformer };
@@ -1 +1 @@
1
- {"version":3,"file":"upstream.mjs","sources":["../../src/core/upstream.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAEA,MAAM,WAAW,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;AAqBlD;;;;AAIG;AACI,MAAM,mBAAmB,GAAG;IACjC,sCAAsC;IACtC,uCAAuC;IACvC,sCAAsC;;AAGxC;;;;;;;;;;;;;;;;AAgBG;SACa,0BAA0B,CACxC,UAAmB,EACnB,OAAgE,UAAU,EAAA;IAE1E,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE;AACvD,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC;AACjC,QAAA,IAAI,QAAQ,KAAK,SAAS,EAAE;AAC1B,YAAA,MAAM,IAAI,KAAK,CACb,qEAAqE,UAAU,CAAA,CAAE,CAClF;QACH;AACA,QAAA,OAAO,QAAQ;IACjB;AAEA,IAAA,KAAK,MAAM,SAAS,IAAI,mBAAmB,EAAE;AAC3C,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;AAChC,QAAA,IAAI,QAAQ,KAAK,SAAS,EAAE;AAC1B,YAAA,OAAO,QAAQ;QACjB;IACF;IAEA,MAAM,IAAI,KAAK,CACb,sEAAsE;QACpE,qEAAqE;QACrE,yEAAyE;AACzE,QAAA,cAAc,CACjB;AACH;AAEA,SAAS,UAAU,CAAC,UAAkB,EAAA;AACpC,IAAA,IAAI;AACF,QAAA,OAAO,WAAW,CAAC,UAAU,CAAwB;IACvD;AAAE,IAAA,MAAM;AACN,QAAA,OAAO,SAAS;IAClB;AACF;;;;"}
1
+ {"version":3,"file":"upstream.mjs","sources":["../../src/core/upstream.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAEA,MAAM,WAAW,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;AAqBlD;;;;AAIG;AACI,MAAM,mBAAmB,GAAG;IACjC,sCAAsC;IACtC,uCAAuC;IACvC,sCAAsC;;AAGxC;;;;;;;;;;;;;;;;AAgBG;SACa,0BAA0B,CACxC,UAAmB,EACnB,OAAgE,UAAU,EAAA;IAE1E,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE;AACvD,QAAA,IAAI,QAAyC;AAC7C,QAAA,IAAI;AACF,YAAA,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC;QAC7B;QAAE,OAAO,KAAK,EAAE;;;;AAId,YAAA,MAAM,IAAI,KAAK,CACb,CAAA,kEAAA,EAAqE,UAAU,MAAM,YAAY,CAAC,KAAK,CAAC,EAAE,EAC1G,EAAE,KAAK,EAAE,CACV;QACH;AACA,QAAA,IAAI,QAAQ,KAAK,SAAS,EAAE;AAC1B,YAAA,MAAM,IAAI,KAAK,CACb,qEAAqE,UAAU,CAAA,CAAE,CAClF;QACH;AACA,QAAA,OAAO,QAAQ;IACjB;AAEA,IAAA,KAAK,MAAM,SAAS,IAAI,mBAAmB,EAAE;AAC3C,QAAA,IAAI,QAAyC;AAC7C,QAAA,IAAI;AACF,YAAA,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;QAC5B;QAAE,OAAO,KAAK,EAAE;;;;;AAKd,YAAA,MAAM,IAAI,KAAK,CACb,CAAA,8CAAA,EAAiD,SAAS,4CAA4C,YAAY,CAAC,KAAK,CAAC,EAAE,EAC3H,EAAE,KAAK,EAAE,CACV;QACH;AACA,QAAA,IAAI,QAAQ,KAAK,SAAS,EAAE;AAC1B,YAAA,OAAO,QAAQ;QACjB;IACF;IAEA,MAAM,IAAI,KAAK,CACb,sEAAsE;QACpE,qEAAqE;QACrE,yEAAyE;AACzE,QAAA,cAAc,CACjB;AACH;AAEA;;;;;;;;;;;;AAYG;AACH,SAAS,UAAU,CAAC,UAAkB,EAAA;AACpC,IAAA,IAAI;AACF,QAAA,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC;IACjC;IAAE,OAAO,KAAK,EAAE;AACd,QAAA,IAAI,iBAAiB,CAAC,KAAK,CAAC,EAAE;AAC5B,YAAA,OAAO,SAAS;QAClB;;;;AAIA,QAAA,MAAM,KAAK;IACb;AACA,IAAA,OAAO,WAAW,CAAC,UAAU,CAAwB;AACvD;AAEA;;;;;;;;;;;;;;;;;;;AAmBG;AACH,SAAS,iBAAiB,CAAC,KAAc,EAAA;AACvC,IAAA,MAAM,IAAI,GAAI,KAA+C,EAAE,IAAI;IACnE,QACE,IAAI,KAAK,kBAAkB;AAC3B,QAAA,IAAI,KAAK,sBAAsB;QAC/B,IAAI,KAAK,+BAA+B;AAE5C;AAEA;AACA,SAAS,YAAY,CAAC,KAAc,EAAA;AAClC,IAAA,IAAI,KAAK,YAAY,KAAK,EAAE;QAC1B,OAAO,KAAK,CAAC,OAAO;IACtB;AACA,IAAA,OAAO,MAAM,CAAC,KAAK,CAAC;AACtB;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/metro",
3
- "version": "0.18.4",
3
+ "version": "0.19.0",
4
4
  "description": "Metro (React Native / Expo) adapter for ttsc plugins.",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/index.mjs",
@@ -35,7 +35,7 @@
35
35
  "src"
36
36
  ],
37
37
  "dependencies": {
38
- "@ttsc/unplugin": "0.18.4"
38
+ "@ttsc/unplugin": "0.19.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@expo/metro-config": "*",
@@ -59,12 +59,10 @@
59
59
  "@types/node": "^25.3.0",
60
60
  "rimraf": "^6.1.2",
61
61
  "rollup": "^4.60.3",
62
- "rollup-plugin-auto-external": "^2.0.0",
63
- "rollup-plugin-node-externals": "^9.0.1",
64
62
  "tinyglobby": "^0.2.16",
65
63
  "tslib": "^2.8.1",
66
64
  "typescript": "^7.0.2",
67
- "ttsc": "0.18.4"
65
+ "ttsc": "0.19.0"
68
66
  },
69
67
  "repository": {
70
68
  "type": "git",
@@ -54,7 +54,18 @@ export function resolveUpstreamTransformer(
54
54
  load: (modulePath: string) => UpstreamTransformer | undefined = tryRequire,
55
55
  ): UpstreamTransformer {
56
56
  if (customPath !== undefined && customPath.length !== 0) {
57
- const upstream = load(customPath);
57
+ let upstream: UpstreamTransformer | undefined;
58
+ try {
59
+ upstream = load(customPath);
60
+ } catch (cause) {
61
+ // The module resolves but failed while initializing (a top-level throw,
62
+ // a missing peer/transitive dependency, or a runtime-ABI rejection).
63
+ // Preserve the original diagnostic instead of masking it as absence.
64
+ throw new Error(
65
+ `[@ttsc/metro] Failed to load the configured upstream transformer "${customPath}": ${errorMessage(cause)}`,
66
+ { cause },
67
+ );
68
+ }
58
69
  if (upstream === undefined) {
59
70
  throw new Error(
60
71
  `[@ttsc/metro] Could not load the configured upstream transformer: ${customPath}`,
@@ -64,7 +75,19 @@ export function resolveUpstreamTransformer(
64
75
  }
65
76
 
66
77
  for (const candidate of UPSTREAM_CANDIDATES) {
67
- const upstream = load(candidate);
78
+ let upstream: UpstreamTransformer | undefined;
79
+ try {
80
+ upstream = load(candidate);
81
+ } catch (cause) {
82
+ // A candidate that resolves but throws while initializing is a broken
83
+ // installation of the active stack, not an absent optional peer. Surface
84
+ // it rather than silently falling through to a candidate that does not
85
+ // match this project.
86
+ throw new Error(
87
+ `[@ttsc/metro] The upstream Metro transformer "${candidate}" is installed but failed to initialize: ${errorMessage(cause)}`,
88
+ { cause },
89
+ );
90
+ }
68
91
  if (upstream !== undefined) {
69
92
  return upstream;
70
93
  }
@@ -78,10 +101,67 @@ export function resolveUpstreamTransformer(
78
101
  );
79
102
  }
80
103
 
104
+ /**
105
+ * Load an upstream transformer module, separating genuine absence from a broken
106
+ * installation.
107
+ *
108
+ * Resolution and execution are split deliberately. `require.resolve` only walks
109
+ * the module graph for the requested specifier; it never executes third-party
110
+ * code, so a failure there proves the requested candidate itself is not present
111
+ * — reported as `undefined` (absence) so automatic probing continues to the
112
+ * next optional peer. Once resolution succeeds, any error thrown by the actual
113
+ * `require` comes from executing the module body, including a missing peer or
114
+ * transitive dependency; that is a real initialization failure and is rethrown
115
+ * with its original message and stack so the caller can preserve it.
116
+ */
81
117
  function tryRequire(modulePath: string): UpstreamTransformer | undefined {
82
118
  try {
83
- return nodeRequire(modulePath) as UpstreamTransformer;
84
- } catch {
85
- return undefined;
119
+ nodeRequire.resolve(modulePath);
120
+ } catch (error) {
121
+ if (isCandidateAbsent(error)) {
122
+ return undefined;
123
+ }
124
+ // A resolution error that is not one of the known "entry point absent"
125
+ // codes (e.g. an invalid specifier) is not evidence of a plain absence;
126
+ // surface it rather than silently skipping the candidate.
127
+ throw error;
128
+ }
129
+ return nodeRequire(modulePath) as UpstreamTransformer;
130
+ }
131
+
132
+ /**
133
+ * Whether a resolution error means the requested candidate's entry point is not
134
+ * available here — i.e. genuine absence, not a broken initialization.
135
+ *
136
+ * Resolution never executes the module body, so a `require.resolve` failure can
137
+ * only concern the requested specifier, never a transitive import of it. Each
138
+ * recognised code says the same thing about that specifier:
139
+ *
140
+ * - `MODULE_NOT_FOUND` / `ERR_MODULE_NOT_FOUND` — the package or file itself is
141
+ * not installed (CJS and ESM loaders respectively).
142
+ * - `ERR_PACKAGE_PATH_NOT_EXPORTED` — the package is installed but the requested
143
+ * subpath is not exported (or its export target is missing). This matters for
144
+ * the `@expo/metro-config/babel-transformer` candidate, a package subpath:
145
+ * under Expo/React Native version skew a present but non-exporting package
146
+ * must stay non-fatal so auto-detection falls through to the next candidate,
147
+ * exactly as a wholly absent package does.
148
+ *
149
+ * An error thrown later, while the resolved module executes, is a real
150
+ * initialization failure and is never routed here — the caller preserves it.
151
+ */
152
+ function isCandidateAbsent(error: unknown): boolean {
153
+ const code = (error as { code?: unknown } | null | undefined)?.code;
154
+ return (
155
+ code === "MODULE_NOT_FOUND" ||
156
+ code === "ERR_MODULE_NOT_FOUND" ||
157
+ code === "ERR_PACKAGE_PATH_NOT_EXPORTED"
158
+ );
159
+ }
160
+
161
+ /** Best-effort message extraction for wrapping an unknown thrown value. */
162
+ function errorMessage(error: unknown): string {
163
+ if (error instanceof Error) {
164
+ return error.message;
86
165
  }
166
+ return String(error);
87
167
  }