@goliapkg/sentori-cli 0.2.1 → 0.3.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/lib/index.js +134 -59
- package/lib/index.js.map +1 -1
- package/lib/react-native.d.ts +25 -0
- package/lib/react-native.d.ts.map +1 -0
- package/lib/react-native.js +74 -0
- package/lib/react-native.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/react-native.test.ts +37 -0
- package/src/index.ts +140 -61
- package/src/react-native.ts +87 -0
package/lib/index.js
CHANGED
|
@@ -1,56 +1,76 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
|
+
import { reactNativeUpload } from './react-native.js';
|
|
3
4
|
import { uploadSourcemaps } from './upload.js';
|
|
4
5
|
const HELP = `sentori-cli — upload release artifacts to Sentori
|
|
5
6
|
|
|
6
7
|
Usage:
|
|
7
8
|
sentori-cli upload sourcemap [options] <path...>
|
|
9
|
+
Upload one or more files or directories. A directory is scanned
|
|
10
|
+
(one level) for *.map / *.js / *.jsbundle / *.bundle / *.hbc;
|
|
11
|
+
a file given explicitly is uploaded as-is. Use this for web
|
|
12
|
+
bundlers (point at the build dir) or for already-composed RN maps.
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
sentori-cli react-native upload [options]
|
|
15
|
+
Compose a Metro packager map + a Hermes map into one source map
|
|
16
|
+
and upload it (plus the bundle). Use this for a Hermes release
|
|
17
|
+
build. Requires --metro-map and --hermes-map.
|
|
12
18
|
|
|
13
|
-
Options:
|
|
19
|
+
Options (both commands):
|
|
14
20
|
--release <r> release identifier — MUST equal the value the SDK
|
|
15
21
|
reports via init({ release }). Required. A mismatch
|
|
16
22
|
means the dashboard silently can't symbolicate.
|
|
17
23
|
--token <t> Sentori token (or set $SENTORI_TOKEN).
|
|
18
24
|
--api-url <url> Sentori API base (default https://api.sentori.golia.jp,
|
|
19
|
-
or $SENTORI_API_URL). For a self-hosted instance,
|
|
20
|
-
host. (Accepts --ingest-url as an alias.)
|
|
21
|
-
--dry-run
|
|
25
|
+
or $SENTORI_API_URL). For a self-hosted instance,
|
|
26
|
+
your host. (Accepts --ingest-url as an alias.)
|
|
27
|
+
--dry-run describe what would be uploaded; don't upload.
|
|
22
28
|
-h, --help show this help.
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
(
|
|
30
|
+
Options (react-native upload):
|
|
31
|
+
--metro-map <p> the *.packager.map Metro emits (--sourcemap-output).
|
|
32
|
+
--hermes-map <p> the *.hbc.map the Hermes compiler emits.
|
|
33
|
+
--bundle <p> optional: also upload the bundle (.jsbundle / .bundle).
|
|
34
|
+
|
|
35
|
+
Hermes release build, by hand:
|
|
36
|
+
npx react-native bundle --platform ios --dev false --entry-file index.js \\
|
|
37
|
+
--bundle-output main.jsbundle --sourcemap-output main.jsbundle.packager.map
|
|
38
|
+
# (the iOS/Android build compiles to Hermes and writes main.jsbundle.hbc.map)
|
|
39
|
+
npx @goliapkg/sentori-cli react-native upload \\
|
|
40
|
+
--release "<app>@<version>+<build>" --token "$SENTORI_TOKEN" \\
|
|
41
|
+
--metro-map main.jsbundle.packager.map --hermes-map main.jsbundle.hbc.map \\
|
|
42
|
+
--bundle main.jsbundle
|
|
33
43
|
`;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
/** Parse the shared options, or print an error + return null. */
|
|
45
|
+
function parseCommon(values) {
|
|
46
|
+
const release = typeof values.release === 'string' ? values.release : undefined;
|
|
47
|
+
if (!release) {
|
|
48
|
+
console.error('error: --release is required (must match the SDK’s init({ release }))');
|
|
49
|
+
return null;
|
|
38
50
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
const dryRun = values['dry-run'] === true;
|
|
52
|
+
const token = (typeof values.token === 'string' ? values.token : undefined) ?? process.env.SENTORI_TOKEN;
|
|
53
|
+
if (!token && !dryRun) {
|
|
54
|
+
console.error('error: --token (or $SENTORI_TOKEN) is required');
|
|
55
|
+
return null;
|
|
43
56
|
}
|
|
57
|
+
const apiUrl = (typeof values['api-url'] === 'string' ? values['api-url'] : undefined) ??
|
|
58
|
+
(typeof values['ingest-url'] === 'string' ? values['ingest-url'] : undefined) ??
|
|
59
|
+
process.env.SENTORI_API_URL ??
|
|
60
|
+
'https://api.sentori.golia.jp';
|
|
61
|
+
return { apiUrl, dryRun, release, token: token ?? '' };
|
|
62
|
+
}
|
|
63
|
+
async function cmdUploadSourcemap(argv) {
|
|
44
64
|
let parsed;
|
|
45
65
|
try {
|
|
46
66
|
parsed = parseArgs({
|
|
47
67
|
allowPositionals: true,
|
|
48
|
-
args: argv
|
|
68
|
+
args: argv,
|
|
49
69
|
options: {
|
|
50
70
|
'api-url': { type: 'string' },
|
|
51
71
|
'dry-run': { type: 'boolean' },
|
|
52
72
|
help: { short: 'h', type: 'boolean' },
|
|
53
|
-
'ingest-url': { type: 'string' },
|
|
73
|
+
'ingest-url': { type: 'string' },
|
|
54
74
|
release: { type: 'string' },
|
|
55
75
|
token: { type: 'string' },
|
|
56
76
|
},
|
|
@@ -61,49 +81,26 @@ async function main(argv) {
|
|
|
61
81
|
console.error(HELP);
|
|
62
82
|
return 2;
|
|
63
83
|
}
|
|
64
|
-
|
|
65
|
-
if (values.help) {
|
|
84
|
+
if (parsed.values.help) {
|
|
66
85
|
console.log(HELP);
|
|
67
86
|
return 0;
|
|
68
87
|
}
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
71
|
-
console.error('error: --release is required (must match the SDK’s init({ release }))');
|
|
72
|
-
return 2;
|
|
73
|
-
}
|
|
74
|
-
const dryRun = !!values['dry-run'];
|
|
75
|
-
const token = values.token ?? process.env.SENTORI_TOKEN;
|
|
76
|
-
if (!token && !dryRun) {
|
|
77
|
-
console.error('error: --token (or $SENTORI_TOKEN) is required');
|
|
88
|
+
const c = parseCommon(parsed.values);
|
|
89
|
+
if (!c)
|
|
78
90
|
return 2;
|
|
79
|
-
|
|
80
|
-
const apiUrl = values['api-url'] ??
|
|
81
|
-
values['ingest-url'] ??
|
|
82
|
-
process.env.SENTORI_API_URL ??
|
|
83
|
-
'https://api.sentori.golia.jp';
|
|
84
|
-
if (positionals.length === 0) {
|
|
91
|
+
if (parsed.positionals.length === 0) {
|
|
85
92
|
console.error('error: at least one path (file or directory) is required');
|
|
86
93
|
return 2;
|
|
87
94
|
}
|
|
88
95
|
try {
|
|
89
96
|
const result = await uploadSourcemaps({
|
|
90
|
-
apiUrl,
|
|
91
|
-
dryRun,
|
|
92
|
-
paths: positionals,
|
|
93
|
-
release,
|
|
94
|
-
token: token
|
|
97
|
+
apiUrl: c.apiUrl,
|
|
98
|
+
dryRun: c.dryRun,
|
|
99
|
+
paths: parsed.positionals,
|
|
100
|
+
release: c.release,
|
|
101
|
+
token: c.token,
|
|
95
102
|
});
|
|
96
|
-
|
|
97
|
-
console.log(`would upload ${result.files.length} file(s) to ${apiUrl.replace(/\/+$/, '')}/admin/api/releases/${encodeURIComponent(release)}/sourcemaps:`);
|
|
98
|
-
for (const f of result.files)
|
|
99
|
-
console.log(` ${f}`);
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
console.log(`uploaded ${result.uploaded} file(s) for release "${release}"`);
|
|
103
|
-
for (const a of result.artifacts ?? [])
|
|
104
|
-
console.log(` ${a.name} [${a.kind}]`);
|
|
105
|
-
console.log('done — minified stack traces on this release will now resolve to source.');
|
|
106
|
-
}
|
|
103
|
+
reportUpload(result, c);
|
|
107
104
|
return 0;
|
|
108
105
|
}
|
|
109
106
|
catch (e) {
|
|
@@ -111,6 +108,84 @@ async function main(argv) {
|
|
|
111
108
|
return 1;
|
|
112
109
|
}
|
|
113
110
|
}
|
|
111
|
+
async function cmdReactNativeUpload(argv) {
|
|
112
|
+
let parsed;
|
|
113
|
+
try {
|
|
114
|
+
parsed = parseArgs({
|
|
115
|
+
args: argv,
|
|
116
|
+
options: {
|
|
117
|
+
'api-url': { type: 'string' },
|
|
118
|
+
bundle: { type: 'string' },
|
|
119
|
+
'dry-run': { type: 'boolean' },
|
|
120
|
+
help: { short: 'h', type: 'boolean' },
|
|
121
|
+
'hermes-map': { type: 'string' },
|
|
122
|
+
'ingest-url': { type: 'string' },
|
|
123
|
+
'metro-map': { type: 'string' },
|
|
124
|
+
release: { type: 'string' },
|
|
125
|
+
token: { type: 'string' },
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
console.error(`error: ${e.message}\n`);
|
|
131
|
+
console.error(HELP);
|
|
132
|
+
return 2;
|
|
133
|
+
}
|
|
134
|
+
if (parsed.values.help) {
|
|
135
|
+
console.log(HELP);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
const c = parseCommon(parsed.values);
|
|
139
|
+
if (!c)
|
|
140
|
+
return 2;
|
|
141
|
+
const metroMap = parsed.values['metro-map'];
|
|
142
|
+
const hermesMap = parsed.values['hermes-map'];
|
|
143
|
+
if (typeof metroMap !== 'string' || typeof hermesMap !== 'string') {
|
|
144
|
+
console.error('error: --metro-map and --hermes-map are both required');
|
|
145
|
+
return 2;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const result = await reactNativeUpload({
|
|
149
|
+
apiUrl: c.apiUrl,
|
|
150
|
+
bundle: typeof parsed.values.bundle === 'string' ? parsed.values.bundle : undefined,
|
|
151
|
+
dryRun: c.dryRun,
|
|
152
|
+
hermesMap,
|
|
153
|
+
metroMap,
|
|
154
|
+
release: c.release,
|
|
155
|
+
token: c.token,
|
|
156
|
+
});
|
|
157
|
+
reportUpload(result, c);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
console.error(`react-native upload failed: ${e.message}`);
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function reportUpload(result, c) {
|
|
166
|
+
if (c.dryRun) {
|
|
167
|
+
console.log(`would upload ${result.files.length} file(s) to ${c.apiUrl.replace(/\/+$/, '')}/admin/api/releases/${encodeURIComponent(c.release)}/sourcemaps:`);
|
|
168
|
+
for (const f of result.files)
|
|
169
|
+
console.log(` ${f}`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log(`uploaded ${result.uploaded ?? result.files.length} file(s) for release "${c.release}" — minified stacks on this release will now resolve to source.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function main(argv) {
|
|
176
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
177
|
+
console.log(HELP);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
const [a, b, ...rest] = argv;
|
|
181
|
+
if (a === 'upload' && b === 'sourcemap')
|
|
182
|
+
return cmdUploadSourcemap(rest);
|
|
183
|
+
if (a === 'react-native' && b === 'upload')
|
|
184
|
+
return cmdReactNativeUpload(rest);
|
|
185
|
+
console.error(`unknown command: ${[a, b].filter(Boolean).join(' ') || '(none)'}\n`);
|
|
186
|
+
console.error(HELP);
|
|
187
|
+
return 2;
|
|
188
|
+
}
|
|
114
189
|
main(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
|
|
115
190
|
console.error(`fatal: ${e.message}`);
|
|
116
191
|
process.exit(1);
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAErC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAE9C,MAAM,IAAI,GAAG
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Resolve `react-native/scripts/compose-source-maps.js` from the
|
|
2
|
+
* current project's node_modules. Returns null if react-native isn't
|
|
3
|
+
* installed or the version doesn't ship that script. */
|
|
4
|
+
export declare function resolveComposeScript(fromDir?: string): null | string;
|
|
5
|
+
/** Compose a Metro packager source map + a Hermes source map into a
|
|
6
|
+
* single map (a temp file the caller is responsible for deleting).
|
|
7
|
+
* Throws if react-native's compose script can't be found or fails. */
|
|
8
|
+
export declare function composeSourceMaps(metroMap: string, hermesMap: string): string;
|
|
9
|
+
export type RnUploadOptions = {
|
|
10
|
+
apiUrl: string;
|
|
11
|
+
/** Optional bundle file (.jsbundle / .bundle) to upload alongside the map. */
|
|
12
|
+
bundle?: string;
|
|
13
|
+
dryRun?: boolean;
|
|
14
|
+
hermesMap: string;
|
|
15
|
+
metroMap: string;
|
|
16
|
+
release: string;
|
|
17
|
+
token: string;
|
|
18
|
+
};
|
|
19
|
+
/** Compose the Metro + Hermes maps, then upload the result (and the
|
|
20
|
+
* bundle, if given). Cleans up the temp composed map. */
|
|
21
|
+
export declare function reactNativeUpload(opts: RnUploadOptions): Promise<{
|
|
22
|
+
files: string[];
|
|
23
|
+
uploaded?: number;
|
|
24
|
+
}>;
|
|
25
|
+
//# sourceMappingURL=react-native.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-native.d.ts","sourceRoot":"","sources":["../src/react-native.ts"],"names":[],"mappings":"AAQA;;yDAEyD;AACzD,wBAAgB,oBAAoB,CAAC,OAAO,SAAgB,GAAG,IAAI,GAAG,MAAM,CAc3E;AAED;;uEAEuE;AACvE,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAkB7E;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED;0DAC0D;AAC1D,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC;IACtE,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAC,CAoBD"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { uploadSourcemaps } from './upload.js';
|
|
7
|
+
/** Resolve `react-native/scripts/compose-source-maps.js` from the
|
|
8
|
+
* current project's node_modules. Returns null if react-native isn't
|
|
9
|
+
* installed or the version doesn't ship that script. */
|
|
10
|
+
export function resolveComposeScript(fromDir = process.cwd()) {
|
|
11
|
+
const req = createRequire(join(fromDir, 'noop.js'));
|
|
12
|
+
for (const id of [
|
|
13
|
+
'react-native/scripts/compose-source-maps.js',
|
|
14
|
+
'@react-native/community-cli-plugin/dist/utils/composeSourceMaps.js',
|
|
15
|
+
]) {
|
|
16
|
+
try {
|
|
17
|
+
const p = req.resolve(id);
|
|
18
|
+
if (existsSync(p))
|
|
19
|
+
return p;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// try next
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/** Compose a Metro packager source map + a Hermes source map into a
|
|
28
|
+
* single map (a temp file the caller is responsible for deleting).
|
|
29
|
+
* Throws if react-native's compose script can't be found or fails. */
|
|
30
|
+
export function composeSourceMaps(metroMap, hermesMap) {
|
|
31
|
+
for (const p of [metroMap, hermesMap]) {
|
|
32
|
+
if (!existsSync(p))
|
|
33
|
+
throw new Error(`no such file: ${p}`);
|
|
34
|
+
}
|
|
35
|
+
const script = resolveComposeScript();
|
|
36
|
+
if (!script) {
|
|
37
|
+
throw new Error("couldn't find react-native's compose-source-maps.js — install react-native, or " +
|
|
38
|
+
'compose the maps yourself and use `sentori-cli upload sourcemap <composed.map>`');
|
|
39
|
+
}
|
|
40
|
+
const out = join(mkdtempSync(join(tmpdir(), 'sentori-rn-')), 'composed.map');
|
|
41
|
+
const r = spawnSync('node', [script, metroMap, hermesMap, '-o', out], { stdio: 'inherit' });
|
|
42
|
+
if (r.status !== 0) {
|
|
43
|
+
throw new Error(`compose-source-maps.js exited with ${r.status ?? 'signal'}`);
|
|
44
|
+
}
|
|
45
|
+
if (!existsSync(out))
|
|
46
|
+
throw new Error('compose-source-maps.js produced no output');
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** Compose the Metro + Hermes maps, then upload the result (and the
|
|
50
|
+
* bundle, if given). Cleans up the temp composed map. */
|
|
51
|
+
export async function reactNativeUpload(opts) {
|
|
52
|
+
const composed = composeSourceMaps(opts.metroMap, opts.hermesMap);
|
|
53
|
+
try {
|
|
54
|
+
const paths = [composed, ...(opts.bundle ? [opts.bundle] : [])];
|
|
55
|
+
const r = await uploadSourcemaps({
|
|
56
|
+
apiUrl: opts.apiUrl,
|
|
57
|
+
dryRun: opts.dryRun,
|
|
58
|
+
paths,
|
|
59
|
+
release: opts.release,
|
|
60
|
+
token: opts.token,
|
|
61
|
+
});
|
|
62
|
+
return { files: r.files, uploaded: r.uploaded };
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
try {
|
|
66
|
+
rmSync(composed, { force: true });
|
|
67
|
+
rmSync(join(composed, '..'), { force: true, recursive: true });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// best-effort cleanup
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=react-native.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-native.js","sourceRoot":"","sources":["../src/react-native.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
6
|
+
|
|
7
|
+
import { composeSourceMaps, resolveComposeScript } from '../react-native.js'
|
|
8
|
+
|
|
9
|
+
let dir = ''
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
dir = await mkdtemp(join(tmpdir(), 'sentori-cli-rn-'))
|
|
12
|
+
})
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await rm(dir, { force: true, recursive: true })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('react-native helpers', () => {
|
|
18
|
+
test('resolveComposeScript returns null when react-native is not installed', () => {
|
|
19
|
+
// This package doesn't depend on react-native, so from sdk/cli's
|
|
20
|
+
// own node_modules there's nothing to find.
|
|
21
|
+
expect(resolveComposeScript()).toBeNull()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('composeSourceMaps throws "no such file" for a missing input', () => {
|
|
25
|
+
expect(() => composeSourceMaps(join(dir, 'nope.map'), join(dir, 'nope2.map'))).toThrow(
|
|
26
|
+
'no such file',
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('composeSourceMaps throws a helpful message when react-native is absent', async () => {
|
|
31
|
+
await writeFile(join(dir, 'a.packager.map'), '{"version":3}')
|
|
32
|
+
await writeFile(join(dir, 'a.hbc.map'), '{"version":3}')
|
|
33
|
+
expect(() =>
|
|
34
|
+
composeSourceMaps(join(dir, 'a.packager.map'), join(dir, 'a.hbc.map')),
|
|
35
|
+
).toThrow(/compose-source-maps\.js/)
|
|
36
|
+
})
|
|
37
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -1,60 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util'
|
|
3
3
|
|
|
4
|
+
import { reactNativeUpload } from './react-native.js'
|
|
4
5
|
import { uploadSourcemaps } from './upload.js'
|
|
5
6
|
|
|
6
7
|
const HELP = `sentori-cli — upload release artifacts to Sentori
|
|
7
8
|
|
|
8
9
|
Usage:
|
|
9
10
|
sentori-cli upload sourcemap [options] <path...>
|
|
11
|
+
Upload one or more files or directories. A directory is scanned
|
|
12
|
+
(one level) for *.map / *.js / *.jsbundle / *.bundle / *.hbc;
|
|
13
|
+
a file given explicitly is uploaded as-is. Use this for web
|
|
14
|
+
bundlers (point at the build dir) or for already-composed RN maps.
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
sentori-cli react-native upload [options]
|
|
17
|
+
Compose a Metro packager map + a Hermes map into one source map
|
|
18
|
+
and upload it (plus the bundle). Use this for a Hermes release
|
|
19
|
+
build. Requires --metro-map and --hermes-map.
|
|
14
20
|
|
|
15
|
-
Options:
|
|
21
|
+
Options (both commands):
|
|
16
22
|
--release <r> release identifier — MUST equal the value the SDK
|
|
17
23
|
reports via init({ release }). Required. A mismatch
|
|
18
24
|
means the dashboard silently can't symbolicate.
|
|
19
25
|
--token <t> Sentori token (or set $SENTORI_TOKEN).
|
|
20
26
|
--api-url <url> Sentori API base (default https://api.sentori.golia.jp,
|
|
21
|
-
or $SENTORI_API_URL). For a self-hosted instance,
|
|
22
|
-
host. (Accepts --ingest-url as an alias.)
|
|
23
|
-
--dry-run
|
|
27
|
+
or $SENTORI_API_URL). For a self-hosted instance,
|
|
28
|
+
your host. (Accepts --ingest-url as an alias.)
|
|
29
|
+
--dry-run describe what would be uploaded; don't upload.
|
|
24
30
|
-h, --help show this help.
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
(
|
|
32
|
+
Options (react-native upload):
|
|
33
|
+
--metro-map <p> the *.packager.map Metro emits (--sourcemap-output).
|
|
34
|
+
--hermes-map <p> the *.hbc.map the Hermes compiler emits.
|
|
35
|
+
--bundle <p> optional: also upload the bundle (.jsbundle / .bundle).
|
|
36
|
+
|
|
37
|
+
Hermes release build, by hand:
|
|
38
|
+
npx react-native bundle --platform ios --dev false --entry-file index.js \\
|
|
39
|
+
--bundle-output main.jsbundle --sourcemap-output main.jsbundle.packager.map
|
|
40
|
+
# (the iOS/Android build compiles to Hermes and writes main.jsbundle.hbc.map)
|
|
41
|
+
npx @goliapkg/sentori-cli react-native upload \\
|
|
42
|
+
--release "<app>@<version>+<build>" --token "$SENTORI_TOKEN" \\
|
|
43
|
+
--metro-map main.jsbundle.packager.map --hermes-map main.jsbundle.hbc.map \\
|
|
44
|
+
--bundle main.jsbundle
|
|
35
45
|
`
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
type Common = { apiUrl: string; dryRun: boolean; release: string; token: string }
|
|
48
|
+
|
|
49
|
+
/** Parse the shared options, or print an error + return null. */
|
|
50
|
+
function parseCommon(values: Record<string, unknown>): Common | null {
|
|
51
|
+
const release = typeof values.release === 'string' ? values.release : undefined
|
|
52
|
+
if (!release) {
|
|
53
|
+
console.error('error: --release is required (must match the SDK’s init({ release }))')
|
|
54
|
+
return null
|
|
41
55
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
const dryRun = values['dry-run'] === true
|
|
57
|
+
const token =
|
|
58
|
+
(typeof values.token === 'string' ? values.token : undefined) ?? process.env.SENTORI_TOKEN
|
|
59
|
+
if (!token && !dryRun) {
|
|
60
|
+
console.error('error: --token (or $SENTORI_TOKEN) is required')
|
|
61
|
+
return null
|
|
46
62
|
}
|
|
63
|
+
const apiUrl =
|
|
64
|
+
(typeof values['api-url'] === 'string' ? values['api-url'] : undefined) ??
|
|
65
|
+
(typeof values['ingest-url'] === 'string' ? values['ingest-url'] : undefined) ??
|
|
66
|
+
process.env.SENTORI_API_URL ??
|
|
67
|
+
'https://api.sentori.golia.jp'
|
|
68
|
+
return { apiUrl, dryRun, release, token: token ?? '' }
|
|
69
|
+
}
|
|
47
70
|
|
|
71
|
+
async function cmdUploadSourcemap(argv: string[]): Promise<number> {
|
|
48
72
|
let parsed
|
|
49
73
|
try {
|
|
50
74
|
parsed = parseArgs({
|
|
51
75
|
allowPositionals: true,
|
|
52
|
-
args: argv
|
|
76
|
+
args: argv,
|
|
53
77
|
options: {
|
|
54
78
|
'api-url': { type: 'string' },
|
|
55
79
|
'dry-run': { type: 'boolean' },
|
|
56
80
|
help: { short: 'h', type: 'boolean' },
|
|
57
|
-
'ingest-url': { type: 'string' },
|
|
81
|
+
'ingest-url': { type: 'string' },
|
|
58
82
|
release: { type: 'string' },
|
|
59
83
|
token: { type: 'string' },
|
|
60
84
|
},
|
|
@@ -64,58 +88,113 @@ async function main(argv: string[]): Promise<number> {
|
|
|
64
88
|
console.error(HELP)
|
|
65
89
|
return 2
|
|
66
90
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (values.help) {
|
|
91
|
+
if (parsed.values.help) {
|
|
70
92
|
console.log(HELP)
|
|
71
93
|
return 0
|
|
72
94
|
}
|
|
73
|
-
const
|
|
74
|
-
if (!
|
|
75
|
-
|
|
95
|
+
const c = parseCommon(parsed.values)
|
|
96
|
+
if (!c) return 2
|
|
97
|
+
if (parsed.positionals.length === 0) {
|
|
98
|
+
console.error('error: at least one path (file or directory) is required')
|
|
76
99
|
return 2
|
|
77
100
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
101
|
+
try {
|
|
102
|
+
const result = await uploadSourcemaps({
|
|
103
|
+
apiUrl: c.apiUrl,
|
|
104
|
+
dryRun: c.dryRun,
|
|
105
|
+
paths: parsed.positionals,
|
|
106
|
+
release: c.release,
|
|
107
|
+
token: c.token,
|
|
108
|
+
})
|
|
109
|
+
reportUpload(result, c)
|
|
110
|
+
return 0
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.error(`upload failed: ${(e as Error).message}`)
|
|
113
|
+
return 1
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function cmdReactNativeUpload(argv: string[]): Promise<number> {
|
|
118
|
+
let parsed
|
|
119
|
+
try {
|
|
120
|
+
parsed = parseArgs({
|
|
121
|
+
args: argv,
|
|
122
|
+
options: {
|
|
123
|
+
'api-url': { type: 'string' },
|
|
124
|
+
bundle: { type: 'string' },
|
|
125
|
+
'dry-run': { type: 'boolean' },
|
|
126
|
+
help: { short: 'h', type: 'boolean' },
|
|
127
|
+
'hermes-map': { type: 'string' },
|
|
128
|
+
'ingest-url': { type: 'string' },
|
|
129
|
+
'metro-map': { type: 'string' },
|
|
130
|
+
release: { type: 'string' },
|
|
131
|
+
token: { type: 'string' },
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.error(`error: ${(e as Error).message}\n`)
|
|
136
|
+
console.error(HELP)
|
|
82
137
|
return 2
|
|
83
138
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
139
|
+
if (parsed.values.help) {
|
|
140
|
+
console.log(HELP)
|
|
141
|
+
return 0
|
|
142
|
+
}
|
|
143
|
+
const c = parseCommon(parsed.values)
|
|
144
|
+
if (!c) return 2
|
|
145
|
+
const metroMap = parsed.values['metro-map']
|
|
146
|
+
const hermesMap = parsed.values['hermes-map']
|
|
147
|
+
if (typeof metroMap !== 'string' || typeof hermesMap !== 'string') {
|
|
148
|
+
console.error('error: --metro-map and --hermes-map are both required')
|
|
91
149
|
return 2
|
|
92
150
|
}
|
|
93
|
-
|
|
94
151
|
try {
|
|
95
|
-
const result = await
|
|
96
|
-
apiUrl,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
152
|
+
const result = await reactNativeUpload({
|
|
153
|
+
apiUrl: c.apiUrl,
|
|
154
|
+
bundle: typeof parsed.values.bundle === 'string' ? parsed.values.bundle : undefined,
|
|
155
|
+
dryRun: c.dryRun,
|
|
156
|
+
hermesMap,
|
|
157
|
+
metroMap,
|
|
158
|
+
release: c.release,
|
|
159
|
+
token: c.token,
|
|
101
160
|
})
|
|
102
|
-
|
|
103
|
-
console.log(
|
|
104
|
-
`would upload ${result.files.length} file(s) to ${apiUrl.replace(/\/+$/, '')}/admin/api/releases/${encodeURIComponent(release)}/sourcemaps:`,
|
|
105
|
-
)
|
|
106
|
-
for (const f of result.files) console.log(` ${f}`)
|
|
107
|
-
} else {
|
|
108
|
-
console.log(`uploaded ${result.uploaded} file(s) for release "${release}"`)
|
|
109
|
-
for (const a of result.artifacts ?? []) console.log(` ${a.name} [${a.kind}]`)
|
|
110
|
-
console.log('done — minified stack traces on this release will now resolve to source.')
|
|
111
|
-
}
|
|
161
|
+
reportUpload(result, c)
|
|
112
162
|
return 0
|
|
113
163
|
} catch (e) {
|
|
114
|
-
console.error(`upload failed: ${(e as Error).message}`)
|
|
164
|
+
console.error(`react-native upload failed: ${(e as Error).message}`)
|
|
115
165
|
return 1
|
|
116
166
|
}
|
|
117
167
|
}
|
|
118
168
|
|
|
169
|
+
function reportUpload(
|
|
170
|
+
result: { files: string[]; uploaded?: number },
|
|
171
|
+
c: Common,
|
|
172
|
+
): void {
|
|
173
|
+
if (c.dryRun) {
|
|
174
|
+
console.log(
|
|
175
|
+
`would upload ${result.files.length} file(s) to ${c.apiUrl.replace(/\/+$/, '')}/admin/api/releases/${encodeURIComponent(c.release)}/sourcemaps:`,
|
|
176
|
+
)
|
|
177
|
+
for (const f of result.files) console.log(` ${f}`)
|
|
178
|
+
} else {
|
|
179
|
+
console.log(
|
|
180
|
+
`uploaded ${result.uploaded ?? result.files.length} file(s) for release "${c.release}" — minified stacks on this release will now resolve to source.`,
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function main(argv: string[]): Promise<number> {
|
|
186
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
187
|
+
console.log(HELP)
|
|
188
|
+
return 0
|
|
189
|
+
}
|
|
190
|
+
const [a, b, ...rest] = argv
|
|
191
|
+
if (a === 'upload' && b === 'sourcemap') return cmdUploadSourcemap(rest)
|
|
192
|
+
if (a === 'react-native' && b === 'upload') return cmdReactNativeUpload(rest)
|
|
193
|
+
console.error(`unknown command: ${[a, b].filter(Boolean).join(' ') || '(none)'}\n`)
|
|
194
|
+
console.error(HELP)
|
|
195
|
+
return 2
|
|
196
|
+
}
|
|
197
|
+
|
|
119
198
|
main(process.argv.slice(2)).then(
|
|
120
199
|
(code) => process.exit(code),
|
|
121
200
|
(e: unknown) => {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import { uploadSourcemaps } from './upload.js'
|
|
8
|
+
|
|
9
|
+
/** Resolve `react-native/scripts/compose-source-maps.js` from the
|
|
10
|
+
* current project's node_modules. Returns null if react-native isn't
|
|
11
|
+
* installed or the version doesn't ship that script. */
|
|
12
|
+
export function resolveComposeScript(fromDir = process.cwd()): null | string {
|
|
13
|
+
const req = createRequire(join(fromDir, 'noop.js'))
|
|
14
|
+
for (const id of [
|
|
15
|
+
'react-native/scripts/compose-source-maps.js',
|
|
16
|
+
'@react-native/community-cli-plugin/dist/utils/composeSourceMaps.js',
|
|
17
|
+
]) {
|
|
18
|
+
try {
|
|
19
|
+
const p = req.resolve(id)
|
|
20
|
+
if (existsSync(p)) return p
|
|
21
|
+
} catch {
|
|
22
|
+
// try next
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Compose a Metro packager source map + a Hermes source map into a
|
|
29
|
+
* single map (a temp file the caller is responsible for deleting).
|
|
30
|
+
* Throws if react-native's compose script can't be found or fails. */
|
|
31
|
+
export function composeSourceMaps(metroMap: string, hermesMap: string): string {
|
|
32
|
+
for (const p of [metroMap, hermesMap]) {
|
|
33
|
+
if (!existsSync(p)) throw new Error(`no such file: ${p}`)
|
|
34
|
+
}
|
|
35
|
+
const script = resolveComposeScript()
|
|
36
|
+
if (!script) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"couldn't find react-native's compose-source-maps.js — install react-native, or " +
|
|
39
|
+
'compose the maps yourself and use `sentori-cli upload sourcemap <composed.map>`',
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
const out = join(mkdtempSync(join(tmpdir(), 'sentori-rn-')), 'composed.map')
|
|
43
|
+
const r = spawnSync('node', [script, metroMap, hermesMap, '-o', out], { stdio: 'inherit' })
|
|
44
|
+
if (r.status !== 0) {
|
|
45
|
+
throw new Error(`compose-source-maps.js exited with ${r.status ?? 'signal'}`)
|
|
46
|
+
}
|
|
47
|
+
if (!existsSync(out)) throw new Error('compose-source-maps.js produced no output')
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type RnUploadOptions = {
|
|
52
|
+
apiUrl: string
|
|
53
|
+
/** Optional bundle file (.jsbundle / .bundle) to upload alongside the map. */
|
|
54
|
+
bundle?: string
|
|
55
|
+
dryRun?: boolean
|
|
56
|
+
hermesMap: string
|
|
57
|
+
metroMap: string
|
|
58
|
+
release: string
|
|
59
|
+
token: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Compose the Metro + Hermes maps, then upload the result (and the
|
|
63
|
+
* bundle, if given). Cleans up the temp composed map. */
|
|
64
|
+
export async function reactNativeUpload(opts: RnUploadOptions): Promise<{
|
|
65
|
+
files: string[]
|
|
66
|
+
uploaded?: number
|
|
67
|
+
}> {
|
|
68
|
+
const composed = composeSourceMaps(opts.metroMap, opts.hermesMap)
|
|
69
|
+
try {
|
|
70
|
+
const paths = [composed, ...(opts.bundle ? [opts.bundle] : [])]
|
|
71
|
+
const r = await uploadSourcemaps({
|
|
72
|
+
apiUrl: opts.apiUrl,
|
|
73
|
+
dryRun: opts.dryRun,
|
|
74
|
+
paths,
|
|
75
|
+
release: opts.release,
|
|
76
|
+
token: opts.token,
|
|
77
|
+
})
|
|
78
|
+
return { files: r.files, uploaded: r.uploaded }
|
|
79
|
+
} finally {
|
|
80
|
+
try {
|
|
81
|
+
rmSync(composed, { force: true })
|
|
82
|
+
rmSync(join(composed, '..'), { force: true, recursive: true })
|
|
83
|
+
} catch {
|
|
84
|
+
// best-effort cleanup
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|