@goliapkg/sentori-expo 0.1.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 +111 -0
- package/app.plugin.js +39 -0
- package/lib/index.d.ts +35 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +64 -0
- package/lib/index.js.map +1 -0
- package/lib/types.d.ts +27 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/package.json +67 -0
- package/scripts/eas-post-build.mjs +103 -0
- package/src/__tests__/derive-release.test.ts +37 -0
- package/src/index.ts +75 -0
- package/src/types.ts +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# @goliapkg/sentori-expo
|
|
2
|
+
|
|
3
|
+
Expo adapter for Sentori — Config Plugin marker, runtime init helper
|
|
4
|
+
that reads `expo-application`, and an EAS post-build hook for source
|
|
5
|
+
map uploads. Built on `@goliapkg/sentori-react-native@>=0.2.0`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bunx expo install @goliapkg/sentori-expo @goliapkg/sentori-react-native expo-application
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Wire it up
|
|
14
|
+
|
|
15
|
+
### 1. app.json
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"expo": {
|
|
20
|
+
"plugins": ["@goliapkg/sentori-expo"]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The plugin is currently a marker — `@goliapkg/sentori-react-native`
|
|
26
|
+
ships its own `expo-module.config.json`, podspec, and Android gradle,
|
|
27
|
+
so Expo Modules autolinking handles the native side. The plugin entry
|
|
28
|
+
gives us a stable extension point for future native config (SDK
|
|
29
|
+
version banner, opt-in crash-handler tuning, etc.) without changing
|
|
30
|
+
your `app.json`.
|
|
31
|
+
|
|
32
|
+
### 2. App.tsx
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import * as Application from 'expo-application'
|
|
36
|
+
import { initSentoriExpo } from '@goliapkg/sentori-expo'
|
|
37
|
+
|
|
38
|
+
initSentoriExpo({
|
|
39
|
+
application: Application,
|
|
40
|
+
token: process.env.EXPO_PUBLIC_SENTORI_TOKEN!,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export default function App() { /* ... */ }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`initSentoriExpo`:
|
|
47
|
+
- Derives the release string `applicationId@version+build` from
|
|
48
|
+
`expo-application`.
|
|
49
|
+
- Defaults the environment to `dev`/`prod` via the RN `__DEV__` flag.
|
|
50
|
+
- Defaults the ingest URL to the public SaaS endpoint.
|
|
51
|
+
|
|
52
|
+
You can override any of those — see `InitOptions`.
|
|
53
|
+
|
|
54
|
+
If you're not on Expo's managed workflow, omit `application` and pass
|
|
55
|
+
`release` explicitly:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
initSentoriExpo({
|
|
59
|
+
release: 'myapp@1.2.3+42',
|
|
60
|
+
token: process.env.EXPO_PUBLIC_SENTORI_TOKEN!,
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. EAS source map upload (optional, recommended)
|
|
65
|
+
|
|
66
|
+
Add to `eas.json`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"build": {
|
|
71
|
+
"production": {
|
|
72
|
+
"hooks": {
|
|
73
|
+
"postPublish": [
|
|
74
|
+
{
|
|
75
|
+
"config": "@goliapkg/sentori-expo/eas-post-build",
|
|
76
|
+
"options": {
|
|
77
|
+
"release": "$EAS_BUILD_RELEASE"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The hook shells out to `@goliapkg/sentori-cli upload sourcemap` — install
|
|
88
|
+
it as a dev dep:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
bun add -D @goliapkg/sentori-cli
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
> Status: Phase 22 sub-A lands the actual `upload sourcemap` and
|
|
95
|
+
> `upload dsym` CLI subcommands. Until then the hook logs a warning
|
|
96
|
+
> and exits 0 — adopt the wiring now and the upload works
|
|
97
|
+
> transparently when you next bump `@goliapkg/sentori-cli`.
|
|
98
|
+
|
|
99
|
+
## What `initSentoriExpo` does under the hood
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
@goliapkg/sentori-expo
|
|
103
|
+
└── reads expo-application metadata
|
|
104
|
+
└── calls @goliapkg/sentori-react-native init({ token, release, ... })
|
|
105
|
+
└── starts the JS-layer global error / promise / network hooks
|
|
106
|
+
└── primes the native iOS / Android crash handlers
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The full feature list (breadcrumbs, capture, network instrumentation,
|
|
110
|
+
native crash capture) lives in `@goliapkg/sentori-react-native` —
|
|
111
|
+
`-expo` only adds the auto-config + EAS plumbing.
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentori Expo Config Plugin.
|
|
3
|
+
*
|
|
4
|
+
* `@goliapkg/sentori-react-native` already exposes
|
|
5
|
+
* `expo-module.config.json` + iOS podspec + Android build.gradle, so
|
|
6
|
+
* Expo Modules autolinking handles the native side without any
|
|
7
|
+
* additional config-plugins work. The Config Plugin entry exists
|
|
8
|
+
* mainly as a marker so users can drop:
|
|
9
|
+
*
|
|
10
|
+
* {
|
|
11
|
+
* "expo": {
|
|
12
|
+
* "plugins": ["@goliapkg/sentori-expo"]
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* into their app.json without breaking the build, and so we can
|
|
17
|
+
* extend it later (e.g. SDK-version metadata in Info.plist /
|
|
18
|
+
* AndroidManifest, native crash-handler opt-ins) without changing
|
|
19
|
+
* the user-facing wiring.
|
|
20
|
+
*
|
|
21
|
+
* The plugin is intentionally CommonJS — Expo's plugin loader uses
|
|
22
|
+
* `require()`.
|
|
23
|
+
*/
|
|
24
|
+
const { withInfoPlist } = require('@expo/config-plugins')
|
|
25
|
+
|
|
26
|
+
const SENTORI_VERSION_KEY = 'SentoriSdkVersion'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {import('@expo/config-plugins').ExpoConfig} config
|
|
30
|
+
* @param {{ sdkVersion?: string }} [props]
|
|
31
|
+
*/
|
|
32
|
+
const withSentori = (config, props = {}) => {
|
|
33
|
+
return withInfoPlist(config, (cfg) => {
|
|
34
|
+
cfg.modResults[SENTORI_VERSION_KEY] = props.sdkVersion || '0.1.0'
|
|
35
|
+
return cfg
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = withSentori
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ExpoApplicationLike, InitOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Drop-in init for Expo apps. Reads bundleId / version / build from
|
|
4
|
+
* `expo-application` (which is shipped in every Expo SDK) so the
|
|
5
|
+
* caller only has to supply the token. Falls back to manual config
|
|
6
|
+
* fields when expo-application isn't installed (bare RN apps), in
|
|
7
|
+
* which case the caller MUST pass `release`.
|
|
8
|
+
*
|
|
9
|
+
* // App.tsx
|
|
10
|
+
* import { initSentoriExpo } from '@goliapkg/sentori-expo'
|
|
11
|
+
* import * as Application from 'expo-application'
|
|
12
|
+
*
|
|
13
|
+
* initSentoriExpo({
|
|
14
|
+
* application: Application,
|
|
15
|
+
* token: process.env.EXPO_PUBLIC_SENTORI_TOKEN!,
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* Why we ask the caller to import `expo-application` and pass it in,
|
|
19
|
+
* instead of `import * as Application from 'expo-application'` here?
|
|
20
|
+
* Bundlers (Metro / Hermes) statically include every import; if this
|
|
21
|
+
* package imported expo-application directly, every consumer would
|
|
22
|
+
* be forced to install it even when running in a bare-RN context.
|
|
23
|
+
*/
|
|
24
|
+
export declare function initSentoriExpo(options: InitOptions): void;
|
|
25
|
+
/**
|
|
26
|
+
* Build a `slug@version+build` release string from expo-application.
|
|
27
|
+
* Returns `undefined` when the module isn't available so the caller
|
|
28
|
+
* can fall back to a manually-supplied release.
|
|
29
|
+
*
|
|
30
|
+
* Exported for callers who want to use the same string outside of
|
|
31
|
+
* init (e.g. as a tag, log prefix, or metric label).
|
|
32
|
+
*/
|
|
33
|
+
export declare function deriveRelease(app: ExpoApplicationLike | undefined): string | undefined;
|
|
34
|
+
export type { ExpoApplicationLike, InitOptions } from './types.js';
|
|
35
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAElE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAe1D;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,mBAAmB,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOtF;AAWD,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Phase 21 sub-D: Expo runtime helpers.
|
|
2
|
+
//
|
|
3
|
+
// The Config Plugin (app.plugin.js) is loaded by Expo at prebuild
|
|
4
|
+
// time; this module is what apps import from JS at runtime.
|
|
5
|
+
import { init as initSentoriRN } from '@goliapkg/sentori-react-native';
|
|
6
|
+
/**
|
|
7
|
+
* Drop-in init for Expo apps. Reads bundleId / version / build from
|
|
8
|
+
* `expo-application` (which is shipped in every Expo SDK) so the
|
|
9
|
+
* caller only has to supply the token. Falls back to manual config
|
|
10
|
+
* fields when expo-application isn't installed (bare RN apps), in
|
|
11
|
+
* which case the caller MUST pass `release`.
|
|
12
|
+
*
|
|
13
|
+
* // App.tsx
|
|
14
|
+
* import { initSentoriExpo } from '@goliapkg/sentori-expo'
|
|
15
|
+
* import * as Application from 'expo-application'
|
|
16
|
+
*
|
|
17
|
+
* initSentoriExpo({
|
|
18
|
+
* application: Application,
|
|
19
|
+
* token: process.env.EXPO_PUBLIC_SENTORI_TOKEN!,
|
|
20
|
+
* })
|
|
21
|
+
*
|
|
22
|
+
* Why we ask the caller to import `expo-application` and pass it in,
|
|
23
|
+
* instead of `import * as Application from 'expo-application'` here?
|
|
24
|
+
* Bundlers (Metro / Hermes) statically include every import; if this
|
|
25
|
+
* package imported expo-application directly, every consumer would
|
|
26
|
+
* be forced to install it even when running in a bare-RN context.
|
|
27
|
+
*/
|
|
28
|
+
export function initSentoriExpo(options) {
|
|
29
|
+
const release = options.release ?? deriveRelease(options.application);
|
|
30
|
+
if (!release) {
|
|
31
|
+
throw new Error('[sentori-expo] could not derive release. ' +
|
|
32
|
+
'Either pass `release` explicitly, or pass `application: Application` ' +
|
|
33
|
+
'from `import * as Application from "expo-application"`.');
|
|
34
|
+
}
|
|
35
|
+
initSentoriRN({
|
|
36
|
+
environment: options.environment ?? (isDev() ? 'dev' : 'prod'),
|
|
37
|
+
ingestUrl: options.ingestUrl ?? 'https://ingest.sentori.golia.jp',
|
|
38
|
+
release,
|
|
39
|
+
token: options.token,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build a `slug@version+build` release string from expo-application.
|
|
44
|
+
* Returns `undefined` when the module isn't available so the caller
|
|
45
|
+
* can fall back to a manually-supplied release.
|
|
46
|
+
*
|
|
47
|
+
* Exported for callers who want to use the same string outside of
|
|
48
|
+
* init (e.g. as a tag, log prefix, or metric label).
|
|
49
|
+
*/
|
|
50
|
+
export function deriveRelease(app) {
|
|
51
|
+
if (!app)
|
|
52
|
+
return undefined;
|
|
53
|
+
const id = app.applicationId ?? app.nativeApplicationVersion ?? 'app';
|
|
54
|
+
const version = app.nativeApplicationVersion ?? '0.0.0';
|
|
55
|
+
const build = app.nativeBuildVersion ?? '0';
|
|
56
|
+
return `${id}@${version}+${build}`;
|
|
57
|
+
}
|
|
58
|
+
function isDev() {
|
|
59
|
+
// RN's __DEV__ is true under Metro dev server; bare false in
|
|
60
|
+
// Hermes release builds. typeof check keeps this safe to import in
|
|
61
|
+
// Node tests where __DEV__ doesn't exist.
|
|
62
|
+
return typeof __DEV__ !== 'undefined' && __DEV__;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,EAAE;AACF,kEAAkE;AAClE,4DAA4D;AAE5D,OAAO,EAAE,IAAI,IAAI,aAAa,EAAE,MAAM,gCAAgC,CAAA;AAItE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,eAAe,CAAC,OAAoB;IAClD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,aAAa,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IACrE,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,2CAA2C;YACzC,uEAAuE;YACvE,yDAAyD,CAC5D,CAAA;IACH,CAAC;IACD,aAAa,CAAC;QACZ,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;QAC9D,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,iCAAiC;QACjE,OAAO;QACP,KAAK,EAAE,OAAO,CAAC,KAAK;KACrB,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,GAAoC;IAChE,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAA;IAC1B,MAAM,EAAE,GACN,GAAG,CAAC,aAAa,IAAI,GAAG,CAAC,wBAAwB,IAAI,KAAK,CAAA;IAC5D,MAAM,OAAO,GAAG,GAAG,CAAC,wBAAwB,IAAI,OAAO,CAAA;IACvD,MAAM,KAAK,GAAG,GAAG,CAAC,kBAAkB,IAAI,GAAG,CAAA;IAC3C,OAAO,GAAG,EAAE,IAAI,OAAO,IAAI,KAAK,EAAE,CAAA;AACpC,CAAC;AAED,SAAS,KAAK;IACZ,6DAA6D;IAC7D,mEAAmE;IACnE,0CAA0C;IAC1C,OAAO,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAA;AAClD,CAAC"}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subset of the `expo-application` module we read for release
|
|
3
|
+
* derivation. Defining a structural type instead of importing the
|
|
4
|
+
* module keeps `expo-application` a peer dep that the Expo runtime
|
|
5
|
+
* provides — bare-RN consumers don't need to install it.
|
|
6
|
+
*/
|
|
7
|
+
export type ExpoApplicationLike = {
|
|
8
|
+
/** e.g. "com.example.myapp" — Android applicationId / iOS bundleId. */
|
|
9
|
+
applicationId?: null | string;
|
|
10
|
+
/** e.g. "5" — Android versionCode / iOS CFBundleVersion. */
|
|
11
|
+
nativeBuildVersion?: null | string;
|
|
12
|
+
/** e.g. "1.2.3" — iOS CFBundleShortVersionString / Android versionName. */
|
|
13
|
+
nativeApplicationVersion?: null | string;
|
|
14
|
+
};
|
|
15
|
+
export type InitOptions = {
|
|
16
|
+
/** Pass `import * as Application from 'expo-application'` here. */
|
|
17
|
+
application?: ExpoApplicationLike;
|
|
18
|
+
/** Override the auto-derived environment. Defaults to dev/prod via __DEV__. */
|
|
19
|
+
environment?: string;
|
|
20
|
+
/** Override ingest URL (self-hosted). Defaults to public SaaS. */
|
|
21
|
+
ingestUrl?: string;
|
|
22
|
+
/** Manual release override — required when `application` is omitted. */
|
|
23
|
+
release?: string;
|
|
24
|
+
/** Project public token, format `st_pk_...`. */
|
|
25
|
+
token: string;
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,uEAAuE;IACvE,aAAa,CAAC,EAAE,IAAI,GAAG,MAAM,CAAA;IAC7B,4DAA4D;IAC5D,kBAAkB,CAAC,EAAE,IAAI,GAAG,MAAM,CAAA;IAClC,2EAA2E;IAC3E,wBAAwB,CAAC,EAAE,IAAI,GAAG,MAAM,CAAA;CACzC,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,mEAAmE;IACnE,WAAW,CAAC,EAAE,mBAAmB,CAAA;IACjC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAA;CACd,CAAA"}
|
package/lib/types.js
ADDED
package/lib/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goliapkg/sentori-expo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Expo adapter for Sentori — Config Plugin marker, expo-application auto-config, EAS post-build helper. Built on @goliapkg/sentori-react-native.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://sentori.golia.jp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/goliajp/sentori.git",
|
|
10
|
+
"directory": "sdk/expo"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/goliajp/sentori/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"sentori",
|
|
17
|
+
"error-tracking",
|
|
18
|
+
"expo",
|
|
19
|
+
"react-native",
|
|
20
|
+
"config-plugin"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./lib/index.js",
|
|
24
|
+
"types": "./lib/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./lib/index.d.ts",
|
|
28
|
+
"default": "./lib/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./app.plugin.js": "./app.plugin.js",
|
|
31
|
+
"./eas-post-build": "./scripts/eas-post-build.mjs"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"lib/",
|
|
35
|
+
"src/",
|
|
36
|
+
"app.plugin.js",
|
|
37
|
+
"scripts/",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.json",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "bun test",
|
|
44
|
+
"prepack": "bun run build"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@goliapkg/sentori-react-native": ">=0.2.0",
|
|
48
|
+
"expo": ">=50",
|
|
49
|
+
"expo-application": ">=5",
|
|
50
|
+
"react-native": ">=0.74"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"expo-application": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@expo/config-plugins": "^9 || ^10"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/bun": "latest",
|
|
62
|
+
"typescript": "^5"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* EAS post-build hook for Sentori source map upload.
|
|
4
|
+
*
|
|
5
|
+
* Wire it from app.json / eas.json:
|
|
6
|
+
*
|
|
7
|
+
* {
|
|
8
|
+
* "build": {
|
|
9
|
+
* "production": {
|
|
10
|
+
* "ios": { "buildArtifactPaths": ["ios/build/**\/*.dSYM"] },
|
|
11
|
+
* "hooks": {
|
|
12
|
+
* "postPublish": [
|
|
13
|
+
* {
|
|
14
|
+
* "config": "@goliapkg/sentori-expo/eas-post-build",
|
|
15
|
+
* "options": { "release": "myapp@1.2.3+42" }
|
|
16
|
+
* }
|
|
17
|
+
* ]
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Or call this script directly from a custom build hook:
|
|
24
|
+
*
|
|
25
|
+
* #!/bin/sh
|
|
26
|
+
* node ./node_modules/@goliapkg/sentori-expo/scripts/eas-post-build.mjs \
|
|
27
|
+
* --token $SENTORI_ADMIN_TOKEN --release "$EAS_BUILD_RELEASE"
|
|
28
|
+
*
|
|
29
|
+
* The script shells out to `sentori-cli` for the actual upload (Phase 22
|
|
30
|
+
* sub-A introduces `sentori-cli upload dsym`). Until that lands this is
|
|
31
|
+
* a stub that logs what it would have done — adopt sub-D in your
|
|
32
|
+
* pipeline now and the CLI integration arrives transparently.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { spawnSync } from 'node:child_process'
|
|
36
|
+
import { existsSync } from 'node:fs'
|
|
37
|
+
|
|
38
|
+
const args = parseArgs(process.argv.slice(2))
|
|
39
|
+
|
|
40
|
+
const token = args.token ?? process.env.SENTORI_ADMIN_TOKEN
|
|
41
|
+
const release = args.release ?? process.env.EAS_BUILD_RELEASE
|
|
42
|
+
const ingestUrl = args.ingest ?? process.env.SENTORI_INGEST_URL
|
|
43
|
+
|
|
44
|
+
if (!token || !release) {
|
|
45
|
+
console.error(
|
|
46
|
+
'[sentori-expo:eas-post-build] missing --token or --release ' +
|
|
47
|
+
'(env: SENTORI_ADMIN_TOKEN, EAS_BUILD_RELEASE)',
|
|
48
|
+
)
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const cli = resolveCli()
|
|
53
|
+
if (!cli) {
|
|
54
|
+
console.warn(
|
|
55
|
+
'[sentori-expo:eas-post-build] sentori-cli not found on PATH or in node_modules. ' +
|
|
56
|
+
'Skipping upload — install @goliapkg/sentori-cli to enable. ' +
|
|
57
|
+
'Phase 22 sub-A will land the proper upload subcommand.',
|
|
58
|
+
)
|
|
59
|
+
process.exit(0)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const cmd = [
|
|
63
|
+
'upload',
|
|
64
|
+
'sourcemap',
|
|
65
|
+
'--token',
|
|
66
|
+
token,
|
|
67
|
+
'--release',
|
|
68
|
+
release,
|
|
69
|
+
...(ingestUrl ? ['--ingest', ingestUrl] : []),
|
|
70
|
+
// Default Expo build output for the JS bundle + sourcemap.
|
|
71
|
+
'./dist',
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
console.log(`[sentori-expo:eas-post-build] running: ${cli} ${cmd.join(' ')}`)
|
|
75
|
+
const r = spawnSync(cli, cmd, { stdio: 'inherit' })
|
|
76
|
+
process.exit(r.status ?? 0)
|
|
77
|
+
|
|
78
|
+
function parseArgs(argv) {
|
|
79
|
+
const out = {}
|
|
80
|
+
for (let i = 0; i < argv.length; i++) {
|
|
81
|
+
const a = argv[i]
|
|
82
|
+
if (a.startsWith('--')) {
|
|
83
|
+
out[a.slice(2)] = argv[i + 1]
|
|
84
|
+
i++
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveCli() {
|
|
91
|
+
// Prefer node_modules/.bin so the locked @goliapkg/sentori-cli wins
|
|
92
|
+
// over a globally-installed older copy.
|
|
93
|
+
for (const p of [
|
|
94
|
+
'./node_modules/.bin/sentori-cli',
|
|
95
|
+
'./node_modules/@goliapkg/sentori-cli/bin/sentori-cli.js',
|
|
96
|
+
]) {
|
|
97
|
+
if (existsSync(p)) return p
|
|
98
|
+
}
|
|
99
|
+
// PATH lookup as last resort.
|
|
100
|
+
const which = spawnSync('which', ['sentori-cli'])
|
|
101
|
+
const found = which.stdout?.toString().trim()
|
|
102
|
+
return found || null
|
|
103
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { deriveRelease } from '../index.js'
|
|
4
|
+
|
|
5
|
+
describe('deriveRelease', () => {
|
|
6
|
+
test('builds slug@version+build from expo-application fields', () => {
|
|
7
|
+
expect(
|
|
8
|
+
deriveRelease({
|
|
9
|
+
applicationId: 'com.example.myapp',
|
|
10
|
+
nativeApplicationVersion: '1.2.3',
|
|
11
|
+
nativeBuildVersion: '42',
|
|
12
|
+
}),
|
|
13
|
+
).toBe('com.example.myapp@1.2.3+42')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('substitutes defaults for missing fields', () => {
|
|
17
|
+
expect(
|
|
18
|
+
deriveRelease({
|
|
19
|
+
applicationId: 'com.example.myapp',
|
|
20
|
+
}),
|
|
21
|
+
).toBe('com.example.myapp@0.0.0+0')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('returns undefined when module is missing', () => {
|
|
25
|
+
expect(deriveRelease(undefined)).toBeUndefined()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('handles null fields gracefully (Expo bare workflow)', () => {
|
|
29
|
+
expect(
|
|
30
|
+
deriveRelease({
|
|
31
|
+
applicationId: null,
|
|
32
|
+
nativeApplicationVersion: null,
|
|
33
|
+
nativeBuildVersion: null,
|
|
34
|
+
}),
|
|
35
|
+
).toBe('app@0.0.0+0')
|
|
36
|
+
})
|
|
37
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Phase 21 sub-D: Expo runtime helpers.
|
|
2
|
+
//
|
|
3
|
+
// The Config Plugin (app.plugin.js) is loaded by Expo at prebuild
|
|
4
|
+
// time; this module is what apps import from JS at runtime.
|
|
5
|
+
|
|
6
|
+
import { init as initSentoriRN } from '@goliapkg/sentori-react-native'
|
|
7
|
+
|
|
8
|
+
import type { ExpoApplicationLike, InitOptions } from './types.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Drop-in init for Expo apps. Reads bundleId / version / build from
|
|
12
|
+
* `expo-application` (which is shipped in every Expo SDK) so the
|
|
13
|
+
* caller only has to supply the token. Falls back to manual config
|
|
14
|
+
* fields when expo-application isn't installed (bare RN apps), in
|
|
15
|
+
* which case the caller MUST pass `release`.
|
|
16
|
+
*
|
|
17
|
+
* // App.tsx
|
|
18
|
+
* import { initSentoriExpo } from '@goliapkg/sentori-expo'
|
|
19
|
+
* import * as Application from 'expo-application'
|
|
20
|
+
*
|
|
21
|
+
* initSentoriExpo({
|
|
22
|
+
* application: Application,
|
|
23
|
+
* token: process.env.EXPO_PUBLIC_SENTORI_TOKEN!,
|
|
24
|
+
* })
|
|
25
|
+
*
|
|
26
|
+
* Why we ask the caller to import `expo-application` and pass it in,
|
|
27
|
+
* instead of `import * as Application from 'expo-application'` here?
|
|
28
|
+
* Bundlers (Metro / Hermes) statically include every import; if this
|
|
29
|
+
* package imported expo-application directly, every consumer would
|
|
30
|
+
* be forced to install it even when running in a bare-RN context.
|
|
31
|
+
*/
|
|
32
|
+
export function initSentoriExpo(options: InitOptions): void {
|
|
33
|
+
const release = options.release ?? deriveRelease(options.application)
|
|
34
|
+
if (!release) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'[sentori-expo] could not derive release. ' +
|
|
37
|
+
'Either pass `release` explicitly, or pass `application: Application` ' +
|
|
38
|
+
'from `import * as Application from "expo-application"`.',
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
initSentoriRN({
|
|
42
|
+
environment: options.environment ?? (isDev() ? 'dev' : 'prod'),
|
|
43
|
+
ingestUrl: options.ingestUrl ?? 'https://ingest.sentori.golia.jp',
|
|
44
|
+
release,
|
|
45
|
+
token: options.token,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build a `slug@version+build` release string from expo-application.
|
|
51
|
+
* Returns `undefined` when the module isn't available so the caller
|
|
52
|
+
* can fall back to a manually-supplied release.
|
|
53
|
+
*
|
|
54
|
+
* Exported for callers who want to use the same string outside of
|
|
55
|
+
* init (e.g. as a tag, log prefix, or metric label).
|
|
56
|
+
*/
|
|
57
|
+
export function deriveRelease(app: ExpoApplicationLike | undefined): string | undefined {
|
|
58
|
+
if (!app) return undefined
|
|
59
|
+
const id =
|
|
60
|
+
app.applicationId ?? app.nativeApplicationVersion ?? 'app'
|
|
61
|
+
const version = app.nativeApplicationVersion ?? '0.0.0'
|
|
62
|
+
const build = app.nativeBuildVersion ?? '0'
|
|
63
|
+
return `${id}@${version}+${build}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isDev(): boolean {
|
|
67
|
+
// RN's __DEV__ is true under Metro dev server; bare false in
|
|
68
|
+
// Hermes release builds. typeof check keeps this safe to import in
|
|
69
|
+
// Node tests where __DEV__ doesn't exist.
|
|
70
|
+
return typeof __DEV__ !== 'undefined' && __DEV__
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
declare const __DEV__: boolean
|
|
74
|
+
|
|
75
|
+
export type { ExpoApplicationLike, InitOptions } from './types.js'
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subset of the `expo-application` module we read for release
|
|
3
|
+
* derivation. Defining a structural type instead of importing the
|
|
4
|
+
* module keeps `expo-application` a peer dep that the Expo runtime
|
|
5
|
+
* provides — bare-RN consumers don't need to install it.
|
|
6
|
+
*/
|
|
7
|
+
export type ExpoApplicationLike = {
|
|
8
|
+
/** e.g. "com.example.myapp" — Android applicationId / iOS bundleId. */
|
|
9
|
+
applicationId?: null | string
|
|
10
|
+
/** e.g. "5" — Android versionCode / iOS CFBundleVersion. */
|
|
11
|
+
nativeBuildVersion?: null | string
|
|
12
|
+
/** e.g. "1.2.3" — iOS CFBundleShortVersionString / Android versionName. */
|
|
13
|
+
nativeApplicationVersion?: null | string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type InitOptions = {
|
|
17
|
+
/** Pass `import * as Application from 'expo-application'` here. */
|
|
18
|
+
application?: ExpoApplicationLike
|
|
19
|
+
/** Override the auto-derived environment. Defaults to dev/prod via __DEV__. */
|
|
20
|
+
environment?: string
|
|
21
|
+
/** Override ingest URL (self-hosted). Defaults to public SaaS. */
|
|
22
|
+
ingestUrl?: string
|
|
23
|
+
/** Manual release override — required when `application` is omitted. */
|
|
24
|
+
release?: string
|
|
25
|
+
/** Project public token, format `st_pk_...`. */
|
|
26
|
+
token: string
|
|
27
|
+
}
|