@observerkit/metro 0.2.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 +118 -0
- package/dist/chunk-6RJOM2CO.cjs +199 -0
- package/dist/chunk-AUOVSTQX.cjs +7 -0
- package/dist/chunk-DFCKYXBG.js +199 -0
- package/dist/cli.cjs +156 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +155 -0
- package/dist/expo.cjs +92 -0
- package/dist/expo.d.cts +24 -0
- package/dist/expo.d.ts +22 -0
- package/dist/expo.js +88 -0
- package/dist/index.cjs +135 -0
- package/dist/index.d.cts +65 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +132 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# @observerkit/metro
|
|
2
|
+
|
|
3
|
+
Metro plugin for ObserverKit: injects debug IDs into React Native bundles and uploads source maps so production stack traces symbolicate, including Hermes builds.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install --save-dev @observerkit/metro
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 1. Wrap your Metro config
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// metro.config.js
|
|
15
|
+
const { getDefaultConfig } = require("@react-native/metro-config")
|
|
16
|
+
const withObserverkit = require("@observerkit/metro")
|
|
17
|
+
|
|
18
|
+
module.exports = withObserverkit(getDefaultConfig(__dirname))
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Debug-id injection only runs for release bundles; dev builds are untouched.
|
|
22
|
+
|
|
23
|
+
## 2. Upload source maps from your native builds
|
|
24
|
+
|
|
25
|
+
The `observerkit-upload` CLI reads the project key from `--project-key` or the `OBSERVERKIT_PROJECT_KEY` environment variable.
|
|
26
|
+
|
|
27
|
+
### Expo
|
|
28
|
+
|
|
29
|
+
Add the config plugin; prebuild wires both native builds automatically:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"expo": {
|
|
34
|
+
"plugins": [["@observerkit/metro/expo", { "projectKey": "YOUR_PROJECT_KEY" }]]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For EAS Update bundles, upload after export:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx expo export && npx observerkit-upload --dir dist
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Bare React Native: iOS
|
|
46
|
+
|
|
47
|
+
Edit the "Bundle React Native code and images" build phase so a source map is emitted and uploaded:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
export SOURCEMAP_FILE="$DERIVED_FILE_DIR/main.jsbundle.map"
|
|
51
|
+
set -e
|
|
52
|
+
WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh"
|
|
53
|
+
REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh"
|
|
54
|
+
/bin/sh -c "$WITH_ENVIRONMENT $REACT_NATIVE_XCODE"
|
|
55
|
+
if [ -f "$SOURCEMAP_FILE" ]; then
|
|
56
|
+
npx observerkit-upload --map "$SOURCEMAP_FILE" --bundle "$CONFIGURATION_BUILD_DIR/main.jsbundle" --project-key "YOUR_PROJECT_KEY" || echo "[ObserverKit] source map upload failed"
|
|
57
|
+
fi
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`--bundle` points at the pre-Hermes JS bundle, which is where `observerkit-upload` finds the debugId on iOS (see Hermes below). The `|| echo` makes sure a failed upload never fails the archive, since this phase runs under `set -e`.
|
|
61
|
+
|
|
62
|
+
### Bare React Native: Android
|
|
63
|
+
|
|
64
|
+
Append to `android/app/build.gradle`:
|
|
65
|
+
|
|
66
|
+
```groovy
|
|
67
|
+
tasks.configureEach { task ->
|
|
68
|
+
def matcher = task.name =~ /^createBundle(\w*)ReleaseJsAndAssets$/
|
|
69
|
+
if (matcher.matches()) {
|
|
70
|
+
task.doLast {
|
|
71
|
+
def flavor = matcher.group(1)
|
|
72
|
+
def variant = flavor.isEmpty()
|
|
73
|
+
? "release"
|
|
74
|
+
: flavor.substring(0, 1).toLowerCase() + flavor.substring(1) + "Release"
|
|
75
|
+
def mapFile = file("$buildDir/generated/sourcemaps/react/" + variant + "/index.android.bundle.map")
|
|
76
|
+
def packagerMapFile = file("$buildDir/intermediates/sourcemaps/react/" + variant + "/index.android.bundle.packager.map")
|
|
77
|
+
if (mapFile.exists()) {
|
|
78
|
+
def result = exec {
|
|
79
|
+
workingDir rootProject.projectDir.parentFile
|
|
80
|
+
environment "OBSERVERKIT_PROJECT_KEY", "YOUR_PROJECT_KEY"
|
|
81
|
+
commandLine "npx", "observerkit-upload", "--map", mapFile.absolutePath, "--packager-map", packagerMapFile.absolutePath
|
|
82
|
+
ignoreExitValue true
|
|
83
|
+
}
|
|
84
|
+
if (result.exitValue != 0) {
|
|
85
|
+
logger.warn("[ObserverKit] source map upload failed")
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
logger.warn("[ObserverKit] No source map found at " + mapFile + ", skipping upload")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The composed map lands in `generated/sourcemaps/react/<variant>/`, while the pre-compose packager map (needed to restore the `debugId`, see Hermes below) stays behind in `intermediates/sourcemaps/react/<variant>/`. `ignoreExitValue` plus the logged warning make sure a failed upload never fails `assembleRelease`.
|
|
96
|
+
|
|
97
|
+
## Hermes
|
|
98
|
+
|
|
99
|
+
Symbolication support targets Hermes, the React Native default JS engine; JSC is not supported.
|
|
100
|
+
|
|
101
|
+
Hermes release builds compose the Metro source map with the bytecode map, which drops the `debugId` field. `observerkit-upload` restores it automatically:
|
|
102
|
+
|
|
103
|
+
- On Android, from the packager map (`<bundle>.packager.map` next to the final map by convention, or pass `--packager-map` explicitly). The Gradle plugin keeps this file around after composing.
|
|
104
|
+
- On iOS, `react-native-xcode.sh` deletes the packager map once the final map is composed, so there is nothing to restore it from there. Instead `observerkit-upload` reads the trailing `//# debugId=<uuid>` comment the ObserverKit serializer appends to the pre-Hermes JS bundle (the final map path with `.map` stripped, or pass `--bundle` explicitly).
|
|
105
|
+
|
|
106
|
+
## CLI reference
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
observerkit-upload --map <path> [--packager-map <path>] [--bundle <path>]
|
|
110
|
+
observerkit-upload --dir <path>
|
|
111
|
+
|
|
112
|
+
--project-key <key> Defaults to $OBSERVERKIT_PROJECT_KEY
|
|
113
|
+
--endpoint <url> Defaults to $OBSERVERKIT_ENDPOINT or https://ingest.observerkit.com
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## pnpm note
|
|
117
|
+
|
|
118
|
+
If your app uses pnpm and Metro is not a direct dependency, add it (`pnpm add -D metro`) so the plugin can load Metro's default serializer.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }// ../plugin-core/src/upload.ts
|
|
2
|
+
var _promises = require('fs/promises');
|
|
3
|
+
var _path = require('path');
|
|
4
|
+
|
|
5
|
+
// ../plugin-core/src/json.ts
|
|
6
|
+
function isJsonObject(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// ../plugin-core/src/upload.ts
|
|
11
|
+
var DEFAULT_MAX_FILE_BYTES = 50 * 1024 * 1024;
|
|
12
|
+
var DEFAULT_MAX_BATCH_BYTES = 100 * 1024 * 1024;
|
|
13
|
+
function hasDebugId(content) {
|
|
14
|
+
if (!content.includes('"debugId"')) return false;
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(content.toString("utf-8"));
|
|
17
|
+
return isJsonObject(parsed) && typeof parsed["debugId"] === "string";
|
|
18
|
+
} catch (e) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function batchBySize(entries, maxBatchBytes) {
|
|
23
|
+
const batches = [];
|
|
24
|
+
let current = [];
|
|
25
|
+
let currentBytes = 0;
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (current.length > 0 && currentBytes + entry.byteLength > maxBatchBytes) {
|
|
28
|
+
batches.push(current);
|
|
29
|
+
current = [];
|
|
30
|
+
currentBytes = 0;
|
|
31
|
+
}
|
|
32
|
+
current.push(entry);
|
|
33
|
+
currentBytes += entry.byteLength;
|
|
34
|
+
}
|
|
35
|
+
if (current.length > 0) batches.push(current);
|
|
36
|
+
return batches;
|
|
37
|
+
}
|
|
38
|
+
async function deleteMapFiles(directory, mapFiles) {
|
|
39
|
+
const results = await Promise.allSettled(
|
|
40
|
+
mapFiles.map((mapFile) => _promises.unlink.call(void 0, _path.join.call(void 0, directory, mapFile)))
|
|
41
|
+
);
|
|
42
|
+
const failures = results.filter((r) => r.status === "rejected");
|
|
43
|
+
if (failures.length > 0) {
|
|
44
|
+
console.warn(
|
|
45
|
+
`[ObserverKit] Failed to delete ${failures.length}/${mapFiles.length} .map files`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
console.log(
|
|
49
|
+
`[ObserverKit] Deleted ${mapFiles.length - failures.length} .map files from output`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
async function uploadBatch(batch, options) {
|
|
53
|
+
const formData = new FormData();
|
|
54
|
+
for (const [index, entry] of batch.entries()) {
|
|
55
|
+
const content = await _promises.readFile.call(void 0, _path.join.call(void 0, options.directory, entry.mapFile));
|
|
56
|
+
formData.append(
|
|
57
|
+
`file-${index}`,
|
|
58
|
+
new Blob([new Uint8Array(content)], { type: "application/json" }),
|
|
59
|
+
entry.mapFile
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const response = await fetch(options.url, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"X-ObserverKit-Key": options.projectKey,
|
|
66
|
+
"X-ObserverKit-Plugin-Version": options.pluginVersion,
|
|
67
|
+
"X-ObserverKit-Plugin-Name": options.pluginName
|
|
68
|
+
},
|
|
69
|
+
body: formData
|
|
70
|
+
});
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
const text = await response.text();
|
|
73
|
+
throw new Error(
|
|
74
|
+
`[ObserverKit] Source map upload failed: ${response.status} ${text}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
var JS_SOURCE_MAPPING_URL = /^\/\/[#@] sourceMappingURL=\S+[ \t]*$/gm;
|
|
79
|
+
var CSS_SOURCE_MAPPING_URL = /^\/\*# sourceMappingURL=\S+[ \t]*\*\/[ \t]*$/gm;
|
|
80
|
+
function isChunkFile(name) {
|
|
81
|
+
return name.endsWith(".js") || name.endsWith(".mjs") || name.endsWith(".cjs") || name.endsWith(".css");
|
|
82
|
+
}
|
|
83
|
+
async function stripSourceMappingUrls(directory, files) {
|
|
84
|
+
let stripped = 0;
|
|
85
|
+
for (const file of files.filter(isChunkFile)) {
|
|
86
|
+
const path = _path.join.call(void 0, directory, file);
|
|
87
|
+
try {
|
|
88
|
+
const content = await _promises.readFile.call(void 0, path, "utf-8");
|
|
89
|
+
if (!content.includes("sourceMappingURL=")) continue;
|
|
90
|
+
const next = content.replace(JS_SOURCE_MAPPING_URL, "").replace(CSS_SOURCE_MAPPING_URL, "");
|
|
91
|
+
if (next !== content) {
|
|
92
|
+
await _promises.writeFile.call(void 0, path, next);
|
|
93
|
+
stripped += 1;
|
|
94
|
+
}
|
|
95
|
+
} catch (e2) {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (stripped > 0) {
|
|
99
|
+
console.log(
|
|
100
|
+
`[ObserverKit] Stripped sourceMappingURL comments from ${stripped} file(s)`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function uploadSourceMaps(options) {
|
|
105
|
+
const { projectKey, directory, endpoint, deleteAfterUpload, pluginVersion, pluginName } = options;
|
|
106
|
+
const maxFileBytes = _nullishCoalesce(options.maxFileBytes, () => ( DEFAULT_MAX_FILE_BYTES));
|
|
107
|
+
const maxBatchBytes = _nullishCoalesce(options.maxBatchBytes, () => ( DEFAULT_MAX_BATCH_BYTES));
|
|
108
|
+
const allFiles = options.files ? [...options.files] : await _promises.readdir.call(void 0, directory, { recursive: true, encoding: "utf-8" });
|
|
109
|
+
const allMapFiles = allFiles.filter((f) => f.endsWith(".map"));
|
|
110
|
+
if (allMapFiles.length === 0) {
|
|
111
|
+
console.log("[ObserverKit] No .map files found, skipping source map upload");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const uploadable = [];
|
|
116
|
+
let skippedNoDebugId = 0;
|
|
117
|
+
let skippedOversize = 0;
|
|
118
|
+
let skippedUnreadable = 0;
|
|
119
|
+
for (const mapFile of allMapFiles) {
|
|
120
|
+
let content;
|
|
121
|
+
try {
|
|
122
|
+
content = await _promises.readFile.call(void 0, _path.join.call(void 0, directory, mapFile));
|
|
123
|
+
} catch (e3) {
|
|
124
|
+
skippedUnreadable += 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!hasDebugId(content)) {
|
|
128
|
+
skippedNoDebugId += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (content.byteLength > maxFileBytes) {
|
|
132
|
+
skippedOversize += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
uploadable.push({ mapFile, byteLength: content.byteLength });
|
|
136
|
+
}
|
|
137
|
+
if (skippedNoDebugId > 0) {
|
|
138
|
+
console.log(
|
|
139
|
+
`[ObserverKit] Skipping ${skippedNoDebugId} source map(s) without a debugId field`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (skippedOversize > 0) {
|
|
143
|
+
console.warn(
|
|
144
|
+
`[ObserverKit] Skipping ${skippedOversize} source map(s) over the per-file ingest limit`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (skippedUnreadable > 0) {
|
|
148
|
+
console.warn(
|
|
149
|
+
`[ObserverKit] Skipping ${skippedUnreadable} unreadable source map(s)`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (uploadable.length === 0) {
|
|
153
|
+
console.log(
|
|
154
|
+
"[ObserverKit] No uploadable source maps found, skipping upload"
|
|
155
|
+
);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const url = `${endpoint.replace(/\/+$/, "")}/v1/sourcemaps`;
|
|
159
|
+
const batches = batchBySize(uploadable, maxBatchBytes);
|
|
160
|
+
for (const [index, batch] of batches.entries()) {
|
|
161
|
+
await uploadBatch(batch, { directory, url, projectKey, pluginVersion, pluginName });
|
|
162
|
+
console.log(
|
|
163
|
+
`[ObserverKit] Uploaded batch ${index + 1}/${batches.length} (${batch.length} source maps)`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (deleteAfterUpload) {
|
|
168
|
+
console.warn(
|
|
169
|
+
"[ObserverKit] Upload failed \u2014 deleting .map files anyway so they don't ship (set deleteAfterUpload: false to keep them)"
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
throw err;
|
|
173
|
+
} finally {
|
|
174
|
+
if (deleteAfterUpload) {
|
|
175
|
+
await deleteMapFiles(directory, allMapFiles);
|
|
176
|
+
await stripSourceMappingUrls(directory, allFiles);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ../plugin-core/src/debug-id.ts
|
|
182
|
+
var _uuid = require('uuid');
|
|
183
|
+
var OBSERVERKIT_NAMESPACE = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d";
|
|
184
|
+
function generateDebugId(content) {
|
|
185
|
+
return _uuid.v5.call(void 0, content, OBSERVERKIT_NAMESPACE);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ../plugin-core/src/snippet.ts
|
|
189
|
+
function buildStackKeyedDebugIdSnippet(debugId) {
|
|
190
|
+
const snippet = `globalThis._debugIds = globalThis._debugIds || {}; try { globalThis._debugIds[new Error().stack] = ${JSON.stringify(debugId)}; } catch (e) {}`;
|
|
191
|
+
return { snippet, lineCount: 1 };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
exports.generateDebugId = generateDebugId; exports.isJsonObject = isJsonObject; exports.buildStackKeyedDebugIdSnippet = buildStackKeyedDebugIdSnippet; exports.uploadSourceMaps = uploadSourceMaps;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});// ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.2_yaml@2.9.0/node_modules/tsup/assets/cjs_shims.js
|
|
2
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
3
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
exports.importMetaUrl = importMetaUrl;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// ../plugin-core/src/upload.ts
|
|
2
|
+
import { readdir, readFile, unlink, writeFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
// ../plugin-core/src/json.ts
|
|
6
|
+
function isJsonObject(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// ../plugin-core/src/upload.ts
|
|
11
|
+
var DEFAULT_MAX_FILE_BYTES = 50 * 1024 * 1024;
|
|
12
|
+
var DEFAULT_MAX_BATCH_BYTES = 100 * 1024 * 1024;
|
|
13
|
+
function hasDebugId(content) {
|
|
14
|
+
if (!content.includes('"debugId"')) return false;
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(content.toString("utf-8"));
|
|
17
|
+
return isJsonObject(parsed) && typeof parsed["debugId"] === "string";
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function batchBySize(entries, maxBatchBytes) {
|
|
23
|
+
const batches = [];
|
|
24
|
+
let current = [];
|
|
25
|
+
let currentBytes = 0;
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (current.length > 0 && currentBytes + entry.byteLength > maxBatchBytes) {
|
|
28
|
+
batches.push(current);
|
|
29
|
+
current = [];
|
|
30
|
+
currentBytes = 0;
|
|
31
|
+
}
|
|
32
|
+
current.push(entry);
|
|
33
|
+
currentBytes += entry.byteLength;
|
|
34
|
+
}
|
|
35
|
+
if (current.length > 0) batches.push(current);
|
|
36
|
+
return batches;
|
|
37
|
+
}
|
|
38
|
+
async function deleteMapFiles(directory, mapFiles) {
|
|
39
|
+
const results = await Promise.allSettled(
|
|
40
|
+
mapFiles.map((mapFile) => unlink(join(directory, mapFile)))
|
|
41
|
+
);
|
|
42
|
+
const failures = results.filter((r) => r.status === "rejected");
|
|
43
|
+
if (failures.length > 0) {
|
|
44
|
+
console.warn(
|
|
45
|
+
`[ObserverKit] Failed to delete ${failures.length}/${mapFiles.length} .map files`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
console.log(
|
|
49
|
+
`[ObserverKit] Deleted ${mapFiles.length - failures.length} .map files from output`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
async function uploadBatch(batch, options) {
|
|
53
|
+
const formData = new FormData();
|
|
54
|
+
for (const [index, entry] of batch.entries()) {
|
|
55
|
+
const content = await readFile(join(options.directory, entry.mapFile));
|
|
56
|
+
formData.append(
|
|
57
|
+
`file-${index}`,
|
|
58
|
+
new Blob([new Uint8Array(content)], { type: "application/json" }),
|
|
59
|
+
entry.mapFile
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const response = await fetch(options.url, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"X-ObserverKit-Key": options.projectKey,
|
|
66
|
+
"X-ObserverKit-Plugin-Version": options.pluginVersion,
|
|
67
|
+
"X-ObserverKit-Plugin-Name": options.pluginName
|
|
68
|
+
},
|
|
69
|
+
body: formData
|
|
70
|
+
});
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
const text = await response.text();
|
|
73
|
+
throw new Error(
|
|
74
|
+
`[ObserverKit] Source map upload failed: ${response.status} ${text}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
var JS_SOURCE_MAPPING_URL = /^\/\/[#@] sourceMappingURL=\S+[ \t]*$/gm;
|
|
79
|
+
var CSS_SOURCE_MAPPING_URL = /^\/\*# sourceMappingURL=\S+[ \t]*\*\/[ \t]*$/gm;
|
|
80
|
+
function isChunkFile(name) {
|
|
81
|
+
return name.endsWith(".js") || name.endsWith(".mjs") || name.endsWith(".cjs") || name.endsWith(".css");
|
|
82
|
+
}
|
|
83
|
+
async function stripSourceMappingUrls(directory, files) {
|
|
84
|
+
let stripped = 0;
|
|
85
|
+
for (const file of files.filter(isChunkFile)) {
|
|
86
|
+
const path = join(directory, file);
|
|
87
|
+
try {
|
|
88
|
+
const content = await readFile(path, "utf-8");
|
|
89
|
+
if (!content.includes("sourceMappingURL=")) continue;
|
|
90
|
+
const next = content.replace(JS_SOURCE_MAPPING_URL, "").replace(CSS_SOURCE_MAPPING_URL, "");
|
|
91
|
+
if (next !== content) {
|
|
92
|
+
await writeFile(path, next);
|
|
93
|
+
stripped += 1;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (stripped > 0) {
|
|
99
|
+
console.log(
|
|
100
|
+
`[ObserverKit] Stripped sourceMappingURL comments from ${stripped} file(s)`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function uploadSourceMaps(options) {
|
|
105
|
+
const { projectKey, directory, endpoint, deleteAfterUpload, pluginVersion, pluginName } = options;
|
|
106
|
+
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
107
|
+
const maxBatchBytes = options.maxBatchBytes ?? DEFAULT_MAX_BATCH_BYTES;
|
|
108
|
+
const allFiles = options.files ? [...options.files] : await readdir(directory, { recursive: true, encoding: "utf-8" });
|
|
109
|
+
const allMapFiles = allFiles.filter((f) => f.endsWith(".map"));
|
|
110
|
+
if (allMapFiles.length === 0) {
|
|
111
|
+
console.log("[ObserverKit] No .map files found, skipping source map upload");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const uploadable = [];
|
|
116
|
+
let skippedNoDebugId = 0;
|
|
117
|
+
let skippedOversize = 0;
|
|
118
|
+
let skippedUnreadable = 0;
|
|
119
|
+
for (const mapFile of allMapFiles) {
|
|
120
|
+
let content;
|
|
121
|
+
try {
|
|
122
|
+
content = await readFile(join(directory, mapFile));
|
|
123
|
+
} catch {
|
|
124
|
+
skippedUnreadable += 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!hasDebugId(content)) {
|
|
128
|
+
skippedNoDebugId += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (content.byteLength > maxFileBytes) {
|
|
132
|
+
skippedOversize += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
uploadable.push({ mapFile, byteLength: content.byteLength });
|
|
136
|
+
}
|
|
137
|
+
if (skippedNoDebugId > 0) {
|
|
138
|
+
console.log(
|
|
139
|
+
`[ObserverKit] Skipping ${skippedNoDebugId} source map(s) without a debugId field`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (skippedOversize > 0) {
|
|
143
|
+
console.warn(
|
|
144
|
+
`[ObserverKit] Skipping ${skippedOversize} source map(s) over the per-file ingest limit`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (skippedUnreadable > 0) {
|
|
148
|
+
console.warn(
|
|
149
|
+
`[ObserverKit] Skipping ${skippedUnreadable} unreadable source map(s)`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (uploadable.length === 0) {
|
|
153
|
+
console.log(
|
|
154
|
+
"[ObserverKit] No uploadable source maps found, skipping upload"
|
|
155
|
+
);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const url = `${endpoint.replace(/\/+$/, "")}/v1/sourcemaps`;
|
|
159
|
+
const batches = batchBySize(uploadable, maxBatchBytes);
|
|
160
|
+
for (const [index, batch] of batches.entries()) {
|
|
161
|
+
await uploadBatch(batch, { directory, url, projectKey, pluginVersion, pluginName });
|
|
162
|
+
console.log(
|
|
163
|
+
`[ObserverKit] Uploaded batch ${index + 1}/${batches.length} (${batch.length} source maps)`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (deleteAfterUpload) {
|
|
168
|
+
console.warn(
|
|
169
|
+
"[ObserverKit] Upload failed \u2014 deleting .map files anyway so they don't ship (set deleteAfterUpload: false to keep them)"
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
throw err;
|
|
173
|
+
} finally {
|
|
174
|
+
if (deleteAfterUpload) {
|
|
175
|
+
await deleteMapFiles(directory, allMapFiles);
|
|
176
|
+
await stripSourceMappingUrls(directory, allFiles);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ../plugin-core/src/debug-id.ts
|
|
182
|
+
import { v5 } from "uuid";
|
|
183
|
+
var OBSERVERKIT_NAMESPACE = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d";
|
|
184
|
+
function generateDebugId(content) {
|
|
185
|
+
return v5(content, OBSERVERKIT_NAMESPACE);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ../plugin-core/src/snippet.ts
|
|
189
|
+
function buildStackKeyedDebugIdSnippet(debugId) {
|
|
190
|
+
const snippet = `globalThis._debugIds = globalThis._debugIds || {}; try { globalThis._debugIds[new Error().stack] = ${JSON.stringify(debugId)}; } catch (e) {}`;
|
|
191
|
+
return { snippet, lineCount: 1 };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export {
|
|
195
|
+
generateDebugId,
|
|
196
|
+
isJsonObject,
|
|
197
|
+
buildStackKeyedDebugIdSnippet,
|
|
198
|
+
uploadSourceMaps
|
|
199
|
+
};
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict"; function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
var _chunk6RJOM2COcjs = require('./chunk-6RJOM2CO.cjs');
|
|
6
|
+
require('./chunk-AUOVSTQX.cjs');
|
|
7
|
+
|
|
8
|
+
// src/cli.ts
|
|
9
|
+
var _util = require('util');
|
|
10
|
+
var _path = require('path');
|
|
11
|
+
|
|
12
|
+
// src/repair.ts
|
|
13
|
+
var _promises = require('fs/promises');
|
|
14
|
+
var DEBUG_ID_COMMENT_PATTERN = /\/\/# debugId=([0-9a-f-]{36})/;
|
|
15
|
+
function defaultPackagerMapPath(mapPath) {
|
|
16
|
+
return mapPath.endsWith(".map") ? `${mapPath.slice(0, -".map".length)}.packager.map` : `${mapPath}.packager.map`;
|
|
17
|
+
}
|
|
18
|
+
function defaultBundlePath(mapPath) {
|
|
19
|
+
return mapPath.endsWith(".map") ? mapPath.slice(0, -".map".length) : mapPath;
|
|
20
|
+
}
|
|
21
|
+
async function readDebugIdFromMap(path) {
|
|
22
|
+
let content;
|
|
23
|
+
try {
|
|
24
|
+
content = await _promises.readFile.call(void 0, path, "utf-8");
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(content);
|
|
31
|
+
} catch (e2) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!_chunk6RJOM2COcjs.isJsonObject.call(void 0, parsed)) return null;
|
|
35
|
+
return typeof parsed["debugId"] === "string" ? parsed["debugId"] : null;
|
|
36
|
+
}
|
|
37
|
+
async function readDebugIdFromBundle(path) {
|
|
38
|
+
let content;
|
|
39
|
+
try {
|
|
40
|
+
content = await _promises.readFile.call(void 0, path, "utf-8");
|
|
41
|
+
} catch (e3) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const match = DEBUG_ID_COMMENT_PATTERN.exec(content);
|
|
45
|
+
return _nullishCoalesce(_optionalChain([match, 'optionalAccess', _ => _[1]]), () => ( null));
|
|
46
|
+
}
|
|
47
|
+
async function repairDebugId(mapPath, packagerMapPath, bundlePath) {
|
|
48
|
+
const content = await _promises.readFile.call(void 0, mapPath, "utf-8");
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(content);
|
|
52
|
+
} catch (e4) {
|
|
53
|
+
throw new Error(`[ObserverKit] ${mapPath} is not valid JSON`);
|
|
54
|
+
}
|
|
55
|
+
if (!_chunk6RJOM2COcjs.isJsonObject.call(void 0, parsed)) {
|
|
56
|
+
throw new Error(`[ObserverKit] ${mapPath} is not a source map object`);
|
|
57
|
+
}
|
|
58
|
+
if (typeof parsed["debugId"] === "string") return;
|
|
59
|
+
const packagerCandidate = _nullishCoalesce(packagerMapPath, () => ( defaultPackagerMapPath(mapPath)));
|
|
60
|
+
let debugId = await readDebugIdFromMap(packagerCandidate);
|
|
61
|
+
const bundleCandidate = _nullishCoalesce(bundlePath, () => ( defaultBundlePath(mapPath)));
|
|
62
|
+
if (!debugId) {
|
|
63
|
+
debugId = await readDebugIdFromBundle(bundleCandidate);
|
|
64
|
+
}
|
|
65
|
+
if (!debugId) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`[ObserverKit] ${mapPath} has no debugId and none was found in ${packagerCandidate} or ${bundleCandidate}. Is withObserverkit applied in metro.config.js? For Hermes iOS builds, pass --bundle pointing at the pre-Hermes JS bundle (e.g. $CONFIGURATION_BUILD_DIR/main.jsbundle).`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
await _promises.writeFile.call(void 0, mapPath, JSON.stringify({ ...parsed, debugId }));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/cli.ts
|
|
74
|
+
var PLUGIN_VERSION = "0.2.0";
|
|
75
|
+
var USAGE = `Usage:
|
|
76
|
+
observerkit-upload --map <path> [--packager-map <path>] [--bundle <path>]
|
|
77
|
+
observerkit-upload --dir <path>
|
|
78
|
+
|
|
79
|
+
Options:
|
|
80
|
+
--map <path> Final source map to upload (repairs debugId first)
|
|
81
|
+
--packager-map <path> Pre-compose Metro map to copy the debugId from
|
|
82
|
+
--bundle <path> Pre-Hermes JS bundle to read a trailing
|
|
83
|
+
"//# debugId=<uuid>" comment from, used when no
|
|
84
|
+
packager map is available (Hermes iOS builds)
|
|
85
|
+
--dir <path> Upload every debug-id source map in a directory
|
|
86
|
+
(e.g. an expo export output)
|
|
87
|
+
--project-key <key> Defaults to $OBSERVERKIT_PROJECT_KEY
|
|
88
|
+
--endpoint <url> Defaults to $OBSERVERKIT_ENDPOINT or
|
|
89
|
+
https://ingest.observerkit.com`;
|
|
90
|
+
async function main() {
|
|
91
|
+
const { values } = _util.parseArgs.call(void 0, {
|
|
92
|
+
options: {
|
|
93
|
+
map: { type: "string" },
|
|
94
|
+
"packager-map": { type: "string" },
|
|
95
|
+
bundle: { type: "string" },
|
|
96
|
+
dir: { type: "string" },
|
|
97
|
+
"project-key": { type: "string" },
|
|
98
|
+
endpoint: { type: "string" }
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const projectKey = _nullishCoalesce(values["project-key"], () => ( process.env["OBSERVERKIT_PROJECT_KEY"]));
|
|
102
|
+
if (!projectKey) {
|
|
103
|
+
console.error(
|
|
104
|
+
"[ObserverKit] Missing project key: pass --project-key or set OBSERVERKIT_PROJECT_KEY"
|
|
105
|
+
);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
const endpoint = _nullishCoalesce(_nullishCoalesce(values.endpoint, () => ( process.env["OBSERVERKIT_ENDPOINT"])), () => ( "https://ingest.observerkit.com"));
|
|
109
|
+
if (!values.map && !values.dir) {
|
|
110
|
+
console.error(USAGE);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
if (values.dir && (values["packager-map"] || values.bundle)) {
|
|
114
|
+
console.error(
|
|
115
|
+
"[ObserverKit] --packager-map and --bundle are ignored when --dir is set"
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (values.map) {
|
|
119
|
+
if (!values.map.endsWith(".map")) {
|
|
120
|
+
console.error(`[ObserverKit] --map must point at a .map file, got "${values.map}"`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
const mapPath = _path.resolve.call(void 0, values.map);
|
|
124
|
+
const packagerMap = values["packager-map"];
|
|
125
|
+
const bundle = values.bundle;
|
|
126
|
+
await repairDebugId(
|
|
127
|
+
mapPath,
|
|
128
|
+
packagerMap ? _path.resolve.call(void 0, packagerMap) : void 0,
|
|
129
|
+
bundle ? _path.resolve.call(void 0, bundle) : void 0
|
|
130
|
+
);
|
|
131
|
+
await _chunk6RJOM2COcjs.uploadSourceMaps.call(void 0, {
|
|
132
|
+
projectKey,
|
|
133
|
+
directory: _path.dirname.call(void 0, mapPath),
|
|
134
|
+
endpoint,
|
|
135
|
+
deleteAfterUpload: false,
|
|
136
|
+
pluginVersion: PLUGIN_VERSION,
|
|
137
|
+
pluginName: "metro",
|
|
138
|
+
files: [_path.basename.call(void 0, mapPath)]
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (values.dir) {
|
|
143
|
+
await _chunk6RJOM2COcjs.uploadSourceMaps.call(void 0, {
|
|
144
|
+
projectKey,
|
|
145
|
+
directory: _path.resolve.call(void 0, values.dir),
|
|
146
|
+
endpoint,
|
|
147
|
+
deleteAfterUpload: false,
|
|
148
|
+
pluginVersion: PLUGIN_VERSION,
|
|
149
|
+
pluginName: "metro"
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
main().catch((err) => {
|
|
154
|
+
console.error(err instanceof Error ? err.message : err);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
});
|
package/dist/cli.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|