@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 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
- <path> one or more files or directories. A directory is scanned
10
- (one level) for *.map and *.js / *.bundle / *.hbc files.
11
- A file given explicitly is uploaded as-is.
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, your
20
- host. (Accepts --ingest-url as an alias.)
21
- --dry-run list what would be uploaded; don't upload.
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
- React Native / Hermes: in a release build the *minified* map (Metro)
25
- and the *bytecode* map (Hermes) must be composed first —
26
- npx react-native bundle --platform ios --dev false \\
27
- --entry-file index.js --bundle-output main.jsbundle \\
28
- --sourcemap-output main.jsbundle.packager.map
29
- node node_modules/react-native/scripts/compose-source-maps.js \\
30
- main.jsbundle.packager.map main.jsbundle.hbc.map -o main.jsbundle.map
31
- then \`sentori-cli upload sourcemap --release "<app>@<version>+<build>" main.jsbundle.map main.jsbundle\`.
32
- (Web bundlers emit a usable .map directly just point at the build dir.)
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
- async function main(argv) {
35
- if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
36
- console.log(HELP);
37
- return 0;
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
- if (argv[0] !== 'upload' || argv[1] !== 'sourcemap') {
40
- console.error(`unknown command: ${argv.slice(0, 2).join(' ') || '(none)'}\n`);
41
- console.error(HELP);
42
- return 2;
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.slice(2),
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' }, // alias, kept for older docs
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
- const { positionals, values } = parsed;
65
- if (values.help) {
84
+ if (parsed.values.help) {
66
85
  console.log(HELP);
67
86
  return 0;
68
87
  }
69
- const release = values.release;
70
- if (!release) {
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
- if (dryRun) {
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BZ,CAAA;AAED,KAAK,UAAU,IAAI,CAAC,IAAc;IAChC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;QAClE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACjB,OAAO,CAAC,CAAA;IACV,CAAC;IACD,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,WAAW,EAAE,CAAC;QACpD,OAAO,CAAC,KAAK,CAAC,oBAAoB,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,IAAI,CAAC,CAAA;QAC7E,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACnB,OAAO,CAAC,CAAA;IACV,CAAC;IAED,IAAI,MAAM,CAAA;IACV,IAAI,CAAC;QACH,MAAM,GAAG,SAAS,CAAC;YACjB,gBAAgB,EAAE,IAAI;YACtB,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;YACnB,OAAO,EAAE;gBACP,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC7B,SAAS,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;gBAC9B,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE;gBACrC,YAAY,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,6BAA6B;gBAC/D,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC3B,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;aAC1B;SACF,CAAC,CAAA;IACJ,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,UAAW,CAAW,CAAC,OAAO,IAAI,CAAC,CAAA;QACjD,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACnB,OAAO,CAAC,CAAA;IACV,CAAC;IACD,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CAAA;IAEtC,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAChB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACjB,OAAO,CAAC,CAAA;IACV,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;IAC9B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,uEAAuE,CAAC,CAAA;QACtF,OAAO,CAAC,CAAA;IACV,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IAClC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,CAAA;IACvD,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;QACtB,OAAO,CAAC,KAAK,CAAC,gDAAgD,CAAC,CAAA;QAC/D,OAAO,CAAC,CAAA;IACV,CAAC;IACD,MAAM,MAAM,GACV,MAAM,CAAC,SAAS,CAAC;QACjB,MAAM,CAAC,YAAY,CAAC;QACpB,OAAO,CAAC,GAAG,CAAC,eAAe;QAC3B,8BAA8B,CAAA;IAChC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,KAAK,CAAC,0DAA0D,CAAC,CAAA;QACzE,OAAO,CAAC,CAAA;IACV,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC;YACpC,MAAM;YACN,MAAM;YACN,KAAK,EAAE,WAAW;YAClB,OAAO;YACP,KAAK,EAAE,KAAK,IAAI,EAAE;SACnB,CAAC,CAAA;QACF,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,CAAC,GAAG,CACT,gBAAgB,MAAM,CAAC,KAAK,CAAC,MAAM,eAAe,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,uBAAuB,kBAAkB,CAAC,OAAO,CAAC,cAAc,CAC7I,CAAA;YACD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,KAAK;gBAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QACrD,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,YAAY,MAAM,CAAC,QAAQ,yBAAyB,OAAO,GAAG,CAAC,CAAA;YAC3E,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,SAAS,IAAI,EAAE;gBAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,CAAA;YAC/E,OAAO,CAAC,GAAG,CAAC,0EAA0E,CAAC,CAAA;QACzF,CAAC;QACD,OAAO,CAAC,CAAA;IACV,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,kBAAmB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAA;QACvD,OAAO,CAAC,CAAA;IACV,CAAC;AACH,CAAC;AAED,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAC9B,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAC5B,CAAC,CAAU,EAAE,EAAE;IACb,OAAO,CAAC,KAAK,CAAC,UAAW,CAAW,CAAC,OAAO,EAAE,CAAC,CAAA;IAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CACF,CAAA"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goliapkg/sentori-cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Sentori CLI \u2014 upload source maps so dashboard stack traces resolve to your original source.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://sentori.golia.jp",
@@ -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
- <path> one or more files or directories. A directory is scanned
12
- (one level) for *.map and *.js / *.bundle / *.hbc files.
13
- A file given explicitly is uploaded as-is.
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, your
22
- host. (Accepts --ingest-url as an alias.)
23
- --dry-run list what would be uploaded; don't upload.
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
- React Native / Hermes: in a release build the *minified* map (Metro)
27
- and the *bytecode* map (Hermes) must be composed first —
28
- npx react-native bundle --platform ios --dev false \\
29
- --entry-file index.js --bundle-output main.jsbundle \\
30
- --sourcemap-output main.jsbundle.packager.map
31
- node node_modules/react-native/scripts/compose-source-maps.js \\
32
- main.jsbundle.packager.map main.jsbundle.hbc.map -o main.jsbundle.map
33
- then \`sentori-cli upload sourcemap --release "<app>@<version>+<build>" main.jsbundle.map main.jsbundle\`.
34
- (Web bundlers emit a usable .map directly just point at the build dir.)
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
- async function main(argv: string[]): Promise<number> {
38
- if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
39
- console.log(HELP)
40
- return 0
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
- if (argv[0] !== 'upload' || argv[1] !== 'sourcemap') {
43
- console.error(`unknown command: ${argv.slice(0, 2).join(' ') || '(none)'}\n`)
44
- console.error(HELP)
45
- return 2
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.slice(2),
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' }, // alias, kept for older docs
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
- const { positionals, values } = parsed
68
-
69
- if (values.help) {
91
+ if (parsed.values.help) {
70
92
  console.log(HELP)
71
93
  return 0
72
94
  }
73
- const release = values.release
74
- if (!release) {
75
- console.error('error: --release is required (must match the SDK’s init({ release }))')
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
- const dryRun = !!values['dry-run']
79
- const token = values.token ?? process.env.SENTORI_TOKEN
80
- if (!token && !dryRun) {
81
- console.error('error: --token (or $SENTORI_TOKEN) is required')
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
- const apiUrl =
85
- values['api-url'] ??
86
- values['ingest-url'] ??
87
- process.env.SENTORI_API_URL ??
88
- 'https://api.sentori.golia.jp'
89
- if (positionals.length === 0) {
90
- console.error('error: at least one path (file or directory) is required')
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 uploadSourcemaps({
96
- apiUrl,
97
- dryRun,
98
- paths: positionals,
99
- release,
100
- token: token ?? '',
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
- if (dryRun) {
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
+ }