@flareapp/react-native-sourcemaps 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,257 @@
1
+ # @flareapp/react-native-sourcemaps
2
+
3
+ Upload React Native (Metro) JavaScript sourcemaps to Flare so production stack
4
+ traces are symbolicated.
5
+
6
+ It has two halves that share one version string:
7
+
8
+ 1. A **Babel plugin** that inlines a build-time version into your app bundle.
9
+ 2. A **CLI** (`flare-rn-sourcemaps upload`) that uploads the `.map` under that
10
+ same version.
11
+
12
+ > The manual CLI flow below works everywhere. For **bare React Native** you can also
13
+ > wire the upload into the native release build so it happens automatically — see
14
+ > "Automatic upload (bare React Native)" near the end. The Expo config plugin ships
15
+ > separately.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install --save-dev @flareapp/react-native-sourcemaps
21
+ ```
22
+
23
+ ## 1. Inline the version into your app
24
+
25
+ Add the Babel plugin to `babel.config.js`:
26
+
27
+ ```js
28
+ module.exports = {
29
+ presets: ['module:@react-native/babel-preset'],
30
+ plugins: ['@flareapp/react-native-sourcemaps/babel'],
31
+ };
32
+ ```
33
+
34
+ Then pass the inlined version when configuring Flare. Import `flareSourcemapVersion`
35
+ from the package's runtime entry — it's a typed `string`, so there is no `process`
36
+ global to type and no `@types/node` to add:
37
+
38
+ ```ts
39
+ import { flare } from '@flareapp/react-native';
40
+ import { flareSourcemapVersion } from '@flareapp/react-native-sourcemaps/runtime';
41
+
42
+ flare.light(FLARE_KEY).configure({ sourcemapVersionId: flareSourcemapVersion });
43
+ ```
44
+
45
+ The plugin replaces every reference to `flareSourcemapVersion` with the version it
46
+ resolves at bundle time (and removes the import, so nothing of this package ships in
47
+ your app bundle). Without the plugin the value is an empty string, which is harmless.
48
+ Set the version in your build environment:
49
+
50
+ ```bash
51
+ export FLARE_SOURCEMAP_VERSION="$(git rev-parse --short HEAD)"
52
+ ```
53
+
54
+ If unset, it falls back to your app's `package.json` version (with a warning). Use the
55
+ **same** version when you build the bundle and when you upload the map. If the two
56
+ differ (for example the env var is set during the build but not during a later upload
57
+ step, so one side falls back to `package.json`), the map will not match and
58
+ symbolication silently fails.
59
+
60
+ If your Babel config adds a generic `process.env` inliner (such as
61
+ `babel-plugin-transform-inline-environment-variables`), list this plugin before it so
62
+ the version token is not rewritten first. The default React Native and Expo presets do
63
+ not inline `process.env`, so no ordering change is needed for a stock setup.
64
+
65
+ ## 2. Upload the sourcemap
66
+
67
+ Generate a bundle + map, then upload the map under the **same** version:
68
+
69
+ > Release builds use Hermes, which compiles your JS to bytecode. The map that
70
+ > symbolicates production frames is the **Hermes-composed** map, not the plain Metro JS
71
+ > map. Point `--sourcemap` at the composed `.map` your build emits (the one next to the
72
+ > shipped bundle), not an intermediate Metro map.
73
+
74
+ ```bash
75
+ # Example: Android, Hermes-composed map produced by your build
76
+ npx flare-rn-sourcemaps upload \
77
+ --api-key "$FLARE_KEY" \
78
+ --sourcemap android/app/build/.../index.android.bundle.map \
79
+ --bundle-filename index.android.bundle \
80
+ --version "$FLARE_SOURCEMAP_VERSION"
81
+ ```
82
+
83
+ Flags:
84
+
85
+ | Flag | Required | Description |
86
+ | ------------------- | -------- | ---------------------------------------------------------------------------------------------- |
87
+ | `--sourcemap` | yes | Path to the composed `.map` file. |
88
+ | `--api-key` | yes\* | Flare API key. Falls back to `FLARE_API_KEY`. |
89
+ | `--bundle-filename` | no | `relative_filename` matched against runtime frames. Defaults to the map basename minus `.map`. |
90
+ | `--version` | no | Defaults to `FLARE_SOURCEMAP_VERSION`, then `package.json` version. |
91
+ | `--api-endpoint` | no | Defaults to `https://flareapp.io/api/sourcemaps`. |
92
+
93
+ \* Without a key the command warns and does nothing.
94
+
95
+ ## Automatic upload (bare React Native)
96
+
97
+ Instead of running the CLI by hand, wire it into your native release build. Both
98
+ hooks read a committed `flare.json` at your project root:
99
+
100
+ ```json
101
+ {
102
+ "apiKey": "your-flare-api-key"
103
+ }
104
+ ```
105
+
106
+ To keep the key out of version control, drop it in a gitignored `.env.local` (or
107
+ `.env`) next to `flare.json` instead — the hooks auto-load `FLARE_API_KEY` from there
108
+ at build time:
109
+
110
+ ```bash
111
+ # .env.local (gitignored)
112
+ FLARE_API_KEY=your-flare-api-key
113
+ ```
114
+
115
+ Precedence: shell env > `.env.local` > `.env` > `flare.json`. An explicit
116
+ `FLARE_API_KEY` in the build environment wins over all of them.
117
+
118
+ The version is taken **only** from `FLARE_SOURCEMAP_VERSION` here (the same variable
119
+ the Babel plugin reads), so the inlined version and the uploaded version always match.
120
+ Set it in your build environment:
121
+
122
+ ```bash
123
+ export FLARE_SOURCEMAP_VERSION="$(git rev-parse --short HEAD)"
124
+ ```
125
+
126
+ If the key or `FLARE_SOURCEMAP_VERSION` is missing, the upload is **skipped with a
127
+ large warning banner** — your build never fails because of a sourcemap problem.
128
+
129
+ ### Android
130
+
131
+ Add this line to `android/app/build.gradle`. It can go anywhere in the file — the script
132
+ hooks the build lazily, so the order doesn't matter:
133
+
134
+ ```gradle
135
+ apply from: "../../node_modules/@flareapp/react-native-sourcemaps/flare.gradle"
136
+ ```
137
+
138
+ This uploads the Hermes-composed map after every `release` JS-bundle task.
139
+
140
+ ### iOS
141
+
142
+ 1. Tell the stock React Native bundle phase to emit a sourcemap by adding this line
143
+ to `ios/.xcode.env`:
144
+
145
+ ```sh
146
+ export SOURCEMAP_FILE="$TARGET_TEMP_DIR/main.jsbundle.map"
147
+ ```
148
+
149
+ Use `$TARGET_TEMP_DIR`, **not** `$CONFIGURATION_BUILD_DIR`. With Hermes,
150
+ `react-native-xcode.sh` writes an intermediate map to
151
+ `$CONFIGURATION_BUILD_DIR/main.jsbundle.map` and then deletes it after composing the
152
+ final map — so pointing `SOURCEMAP_FILE` there would have the composed map deleted
153
+ out from under the upload.
154
+
155
+ 2. In Xcode, add a new "Run Script" build phase named **Upload Flare sourcemaps**,
156
+ placed **after** "Bundle React Native code and images", with this script:
157
+
158
+ ```sh
159
+ set -e
160
+ WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh"
161
+ FLARE_XCODE="../node_modules/@flareapp/react-native-sourcemaps/scripts/flare-xcode.sh"
162
+ /bin/sh "$WITH_ENVIRONMENT" "$FLARE_XCODE"
163
+ ```
164
+
165
+ The `with-environment.sh` wrapper is required so the phase sees `SOURCEMAP_FILE`
166
+ (and any `FLARE_*` vars) exported by `.xcode.env`.
167
+
168
+ 3. In that build phase, **uncheck "Based on Dependency Analysis"**. With no input/output
169
+ files, Xcode otherwise warns that the script has ambiguous dependencies and runs on
170
+ every build — which is what you want for a release upload. (The Expo config plugin
171
+ sets this automatically.)
172
+
173
+ > The key comes from `flare.json` or the auto-loaded `.env.local`, so it's there even
174
+ > for a GUI build. The **version** (`FLARE_SOURCEMAP_VERSION`) is the one variable the
175
+ > phase still reads from the **build's** environment, inherited from whatever launched
176
+ > the build. `react-native run-ios`, `xcodebuild`, or Fastlane from a terminal that
177
+ > exported it works; a build from the Xcode GUI (including Product > Archive) doesn't
178
+ > have it, so archive from the command line or in CI for releases. The upload skips with
179
+ > the banner if it's missing, and the build still succeeds.
180
+
181
+ #### Custom build configurations (bare / brownfield)
182
+
183
+ Your build configuration doesn't have to be called `Release`.
184
+
185
+ The hooks upload whenever a build makes a JavaScript bundle. They skip a build only when
186
+ its name contains `debug` (any casing).
187
+
188
+ - Uploads: `Release`, `Staging`, `Production`, `AppStore`, your own Android build type
189
+ - Skipped: `Debug`, `debug`, `StagingDebug`
190
+
191
+ A debug build runs from Metro and makes no bundle, so there's nothing to upload. The hook
192
+ skips it and your dev builds stay fast.
193
+
194
+ Want a bundling config to skip the upload anyway? Put `debug` in its name. Or remove the
195
+ Flare setup you added above — the `apply from "…/flare.gradle"` line in
196
+ `android/app/build.gradle` on Android, or the **Upload Flare sourcemaps** build phase in
197
+ Xcode on iOS — which turns the automatic upload off completely.
198
+
199
+ ### Expo (CNG / managed)
200
+
201
+ For an Expo project that uses prebuild (CNG), add the config plugin to `app.json` —
202
+ it injects the same native wiring on every prebuild, so it survives regeneration:
203
+
204
+ ```json
205
+ {
206
+ "expo": {
207
+ "plugins": [["@flareapp/react-native-sourcemaps/expo", { "apiKey": "YOUR PROJECT KEY" }]]
208
+ }
209
+ }
210
+ ```
211
+
212
+ You still add the Babel plugin and pass `flareSourcemapVersion` (steps above), and you
213
+ still set `FLARE_SOURCEMAP_VERSION` in the build environment (locally, or in
214
+ `eas.json`'s `build.<profile>.env` for EAS Build). The plugin creates a `flare.json` at
215
+ your project root and adds it to `.gitignore`. That's expected. You don't edit it; it's
216
+ generated from your `app.json`.
217
+
218
+ **Releasing without EAS?** You don't have to. The key comes from `flare.json` or the
219
+ auto-loaded `.env.local`; the upload reads `FLARE_SOURCEMAP_VERSION` from the build's
220
+ environment, inherited from whatever **launches** the build. So EAS Build (`eas.json`
221
+ env), `eas build --local`, or a command-line archive (`xcodebuild` / Fastlane on iOS,
222
+ `./gradlew bundleRelease` on Android) all work, as long as you exported the version in
223
+ that shell.
224
+
225
+ > The one exception is the version under the **Xcode / Android Studio GUI** (including
226
+ > Product > Archive): it was launched by the OS, not your shell, so it doesn't have
227
+ > `FLARE_SOURCEMAP_VERSION` and the upload skips with the banner. Archive from the
228
+ > command line, or use EAS, for releases.
229
+
230
+ > **`expo run:android` / `expo run:ios` are not a reliable upload path.** Both
231
+ > eager-bundle the JS to a temp dir, and the native bundle task our hook runs after can
232
+ > be reported **up-to-date** and skipped — so you get an installed app but no upload.
233
+ > Changing only `FLARE_SOURCEMAP_VERSION` doesn't re-trigger it (it isn't a build
234
+ > input). Use `expo run:*` for development, but **release and verify** through
235
+ > `eas build` / `eas build --local` or a direct native build (`./gradlew
236
+ :app:assembleRelease`, `xcodebuild`). When testing locally, add `--rerun-tasks` to the
237
+ > Gradle command to force a fresh bundle and upload.
238
+
239
+ > **OTA / EAS Update is not covered.** The plugin only runs during a native build
240
+ > (`expo run:*`, EAS Build). `eas update` ships JS via `expo export` with no native
241
+ > build phase, so it uploads no map. For OTA releases, upload the map yourself with
242
+ > `flare-rn-sourcemaps upload` under the **same** `FLARE_SOURCEMAP_VERSION` you exported
243
+ > with.
244
+
245
+ ## Expo (`expo export`)
246
+
247
+ ```bash
248
+ FLARE_SOURCEMAP_VERSION="$(git rev-parse --short HEAD)" npx expo export
249
+ npx flare-rn-sourcemaps upload \
250
+ --api-key "$FLARE_KEY" \
251
+ --sourcemap dist/_expo/static/js/ios/index-*.hbc.map \
252
+ --bundle-filename main.jsbundle \
253
+ --version "$FLARE_SOURCEMAP_VERSION"
254
+ ```
255
+
256
+ > `--bundle-filename` must match how frames appear in your Flare reports. If
257
+ > traces are not symbolicating, that is the value to adjust.
package/dist/babel.cjs ADDED
@@ -0,0 +1,52 @@
1
+ const require_version = require('./version-Z9pawNvw.cjs');
2
+
3
+ //#region src/babel.ts
4
+ const RUNTIME_SOURCE = "@flareapp/react-native-sourcemaps/runtime";
5
+ const EXPORT_NAME = "flareSourcemapVersion";
6
+ /**
7
+ * Babel plugin that inlines `flareSourcemapVersion` (imported from
8
+ * `@flareapp/react-native-sourcemaps/runtime`) with the resolved version string at
9
+ * bundle time, then removes the now-dead import so nothing of this package ships at
10
+ * runtime. The version is resolved once per file (lazily, only when the import is
11
+ * present) via the shared `resolveVersion()`.
12
+ */
13
+ function flareSourcemapsBabelPlugin({ types: t }) {
14
+ let version;
15
+ return {
16
+ name: "@flareapp/react-native-sourcemaps",
17
+ pre() {
18
+ version = void 0;
19
+ },
20
+ visitor: { ImportDeclaration(path) {
21
+ if (path.node.source.value !== RUNTIME_SOURCE) return;
22
+ const remaining = [];
23
+ let inlinedAny = false;
24
+ for (const specifier of path.node.specifiers) {
25
+ if (!isFlareVersionSpecifier(specifier, t)) {
26
+ remaining.push(specifier);
27
+ continue;
28
+ }
29
+ const binding = path.scope.getBinding(specifier.local.name);
30
+ if (!binding) {
31
+ remaining.push(specifier);
32
+ continue;
33
+ }
34
+ if (version === void 0) version = require_version.resolveVersion();
35
+ for (const reference of binding.referencePaths) reference.replaceWith(t.stringLiteral(version));
36
+ inlinedAny = true;
37
+ }
38
+ if (!inlinedAny) return;
39
+ if (remaining.length === 0) path.remove();
40
+ else path.node.specifiers = remaining;
41
+ } }
42
+ };
43
+ }
44
+ /** Matches `import { flareSourcemapVersion } from '.../runtime'` (named, possibly aliased). */
45
+ function isFlareVersionSpecifier(specifier, t) {
46
+ if (!t.isImportSpecifier(specifier)) return false;
47
+ const imported = specifier.imported;
48
+ return (t.isIdentifier(imported) ? imported.name : imported.value) === EXPORT_NAME;
49
+ }
50
+
51
+ //#endregion
52
+ module.exports = flareSourcemapsBabelPlugin;
@@ -0,0 +1,18 @@
1
+ import { t as index_legacy_d_exports } from "./index-legacy-aO8l3fYt.cjs";
2
+ import * as Babel from "@babel/core";
3
+
4
+ //#region src/babel.d.ts
5
+ type Types = typeof index_legacy_d_exports;
6
+ /**
7
+ * Babel plugin that inlines `flareSourcemapVersion` (imported from
8
+ * `@flareapp/react-native-sourcemaps/runtime`) with the resolved version string at
9
+ * bundle time, then removes the now-dead import so nothing of this package ships at
10
+ * runtime. The version is resolved once per file (lazily, only when the import is
11
+ * present) via the shared `resolveVersion()`.
12
+ */
13
+ declare function flareSourcemapsBabelPlugin({
14
+ types: t
15
+ }: {
16
+ types: Types;
17
+ }): Babel.PluginObj;
18
+ export = flareSourcemapsBabelPlugin;
@@ -0,0 +1,19 @@
1
+ import { t as index_legacy_d_exports } from "./index-legacy-DeTFfC6B.mjs";
2
+ import * as Babel from "@babel/core";
3
+
4
+ //#region src/babel.d.ts
5
+ type Types = typeof index_legacy_d_exports;
6
+ /**
7
+ * Babel plugin that inlines `flareSourcemapVersion` (imported from
8
+ * `@flareapp/react-native-sourcemaps/runtime`) with the resolved version string at
9
+ * bundle time, then removes the now-dead import so nothing of this package ships at
10
+ * runtime. The version is resolved once per file (lazily, only when the import is
11
+ * present) via the shared `resolveVersion()`.
12
+ */
13
+ declare function flareSourcemapsBabelPlugin({
14
+ types: t
15
+ }: {
16
+ types: Types;
17
+ }): Babel.PluginObj;
18
+ //#endregion
19
+ export { flareSourcemapsBabelPlugin as default };
package/dist/babel.mjs ADDED
@@ -0,0 +1,52 @@
1
+ import { n as resolveVersion } from "./version-DnFqflPl.mjs";
2
+
3
+ //#region src/babel.ts
4
+ const RUNTIME_SOURCE = "@flareapp/react-native-sourcemaps/runtime";
5
+ const EXPORT_NAME = "flareSourcemapVersion";
6
+ /**
7
+ * Babel plugin that inlines `flareSourcemapVersion` (imported from
8
+ * `@flareapp/react-native-sourcemaps/runtime`) with the resolved version string at
9
+ * bundle time, then removes the now-dead import so nothing of this package ships at
10
+ * runtime. The version is resolved once per file (lazily, only when the import is
11
+ * present) via the shared `resolveVersion()`.
12
+ */
13
+ function flareSourcemapsBabelPlugin({ types: t }) {
14
+ let version;
15
+ return {
16
+ name: "@flareapp/react-native-sourcemaps",
17
+ pre() {
18
+ version = void 0;
19
+ },
20
+ visitor: { ImportDeclaration(path) {
21
+ if (path.node.source.value !== RUNTIME_SOURCE) return;
22
+ const remaining = [];
23
+ let inlinedAny = false;
24
+ for (const specifier of path.node.specifiers) {
25
+ if (!isFlareVersionSpecifier(specifier, t)) {
26
+ remaining.push(specifier);
27
+ continue;
28
+ }
29
+ const binding = path.scope.getBinding(specifier.local.name);
30
+ if (!binding) {
31
+ remaining.push(specifier);
32
+ continue;
33
+ }
34
+ if (version === void 0) version = resolveVersion();
35
+ for (const reference of binding.referencePaths) reference.replaceWith(t.stringLiteral(version));
36
+ inlinedAny = true;
37
+ }
38
+ if (!inlinedAny) return;
39
+ if (remaining.length === 0) path.remove();
40
+ else path.node.specifiers = remaining;
41
+ } }
42
+ };
43
+ }
44
+ /** Matches `import { flareSourcemapVersion } from '.../runtime'` (named, possibly aliased). */
45
+ function isFlareVersionSpecifier(specifier, t) {
46
+ if (!t.isImportSpecifier(specifier)) return false;
47
+ const imported = specifier.imported;
48
+ return (t.isIdentifier(imported) ? imported.name : imported.value) === EXPORT_NAME;
49
+ }
50
+
51
+ //#endregion
52
+ export { flareSourcemapsBabelPlugin as default };
package/dist/bin.cjs ADDED
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env node
2
+ const require_uploadSourcemaps = require('./uploadSourcemaps-D6J5FWDO.cjs');
3
+ const require_version = require('./version-Z9pawNvw.cjs');
4
+ let node_fs = require("node:fs");
5
+ let node_path = require("node:path");
6
+
7
+ //#region src/banner.ts
8
+ const BORDER = "=".repeat(60);
9
+ /**
10
+ * Mask an API key for display in a build log, which CI commonly archives. A key
11
+ * long enough to stay unguessable keeps a short head/tail so the user can still
12
+ * recognise WHICH key it was; a short key is fully masked. Never returns the key
13
+ * in full, so the secret cannot leak through the failure banner.
14
+ */
15
+ function maskApiKey(apiKey) {
16
+ if (apiKey.length <= 12) return "*".repeat(apiKey.length);
17
+ return `${apiKey.slice(0, 4)}${"*".repeat(8)}${apiKey.slice(-4)}`;
18
+ }
19
+ /**
20
+ * The deliberately large failure banner. A one-line "failed to upload" is too easy
21
+ * to miss in a long native-build log, so this is a bordered block surrounded by
22
+ * blank lines. Resolved values are interpolated into the re-run command so the
23
+ * user can copy-paste it; unknown values show a labelled placeholder (in the
24
+ * version-unset case `version` stays a placeholder — it is the value to supply).
25
+ * The API key is the one exception: it is MASKED, never printed in full, because
26
+ * this banner lands in the native build log.
27
+ */
28
+ function formatFailureBanner(info) {
29
+ const sourcemap = info.sourcemap ?? "<path-to-map>";
30
+ const bundleFilename = info.bundleFilename ?? "<bundle-filename>";
31
+ const version = info.version && info.version.length > 0 ? info.version : "<flare-sourcemap-version>";
32
+ const hasApiKey = !!(info.apiKey && info.apiKey.length > 0);
33
+ const apiKey = hasApiKey ? maskApiKey(info.apiKey) : "<your-flare-api-key>";
34
+ const endpointFlag = info.apiEndpoint ? ` --api-endpoint ${info.apiEndpoint}` : "";
35
+ const lines = [
36
+ "",
37
+ BORDER,
38
+ " FLARE SOURCEMAP UPLOAD FAILED",
39
+ ` Reason: ${info.reason}`,
40
+ " Your release will report minified stack traces until the",
41
+ " sourcemap is uploaded. Re-run manually:",
42
+ ` npx flare-rn-sourcemaps upload --sourcemap ${sourcemap} \\`,
43
+ ` --bundle-filename ${bundleFilename} --version ${version} --api-key ${apiKey}${endpointFlag}`
44
+ ];
45
+ if (hasApiKey) lines.push(" (the --api-key above is masked; pass your full Flare key, or set FLARE_API_KEY, when re-running)");
46
+ lines.push(BORDER, "");
47
+ return lines.join("\n");
48
+ }
49
+ function printFailureBanner(info) {
50
+ console.error(formatFailureBanner(info));
51
+ }
52
+
53
+ //#endregion
54
+ //#region src/config.ts
55
+ /**
56
+ * Read flare.json from an EXPLICIT path. The hooks run from android/ or ios/, so
57
+ * resolving relative to process.cwd() would read the wrong file — callers always
58
+ * pass --config. Missing or malformed file → empty config, so resolution falls
59
+ * through to env, then to the no-key skip-with-banner. Any `version` key is
60
+ * deliberately ignored: in the auto path the version flows only through
61
+ * FLARE_SOURCEMAP_VERSION (see resolveAutoVersion / the design doc).
62
+ */
63
+ function readFlareConfig(configPath) {
64
+ if (!configPath) return {};
65
+ try {
66
+ const raw = JSON.parse((0, node_fs.readFileSync)(configPath, "utf8"));
67
+ const config = {};
68
+ const apiKey = asString(raw.apiKey);
69
+ const apiEndpoint = asString(raw.apiEndpoint);
70
+ if (apiKey !== void 0) config.apiKey = apiKey;
71
+ if (apiEndpoint !== void 0) config.apiEndpoint = apiEndpoint;
72
+ return config;
73
+ } catch {
74
+ return {};
75
+ }
76
+ }
77
+ function asString(value) {
78
+ return typeof value === "string" && value.length > 0 ? value : void 0;
79
+ }
80
+
81
+ //#endregion
82
+ //#region src/env.ts
83
+ const ENV_FILES = [".env.local", ".env"];
84
+ /**
85
+ * Load FLARE_* variables from .env.local / .env in `rootDir` into process.env,
86
+ * without overwriting anything already set.
87
+ *
88
+ * The native build hooks run in a fresh process whose environment often does NOT
89
+ * carry the upload key: the app's .env files are loaded by Metro at JS runtime, not
90
+ * at native build time, and a build launched from an IDE — or served by a reused
91
+ * Gradle daemon with a stale environment — won't see a key you exported in your
92
+ * shell. Reading the key from the FILE here makes "drop FLARE_API_KEY in .env.local"
93
+ * work the way developers expect, on both platforms, without re-exporting it per
94
+ * build.
95
+ *
96
+ * Only FLARE_-prefixed keys are read, so this never pulls unrelated secrets from the
97
+ * file into the process. Minimal parser (no dependency): `KEY=value`, an optional
98
+ * `export ` prefix, surrounding single/double quotes stripped, blank and `#` comment
99
+ * lines ignored. Inline comments are NOT stripped (a `#` is treated as part of the
100
+ * value), which is fine for the alphanumeric keys Flare issues.
101
+ */
102
+ function loadEnvFiles(rootDir) {
103
+ for (const file of ENV_FILES) {
104
+ let raw;
105
+ try {
106
+ raw = (0, node_fs.readFileSync)((0, node_path.join)(rootDir, file), "utf8");
107
+ } catch {
108
+ continue;
109
+ }
110
+ for (const [key, value] of parseFlareEnv(raw)) if (process.env[key] === void 0) process.env[key] = value;
111
+ }
112
+ }
113
+ function parseFlareEnv(raw) {
114
+ const out = [];
115
+ for (const line of raw.split(/\r?\n/)) {
116
+ const trimmed = line.trim();
117
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
118
+ const body = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
119
+ const eq = body.indexOf("=");
120
+ if (eq === -1) continue;
121
+ const key = body.slice(0, eq).trim();
122
+ if (!key.startsWith("FLARE_")) continue;
123
+ let value = body.slice(eq + 1).trim();
124
+ if ((value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) && value.length >= 2) value = value.slice(1, -1);
125
+ out.push([key, value]);
126
+ }
127
+ return out;
128
+ }
129
+
130
+ //#endregion
131
+ //#region src/cli.ts
132
+ const USAGE = "Usage: flare-rn-sourcemaps upload --sourcemap <path> [--api-key <key>] [--bundle-filename <name>] [--version <v>] [--api-endpoint <url>] [--config <flare.json>] [--auto]";
133
+ /** Minimal `--flag value` / `--flag` parser. No external dependency. */
134
+ function parseArgs(argv) {
135
+ const [command, ...rest] = argv;
136
+ const flags = {};
137
+ for (let i = 0; i < rest.length; i++) {
138
+ const arg = rest[i];
139
+ if (!arg.startsWith("--")) continue;
140
+ const body = arg.slice(2);
141
+ const eq = body.indexOf("=");
142
+ if (eq !== -1) {
143
+ flags[body.slice(0, eq)] = body.slice(eq + 1);
144
+ continue;
145
+ }
146
+ const key = body;
147
+ const next = rest[i + 1];
148
+ if (next !== void 0 && !next.startsWith("--")) {
149
+ flags[key] = next;
150
+ i++;
151
+ } else flags[key] = "true";
152
+ }
153
+ return {
154
+ command,
155
+ flags
156
+ };
157
+ }
158
+ async function runCli(argv) {
159
+ const { command, flags } = parseArgs(argv);
160
+ if (command !== "upload") {
161
+ console.error(`${require_version.LOG_PREFIX}: Unknown command "${command ?? ""}".\n${USAGE}`);
162
+ process.exitCode = 1;
163
+ return;
164
+ }
165
+ const sourcemap = flags.sourcemap;
166
+ if (!sourcemap) {
167
+ console.error(`${require_version.LOG_PREFIX}: --sourcemap is required.\n${USAGE}`);
168
+ process.exitCode = 1;
169
+ return;
170
+ }
171
+ if (flags.auto === "true") loadEnvFiles(flags.config ? (0, node_path.dirname)(flags.config) : process.cwd());
172
+ const config = readFlareConfig(flags.config);
173
+ const apiKey = flags["api-key"] ?? process.env.FLARE_API_KEY ?? config.apiKey ?? "";
174
+ const apiEndpoint = flags["api-endpoint"] ?? process.env.FLARE_API_ENDPOINT ?? config.apiEndpoint;
175
+ const bundleFilename = flags["bundle-filename"];
176
+ if (flags.auto === "true") {
177
+ await runAutoUpload({
178
+ apiKey,
179
+ sourcemap,
180
+ bundleFilename,
181
+ apiEndpoint,
182
+ version: flags.version
183
+ });
184
+ return;
185
+ }
186
+ await require_uploadSourcemaps.uploadSourcemaps({
187
+ apiKey,
188
+ sourcemap,
189
+ bundleFilename,
190
+ version: flags.version,
191
+ apiEndpoint
192
+ });
193
+ }
194
+ /**
195
+ * The build-hook upload path. It NEVER throws and NEVER sets a non-zero exit code
196
+ * (a Gradle doLast / Xcode run-script phase would abort the build on a non-zero
197
+ * child). Every failure mode — no key, unresolved version, upload error — prints the
198
+ * loud banner and returns, leaving the build green. Only arg misuse (handled in
199
+ * runCli before we get here) exits non-zero.
200
+ */
201
+ async function runAutoUpload(options) {
202
+ const { apiKey, sourcemap, bundleFilename, apiEndpoint } = options;
203
+ if (!apiKey) {
204
+ printFailureBanner({
205
+ reason: "No Flare API key. Set FLARE_API_KEY (shell, .env.local, or .env) or add \"apiKey\" to flare.json.",
206
+ sourcemap,
207
+ bundleFilename,
208
+ apiKey,
209
+ apiEndpoint
210
+ });
211
+ return;
212
+ }
213
+ const version = require_version.resolveAutoVersion(options.version);
214
+ if (!version) {
215
+ printFailureBanner({
216
+ reason: "FLARE_SOURCEMAP_VERSION is not set (required for the automatic upload).",
217
+ sourcemap,
218
+ bundleFilename,
219
+ apiKey,
220
+ apiEndpoint
221
+ });
222
+ return;
223
+ }
224
+ try {
225
+ await require_uploadSourcemaps.uploadSourcemaps({
226
+ apiKey,
227
+ sourcemap,
228
+ bundleFilename,
229
+ version,
230
+ apiEndpoint
231
+ });
232
+ } catch (error) {
233
+ printFailureBanner({
234
+ reason: error instanceof Error ? error.message : String(error),
235
+ sourcemap,
236
+ bundleFilename,
237
+ version,
238
+ apiKey,
239
+ apiEndpoint
240
+ });
241
+ }
242
+ }
243
+
244
+ //#endregion
245
+ //#region src/bin.ts
246
+ runCli(process.argv.slice(2)).catch((error) => {
247
+ console.error(error);
248
+ process.exitCode = 1;
249
+ });
250
+
251
+ //#endregion
package/dist/bin.d.cts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/bin.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };