@ttsc/metro 0.16.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jeongho Nam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # `@ttsc/metro`
2
+
3
+ ![banner of @ttsc/metro](https://ttsc.dev/og.jpg)
4
+
5
+ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/ttsc/blob/master/LICENSE) [![NPM Version](https://img.shields.io/npm/v/@ttsc/metro.svg)](https://www.npmjs.com/package/@ttsc/metro) [![NPM Downloads](https://img.shields.io/npm/dm/@ttsc/metro.svg)](https://www.npmjs.com/package/@ttsc/metro) [![Build Status](https://github.com/samchon/ttsc/workflows/test/badge.svg)](https://github.com/samchon/ttsc/actions?query=workflow%3Atest) [![Guide Documents](https://img.shields.io/badge/Guide-Documents-forestgreen)](https://ttsc.dev/docs) [![Discord Badge](https://img.shields.io/badge/discord-samchon-d91965?style=flat&labelColor=5866f2&logo=discord&logoColor=white&link=https://discord.gg/E94XhzrUCZ)](https://discord.gg/E94XhzrUCZ)
6
+
7
+ Metro (React Native / Expo) adapter for `ttsc` plugins.
8
+
9
+ React Native and Expo bundle with **Metro**, which transpiles each file with Babel (`babel-preset-expo` / `@react-native/metro-babel-transformer`). Babel strips TypeScript types and never runs TypeScript transformers, so neither the `ttsc` CLI nor `@ttsc/unplugin` can reach an RN/Expo build. `@ttsc/metro` wires a Metro custom transformer that runs the `ttsc` plugin pass (typia, nestia, …) on each TypeScript file, then hands the result to your existing Expo/React-Native Babel transformer.
10
+
11
+ ## Setup
12
+
13
+ Install `ttsc` and TypeScript-Go first. Then install the Metro adapter:
14
+
15
+ ```bash
16
+ npm install -D ttsc typescript@rc
17
+ npm install -D @ttsc/metro
18
+ ```
19
+
20
+ Wrap your Metro config with `withTtsc`.
21
+
22
+ ### Expo
23
+
24
+ ```js
25
+ // metro.config.js
26
+ const { getDefaultConfig } = require("expo/metro-config");
27
+ const { withTtsc } = require("@ttsc/metro");
28
+
29
+ module.exports = withTtsc(getDefaultConfig(__dirname));
30
+ ```
31
+
32
+ ### Bare React Native
33
+
34
+ ```js
35
+ // metro.config.js
36
+ const { getDefaultConfig } = require("@react-native/metro-config");
37
+ const { withTtsc } = require("@ttsc/metro");
38
+
39
+ module.exports = withTtsc(getDefaultConfig(__dirname));
40
+ ```
41
+
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
+
44
+ ## Configuration
45
+
46
+ 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.
47
+
48
+ Options are the second argument and mirror `@ttsc/unplugin`, plus a few Metro-specific knobs:
49
+
50
+ ```js
51
+ module.exports = withTtsc(getDefaultConfig(__dirname), {
52
+ project: "tsconfig.build.json",
53
+ plugins: [{ transform: "typia/lib/transform" }],
54
+ exclude: ["__tests__"],
55
+ });
56
+ ```
57
+
58
+ - `project`: path to the `tsconfig.json` the transformer should read (resolved from `process.cwd()`).
59
+ - `compilerOptions`: a temporary overlay layered on the selected project config.
60
+ - `plugins`: an explicit `ttsc` plugin list override, or `false` to disable project plugins.
61
+ - `upstreamTransformer`: an explicit module path for the Babel transformer to delegate to, when auto-detection is not what you want.
62
+ - `include` / `exclude`: substring patterns matched against the project-relative file path, selecting which files run through the `ttsc` pass (`.ts`/`.tsx`/`.cts`/`.mts` only; declaration and JavaScript files always pass straight through).
63
+
64
+ Options are forwarded from the Metro **config** process to Metro's **worker** processes through the `TTSC_METRO_OPTIONS` environment variable, so they must stay JSON-serialisable (hence substring patterns rather than `RegExp`).
65
+
66
+ ## How it works
67
+
68
+ For each TypeScript file Metro asks to transform:
69
+
70
+ 1. `@ttsc/metro` runs the `ttsc` plugin pass (reusing `@ttsc/unplugin`'s transform core) → transformed **TypeScript** source.
71
+ 2. The transformed source is handed to the upstream Expo/React-Native Babel transformer, which strips types, applies the RN transforms, and returns the Babel AST Metro consumes.
72
+
73
+ The plugin contract, `tsconfig` discovery, and per-build cache are identical to every other `ttsc` bundler integration.
74
+
75
+ ## Caveats (v1)
76
+
77
+ - **Cost model.** This release reuses `@ttsc/unplugin`'s transform core, which type-checks the whole `tsconfig` project and caches the result per process. Metro runs transforms in a multi-process worker pool, so the project is compiled once per worker (on that worker's first file). A resident, incremental, per-file compiler shared across workers is the planned optimization, tracked in [samchon/ttsc#255](https://github.com/samchon/ttsc/issues/255).
78
+ - **Cache invalidation.** Metro keys its transform cache on per-file content plus a static transformer key. A `ttsc` transform can depend on a _type_ in another file; editing that type does not change the dependent file's content, so Metro may serve a stale transform. After changing `tsconfig`/plugin configuration or a depended-upon type, restart Metro with `--reset-cache`.
79
+ - **Type errors fail the build.** The `ttsc` pass type-checks; a project type error surfaces as a Metro build error, matching the other `ttsc` bundler integrations.
80
+
81
+ ## Sponsors
82
+
83
+ [![Sponsors](https://raw.githubusercontent.com/samchon/sponsor-images/refs/heads/master/public/circle.svg)](https://github.com/sponsors/samchon)
84
+
85
+ Thanks for your support.
86
+
87
+ Your [donation](https://github.com/sponsors/samchon) encourages `ttsc` development.
88
+
89
+ ## References
90
+
91
+ Inspired by [`@elliots/metro-transformer-typical`](https://github.com/elliots/typical/tree/main/packages/metro-transformer).
@@ -0,0 +1,77 @@
1
+ import type { TtscUnpluginOptions } from "@ttsc/unplugin/api";
2
+ /**
3
+ * Options accepted by {@link withTtsc} and the Metro transformer.
4
+ *
5
+ * The `project` / `compilerOptions` / `plugins` fields are inherited from
6
+ * `@ttsc/unplugin` so the Metro adapter speaks the exact same configuration
7
+ * language as every other bundler integration. The remaining fields are
8
+ * Metro-specific.
9
+ *
10
+ * Every field is JSON-serialisable on purpose: `withTtsc` runs in the Metro
11
+ * **config** process, but the transformer runs in Metro's **worker** processes,
12
+ * so the resolved options have to survive a structured-clone / env round-trip
13
+ * to reach them (see {@link serializeOptions}). That is why `include`/`exclude`
14
+ * are plain substring patterns rather than `RegExp`.
15
+ */
16
+ export interface TtscMetroOptions extends TtscUnpluginOptions {
17
+ /**
18
+ * Explicit module path of the upstream Metro Babel transformer to delegate to
19
+ * after the ttsc pass.
20
+ *
21
+ * When omitted it is auto-detected: `@expo/metro-config/babel-transformer`
22
+ * first (Expo), then `@react-native/metro-babel-transformer`, then the legacy
23
+ * `metro-react-native-babel-transformer`.
24
+ */
25
+ upstreamTransformer?: string;
26
+ /**
27
+ * Substring patterns; when non-empty only files whose path contains one of
28
+ * them are run through the ttsc pass. Non-matching files are passed straight
29
+ * to the upstream transformer.
30
+ */
31
+ include?: string[];
32
+ /**
33
+ * Substring patterns; files whose path contains one of them skip the ttsc
34
+ * pass and go straight to the upstream transformer. Applied after
35
+ * {@link include}.
36
+ */
37
+ exclude?: string[];
38
+ }
39
+ /**
40
+ * Fully-resolved options, split into the ttsc-side overlay and Metro-side
41
+ * knobs.
42
+ */
43
+ export interface ResolvedTtscMetroOptions {
44
+ /** Options forwarded verbatim to the `@ttsc/unplugin` transform core. */
45
+ ttsc: TtscUnpluginOptions;
46
+ /** Explicit upstream transformer module path, or `undefined` to auto-detect. */
47
+ upstreamTransformer?: string;
48
+ /** Resolved include patterns (never `undefined`). */
49
+ include: string[];
50
+ /** Resolved exclude patterns (never `undefined`). */
51
+ exclude: string[];
52
+ }
53
+ /**
54
+ * Environment variable that carries the resolved options from the Metro config
55
+ * process to the worker processes.
56
+ *
57
+ * Metro forks its transform workers (jest-worker) from the process that loaded
58
+ * `metro.config.js`, so a variable set on `process.env` before Metro boots is
59
+ * inherited by every worker. This is the only channel `withTtsc`'s arguments
60
+ * can reach the transformer through — the worker never sees the `withTtsc`
61
+ * call.
62
+ */
63
+ export declare const ENV_KEY = "TTSC_METRO_OPTIONS";
64
+ /**
65
+ * Serialise user options for transport to the worker processes via
66
+ * {@link ENV_KEY}.
67
+ */
68
+ export declare function serializeOptions(options: TtscMetroOptions): string;
69
+ /**
70
+ * Reconstruct the resolved options inside a worker process.
71
+ *
72
+ * Reads {@link ENV_KEY}; when it is unset or malformed the adapter falls back to
73
+ * defaults, which means "auto-discover `tsconfig.json` and read its configured
74
+ * plugins" — the standard ttsc behaviour, and the right thing for a project
75
+ * that called `withTtsc(config)` with no explicit options.
76
+ */
77
+ export declare function resolveOptionsFromEnv(): ResolvedTtscMetroOptions;
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Environment variable that carries the resolved options from the Metro config
5
+ * process to the worker processes.
6
+ *
7
+ * Metro forks its transform workers (jest-worker) from the process that loaded
8
+ * `metro.config.js`, so a variable set on `process.env` before Metro boots is
9
+ * inherited by every worker. This is the only channel `withTtsc`'s arguments
10
+ * can reach the transformer through — the worker never sees the `withTtsc`
11
+ * call.
12
+ */
13
+ const ENV_KEY = "TTSC_METRO_OPTIONS";
14
+ /**
15
+ * Serialise user options for transport to the worker processes via
16
+ * {@link ENV_KEY}.
17
+ */
18
+ function serializeOptions(options) {
19
+ return JSON.stringify(options ?? {});
20
+ }
21
+ /**
22
+ * Reconstruct the resolved options inside a worker process.
23
+ *
24
+ * Reads {@link ENV_KEY}; when it is unset or malformed the adapter falls back to
25
+ * defaults, which means "auto-discover `tsconfig.json` and read its configured
26
+ * plugins" — the standard ttsc behaviour, and the right thing for a project
27
+ * that called `withTtsc(config)` with no explicit options.
28
+ */
29
+ function resolveOptionsFromEnv() {
30
+ const raw = process.env[ENV_KEY];
31
+ const parsed = parse(raw);
32
+ return {
33
+ ttsc: {
34
+ project: parsed.project,
35
+ compilerOptions: parsed.compilerOptions,
36
+ ...("plugins" in parsed ? { plugins: parsed.plugins } : {}),
37
+ },
38
+ upstreamTransformer: typeof parsed.upstreamTransformer === "string"
39
+ ? parsed.upstreamTransformer
40
+ : undefined,
41
+ include: toStringArray(parsed.include),
42
+ exclude: toStringArray(parsed.exclude),
43
+ };
44
+ }
45
+ function parse(raw) {
46
+ if (raw === undefined || raw.length === 0) {
47
+ return {};
48
+ }
49
+ try {
50
+ const value = JSON.parse(raw);
51
+ // Only a plain object is a valid payload; arrays, `null`, numbers, strings,
52
+ // and booleans (all valid JSON) degrade to defaults rather than leaking a
53
+ // wrong-shaped value downstream.
54
+ return typeof value === "object" && value !== null && !Array.isArray(value)
55
+ ? value
56
+ : {};
57
+ }
58
+ catch {
59
+ return {};
60
+ }
61
+ }
62
+ /**
63
+ * Coerce an untrusted env value into a `string[]`. A non-array (e.g. the common
64
+ * mistake of passing a bare string for `include`/`exclude`) becomes `[]` so the
65
+ * worker never calls `.some` on a non-array and crashes.
66
+ */
67
+ function toStringArray(value) {
68
+ return Array.isArray(value)
69
+ ? value.filter((entry) => typeof entry === "string")
70
+ : [];
71
+ }
72
+
73
+ exports.ENV_KEY = ENV_KEY;
74
+ exports.resolveOptionsFromEnv = resolveOptionsFromEnv;
75
+ exports.serializeOptions = serializeOptions;
76
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.js","sources":["../../src/core/options.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAyDA;;;;;;;;;AASG;AACI,MAAM,OAAO,GAAG;AAEvB;;;AAGG;AACG,SAAU,gBAAgB,CAAC,OAAyB,EAAA;IACxD,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,IAAI,EAAE,CAAC;AACtC;AAEA;;;;;;;AAOG;SACa,qBAAqB,GAAA;IACnC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;AAChC,IAAA,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC;IACzB,OAAO;AACL,QAAA,IAAI,EAAE;YACJ,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,eAAe,EAAE,MAAM,CAAC,eAAe;AACvC,YAAA,IAAI,SAAS,IAAI,MAAM,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC;AAC5D,SAAA;AACD,QAAA,mBAAmB,EACjB,OAAO,MAAM,CAAC,mBAAmB,KAAK;cAClC,MAAM,CAAC;AACT,cAAE,SAAS;AACf,QAAA,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC;AACtC,QAAA,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC;KACvC;AACH;AAEA,SAAS,KAAK,CAAC,GAAuB,EAAA;IACpC,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE;AACzC,QAAA,OAAO,EAAE;IACX;AACA,IAAA,IAAI;QACF,MAAM,KAAK,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;;;;AAItC,QAAA,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK;AACxE,cAAG;cACD,EAAE;IACR;AAAE,IAAA,MAAM;AACN,QAAA,OAAO,EAAE;IACX;AACF;AAEA;;;;AAIG;AACH,SAAS,aAAa,CAAC,KAAc,EAAA;AACnC,IAAA,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK;AACxB,UAAE,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,KAAsB,OAAO,KAAK,KAAK,QAAQ;UAClE,EAAE;AACR;;;;;;"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Environment variable that carries the resolved options from the Metro config
3
+ * process to the worker processes.
4
+ *
5
+ * Metro forks its transform workers (jest-worker) from the process that loaded
6
+ * `metro.config.js`, so a variable set on `process.env` before Metro boots is
7
+ * inherited by every worker. This is the only channel `withTtsc`'s arguments
8
+ * can reach the transformer through — the worker never sees the `withTtsc`
9
+ * call.
10
+ */
11
+ const ENV_KEY = "TTSC_METRO_OPTIONS";
12
+ /**
13
+ * Serialise user options for transport to the worker processes via
14
+ * {@link ENV_KEY}.
15
+ */
16
+ function serializeOptions(options) {
17
+ return JSON.stringify(options ?? {});
18
+ }
19
+ /**
20
+ * Reconstruct the resolved options inside a worker process.
21
+ *
22
+ * Reads {@link ENV_KEY}; when it is unset or malformed the adapter falls back to
23
+ * defaults, which means "auto-discover `tsconfig.json` and read its configured
24
+ * plugins" — the standard ttsc behaviour, and the right thing for a project
25
+ * that called `withTtsc(config)` with no explicit options.
26
+ */
27
+ function resolveOptionsFromEnv() {
28
+ const raw = process.env[ENV_KEY];
29
+ const parsed = parse(raw);
30
+ return {
31
+ ttsc: {
32
+ project: parsed.project,
33
+ compilerOptions: parsed.compilerOptions,
34
+ ...("plugins" in parsed ? { plugins: parsed.plugins } : {}),
35
+ },
36
+ upstreamTransformer: typeof parsed.upstreamTransformer === "string"
37
+ ? parsed.upstreamTransformer
38
+ : undefined,
39
+ include: toStringArray(parsed.include),
40
+ exclude: toStringArray(parsed.exclude),
41
+ };
42
+ }
43
+ function parse(raw) {
44
+ if (raw === undefined || raw.length === 0) {
45
+ return {};
46
+ }
47
+ try {
48
+ const value = JSON.parse(raw);
49
+ // Only a plain object is a valid payload; arrays, `null`, numbers, strings,
50
+ // and booleans (all valid JSON) degrade to defaults rather than leaking a
51
+ // wrong-shaped value downstream.
52
+ return typeof value === "object" && value !== null && !Array.isArray(value)
53
+ ? value
54
+ : {};
55
+ }
56
+ catch {
57
+ return {};
58
+ }
59
+ }
60
+ /**
61
+ * Coerce an untrusted env value into a `string[]`. A non-array (e.g. the common
62
+ * mistake of passing a bare string for `include`/`exclude`) becomes `[]` so the
63
+ * worker never calls `.some` on a non-array and crashes.
64
+ */
65
+ function toStringArray(value) {
66
+ return Array.isArray(value)
67
+ ? value.filter((entry) => typeof entry === "string")
68
+ : [];
69
+ }
70
+
71
+ export { ENV_KEY, resolveOptionsFromEnv, serializeOptions };
72
+ //# sourceMappingURL=options.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.mjs","sources":["../../src/core/options.ts"],"sourcesContent":[null],"names":[],"mappings":"AAyDA;;;;;;;;;AASG;AACI,MAAM,OAAO,GAAG;AAEvB;;;AAGG;AACG,SAAU,gBAAgB,CAAC,OAAyB,EAAA;IACxD,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,IAAI,EAAE,CAAC;AACtC;AAEA;;;;;;;AAOG;SACa,qBAAqB,GAAA;IACnC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;AAChC,IAAA,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC;IACzB,OAAO;AACL,QAAA,IAAI,EAAE;YACJ,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,eAAe,EAAE,MAAM,CAAC,eAAe;AACvC,YAAA,IAAI,SAAS,IAAI,MAAM,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC;AAC5D,SAAA;AACD,QAAA,mBAAmB,EACjB,OAAO,MAAM,CAAC,mBAAmB,KAAK;cAClC,MAAM,CAAC;AACT,cAAE,SAAS;AACf,QAAA,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC;AACtC,QAAA,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC;KACvC;AACH;AAEA,SAAS,KAAK,CAAC,GAAuB,EAAA;IACpC,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE;AACzC,QAAA,OAAO,EAAE;IACX;AACA,IAAA,IAAI;QACF,MAAM,KAAK,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;;;;AAItC,QAAA,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK;AACxE,cAAG;cACD,EAAE;IACR;AAAE,IAAA,MAAM;AACN,QAAA,OAAO,EAAE;IACX;AACF;AAEA;;;;AAIG;AACH,SAAS,aAAa,CAAC,KAAc,EAAA;AACnC,IAAA,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK;AACxB,UAAE,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,KAAsB,OAAO,KAAK,KAAK,QAAQ;UAClE,EAAE;AACR;;;;"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The subset of the Metro Babel-transformer contract this adapter relies on.
3
+ *
4
+ * Metro loads the module named by `transformer.babelTransformerPath` and calls
5
+ * its `transform` once per file, expecting a Babel AST back. `getCacheKey` is
6
+ * an optional export Metro folds into its transform-cache key; Metro invokes it
7
+ * with arguments (e.g. `{ projectRoot, enableBabelRCLookup }`), so it is typed
8
+ * variadic.
9
+ */
10
+ export interface UpstreamTransformer {
11
+ transform(params: {
12
+ src: string;
13
+ filename: string;
14
+ options: Record<string, unknown>;
15
+ [key: string]: unknown;
16
+ }): Promise<{
17
+ ast: object;
18
+ }>;
19
+ getCacheKey?: (...args: unknown[]) => string;
20
+ }
21
+ /**
22
+ * Upstream transformer module specifiers tried (in order) when no explicit
23
+ * `upstreamTransformer` is configured: Expo first, then modern bare React
24
+ * Native, then the legacy package.
25
+ */
26
+ export declare const UPSTREAM_CANDIDATES: readonly ["@expo/metro-config/babel-transformer", "@react-native/metro-babel-transformer", "metro-react-native-babel-transformer"];
27
+ /**
28
+ * Resolve the upstream Metro Babel transformer to delegate to.
29
+ *
30
+ * Detection order, most specific first:
31
+ *
32
+ * 1. An explicit `customPath` (the `upstreamTransformer` option);
33
+ * 2. Each of {@link UPSTREAM_CANDIDATES} in turn.
34
+ *
35
+ * These are declared as optional peers and resolved at runtime against the
36
+ * consumer project, so the adapter carries no Metro/Expo dependency itself.
37
+ * Resolution is not memoised — Node's own module cache already makes the
38
+ * repeated `require` a cheap lookup, and keeping no module-level state lets a
39
+ * changed `upstreamTransformer` always take effect.
40
+ *
41
+ * `load` is injectable purely so the resolution order and the not-found path
42
+ * can be tested deterministically; production always uses the real `require`.
43
+ */
44
+ export declare function resolveUpstreamTransformer(customPath?: string, load?: (modulePath: string) => UpstreamTransformer | undefined): UpstreamTransformer;
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ var node_module = require('node:module');
4
+
5
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
6
+ const nodeRequire = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('core/upstream.js', document.baseURI).href)));
7
+ /**
8
+ * Upstream transformer module specifiers tried (in order) when no explicit
9
+ * `upstreamTransformer` is configured: Expo first, then modern bare React
10
+ * Native, then the legacy package.
11
+ */
12
+ const UPSTREAM_CANDIDATES = [
13
+ "@expo/metro-config/babel-transformer",
14
+ "@react-native/metro-babel-transformer",
15
+ "metro-react-native-babel-transformer",
16
+ ];
17
+ /**
18
+ * Resolve the upstream Metro Babel transformer to delegate to.
19
+ *
20
+ * Detection order, most specific first:
21
+ *
22
+ * 1. An explicit `customPath` (the `upstreamTransformer` option);
23
+ * 2. Each of {@link UPSTREAM_CANDIDATES} in turn.
24
+ *
25
+ * These are declared as optional peers and resolved at runtime against the
26
+ * consumer project, so the adapter carries no Metro/Expo dependency itself.
27
+ * Resolution is not memoised — Node's own module cache already makes the
28
+ * repeated `require` a cheap lookup, and keeping no module-level state lets a
29
+ * changed `upstreamTransformer` always take effect.
30
+ *
31
+ * `load` is injectable purely so the resolution order and the not-found path
32
+ * can be tested deterministically; production always uses the real `require`.
33
+ */
34
+ function resolveUpstreamTransformer(customPath, load = tryRequire) {
35
+ if (customPath !== undefined && customPath.length !== 0) {
36
+ const upstream = load(customPath);
37
+ if (upstream === undefined) {
38
+ throw new Error(`[@ttsc/metro] Could not load the configured upstream transformer: ${customPath}`);
39
+ }
40
+ return upstream;
41
+ }
42
+ for (const candidate of UPSTREAM_CANDIDATES) {
43
+ const upstream = load(candidate);
44
+ if (upstream !== undefined) {
45
+ return upstream;
46
+ }
47
+ }
48
+ throw new Error("[@ttsc/metro] Could not find an upstream Metro transformer. Install " +
49
+ "@expo/metro-config (Expo) or @react-native/metro-babel-transformer " +
50
+ "(React Native), or set the `upstreamTransformer` option to an explicit " +
51
+ "module path.");
52
+ }
53
+ function tryRequire(modulePath) {
54
+ try {
55
+ return nodeRequire(modulePath);
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+
62
+ exports.UPSTREAM_CANDIDATES = UPSTREAM_CANDIDATES;
63
+ exports.resolveUpstreamTransformer = resolveUpstreamTransformer;
64
+ //# sourceMappingURL=upstream.js.map
@@ -0,0 +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;;;;;"}
@@ -0,0 +1,60 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const nodeRequire = createRequire(import.meta.url);
4
+ /**
5
+ * Upstream transformer module specifiers tried (in order) when no explicit
6
+ * `upstreamTransformer` is configured: Expo first, then modern bare React
7
+ * Native, then the legacy package.
8
+ */
9
+ const UPSTREAM_CANDIDATES = [
10
+ "@expo/metro-config/babel-transformer",
11
+ "@react-native/metro-babel-transformer",
12
+ "metro-react-native-babel-transformer",
13
+ ];
14
+ /**
15
+ * Resolve the upstream Metro Babel transformer to delegate to.
16
+ *
17
+ * Detection order, most specific first:
18
+ *
19
+ * 1. An explicit `customPath` (the `upstreamTransformer` option);
20
+ * 2. Each of {@link UPSTREAM_CANDIDATES} in turn.
21
+ *
22
+ * These are declared as optional peers and resolved at runtime against the
23
+ * consumer project, so the adapter carries no Metro/Expo dependency itself.
24
+ * Resolution is not memoised — Node's own module cache already makes the
25
+ * repeated `require` a cheap lookup, and keeping no module-level state lets a
26
+ * changed `upstreamTransformer` always take effect.
27
+ *
28
+ * `load` is injectable purely so the resolution order and the not-found path
29
+ * can be tested deterministically; production always uses the real `require`.
30
+ */
31
+ function resolveUpstreamTransformer(customPath, load = tryRequire) {
32
+ if (customPath !== undefined && customPath.length !== 0) {
33
+ const upstream = load(customPath);
34
+ if (upstream === undefined) {
35
+ throw new Error(`[@ttsc/metro] Could not load the configured upstream transformer: ${customPath}`);
36
+ }
37
+ return upstream;
38
+ }
39
+ for (const candidate of UPSTREAM_CANDIDATES) {
40
+ const upstream = load(candidate);
41
+ if (upstream !== undefined) {
42
+ return upstream;
43
+ }
44
+ }
45
+ throw new Error("[@ttsc/metro] Could not find an upstream Metro transformer. Install " +
46
+ "@expo/metro-config (Expo) or @react-native/metro-babel-transformer " +
47
+ "(React Native), or set the `upstreamTransformer` option to an explicit " +
48
+ "module path.");
49
+ }
50
+ function tryRequire(modulePath) {
51
+ try {
52
+ return nodeRequire(modulePath);
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
58
+
59
+ export { UPSTREAM_CANDIDATES, resolveUpstreamTransformer };
60
+ //# sourceMappingURL=upstream.mjs.map
@@ -0,0 +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;;;;"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { TtscMetroOptions } from "./core/options";
2
+ export type { ResolvedTtscMetroOptions, TtscMetroOptions, } from "./core/options";
3
+ /**
4
+ * Minimal structural type for a Metro config object — avoids a hard dependency
5
+ * on Metro's types while letting {@link withTtsc} preserve the caller's exact
6
+ * config type.
7
+ */
8
+ interface MetroConfigLike {
9
+ transformer?: {
10
+ babelTransformerPath?: string;
11
+ [key: string]: unknown;
12
+ };
13
+ [key: string]: unknown;
14
+ }
15
+ /**
16
+ * Wrap a Metro config so ttsc plugins run on every TypeScript file.
17
+ *
18
+ * Sets `transformer.babelTransformerPath` to this package's transformer and
19
+ * publishes the resolved options to Metro's worker processes via the
20
+ * {@link ENV_KEY} environment variable (the workers never see this call, so env
21
+ * is the transport — see `core/options.ts`). Compatible with Expo's
22
+ * `getDefaultConfig()` and bare React Native alike.
23
+ *
24
+ * With no `options`, the transformer auto-discovers `tsconfig.json` and runs
25
+ * the plugins configured there — the standard ttsc model. Pass `options` only
26
+ * to override the project path, plugin list, or include/exclude filters.
27
+ */
28
+ export declare function withTtsc<T extends MetroConfigLike>(config: T, options?: TtscMetroOptions): T;
package/lib/index.js ADDED
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ var path = require('node:path');
4
+ var node_url = require('node:url');
5
+ var options = require('./core/options.js');
6
+
7
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
8
+ /**
9
+ * `@ttsc/metro` — Metro (React Native / Expo) adapter for ttsc plugins.
10
+ *
11
+ * Metro bundles with Babel, which strips TypeScript types and never runs ttsc
12
+ * plugins, so neither the `ttsc` CLI nor `@ttsc/unplugin` can reach an RN/Expo
13
+ * build. {@link withTtsc} wires a Metro custom transformer that runs the ttsc
14
+ * plugin pass on each TypeScript file before handing the result to the
15
+ * project's existing Expo/React-Native Babel transformer.
16
+ *
17
+ * @example
18
+ * Expo```js
19
+ * // metro.config.js
20
+ * const { getDefaultConfig } = require("expo/metro-config");
21
+ * const { withTtsc } = require("@ttsc/metro");
22
+ *
23
+ * module.exports = withTtsc(getDefaultConfig(__dirname));
24
+ * ```;
25
+ *
26
+ * @example
27
+ * Bare React Native
28
+ * ```js
29
+ * // metro.config.js
30
+ * const { getDefaultConfig } = require("@react-native/metro-config");
31
+ * const { withTtsc } = require("@ttsc/metro");
32
+ *
33
+ * module.exports = withTtsc(getDefaultConfig(__dirname));
34
+ * ```
35
+ */
36
+ /**
37
+ * Wrap a Metro config so ttsc plugins run on every TypeScript file.
38
+ *
39
+ * Sets `transformer.babelTransformerPath` to this package's transformer and
40
+ * publishes the resolved options to Metro's worker processes via the
41
+ * {@link ENV_KEY} environment variable (the workers never see this call, so env
42
+ * is the transport — see `core/options.ts`). Compatible with Expo's
43
+ * `getDefaultConfig()` and bare React Native alike.
44
+ *
45
+ * With no `options`, the transformer auto-discovers `tsconfig.json` and runs
46
+ * the plugins configured there — the standard ttsc model. Pass `options` only
47
+ * to override the project path, plugin list, or include/exclude filters.
48
+ */
49
+ function withTtsc(config, options$1 = {}) {
50
+ process.env[options.ENV_KEY] = options.serializeOptions(options$1);
51
+ return {
52
+ ...config,
53
+ transformer: {
54
+ ...config.transformer,
55
+ babelTransformerPath: transformerModulePath(),
56
+ },
57
+ };
58
+ }
59
+ /**
60
+ * Absolute path to the built transformer module Metro will `require`.
61
+ *
62
+ * Always the CommonJS build (`transformer.js`) next to this module: Metro
63
+ * resolves `babelTransformerPath` with `require`, and `metro.config.js` is a
64
+ * CommonJS module. Rollup rewrites `import.meta.url` for both the CJS and ESM
65
+ * builds, so this resolves correctly regardless of how the config loaded this
66
+ * entry.
67
+ */
68
+ function transformerModulePath() {
69
+ return path.join(path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.js', document.baseURI).href)))), "transformer.js");
70
+ }
71
+
72
+ exports.withTtsc = withTtsc;
73
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":[null],"names":["options","ENV_KEY","serializeOptions","join","dirname","fileURLToPath"],"mappings":";;;;;;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BG;AAyBH;;;;;;;;;;;;AAYG;SACa,QAAQ,CACtB,MAAS,EACTA,YAA4B,EAAE,EAAA;IAE9B,OAAO,CAAC,GAAG,CAACC,eAAO,CAAC,GAAGC,wBAAgB,CAACF,SAAO,CAAC;IAChD,OAAO;AACL,QAAA,GAAG,MAAM;AACT,QAAA,WAAW,EAAE;YACX,GAAG,MAAM,CAAC,WAAW;YACrB,oBAAoB,EAAE,qBAAqB,EAAE;AAC9C,SAAA;KACG;AACR;AAEA;;;;;;;;AAQG;AACH,SAAS,qBAAqB,GAAA;AAC5B,IAAA,OAAOG,SAAI,CAACC,YAAO,CAACC,sBAAa,CAAC,0PAAe,CAAC,CAAC,EAAE,gBAAgB,CAAC;AACxE;;;;"}