zipy-mobile-cli 1.0.0 → 1.0.2

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 CHANGED
@@ -1,10 +1,14 @@
1
1
  # zipy-mobile-cli
2
2
 
3
+ <<<<<<< HEAD
4
+ CLI to integrate [Zipy](https://zipy.ai) with mobile apps. When you build your app, it uploads Android sourcemaps/bundles and iOS dSYMs so Zipy can symbolicate errors.
5
+ =======
3
6
  CLI to integrate [Zipy](https://zipy.ai) with mobile apps. When you build your app (e.g. React Native Android), it uploads sourcemaps so Zipy can symbolicate errors.
7
+ >>>>>>> origin/main
4
8
 
5
9
  ## Install
6
10
 
7
- **Recommended (for Gradle apply-from, like Sentry):** add as a devDependency in your React Native project so `require.resolve('zipy-mobile-cli/package.json')` finds it when Gradle runs:
11
+ **Recommended (for Gradle apply-from):** add as a devDependency in your React Native project so `require.resolve('zipy-mobile-cli/package.json')` finds it when Gradle runs:
8
12
 
9
13
  ```bash
10
14
  npm install zipy-mobile-cli --save-dev
@@ -42,9 +46,10 @@ This will:
42
46
  - Release: `android/app/build/generated/sourcemaps/react/release/index.android.bundle.map`
43
47
  - Debug: `android/app/build/generated/sourcemaps/react/debug/index.android.bundle.map`
44
48
  - Create a config file **`.zipy-mobile.json`** in the project root with those paths.
45
- - **Add an `apply from:` line to `android/app/build.gradle`** (Sentry-style) that applies `zipy.gradle`. That script hooks into **`react/release`** (the task that bundles JS and generates the source map). When that task finishes, it runs `npx zipy-mobile-cli upload-sourcemaps release`; the paths in `.zipy-mobile.json` match this task’s outputs. Debug is not hooked.
49
+ - **Add an `apply from:` line to `android/app/build.gradle`** that applies `zipy.gradle`. That script hooks into **`react/release`** (the task that bundles JS and generates the source map). When that task finishes, it runs `npx zipy-mobile-cli upload-sourcemaps release`; the paths in `.zipy-mobile.json` match this task’s outputs. Debug is not hooked.
50
+ - **Add an iOS Run Script Build Phase** named `ZipySourcemapsUpload` that detects the generated `.dSYM` from Xcode build variables and uploads it automatically on each build/run.
46
51
 
47
- So whenever a new build is made, sourcemaps are uploaded automatically; you don’t need to run a separate command.
52
+ So whenever a new build is made, Android sourcemaps or iOS dSYMs are uploaded automatically; you don’t need to run a separate command.
48
53
 
49
54
  ### 2. Configure upload (from init)
50
55
 
@@ -96,11 +101,14 @@ Example (created by init; API key and customer ID are set at init):
96
101
  "android": {
97
102
  "release": "android/app/build/generated/assets/react/release/index.android.bundle"
98
103
  }
104
+ },
105
+ "dsym": {
106
+ "ios": {}
99
107
  }
100
108
  }
101
109
  ```
102
110
 
103
- Upload is sent as: `POST {uploadBaseUrl}/sourcemaps-mobile-service//v1/upload/{apiKey}/{customerId}` with form fields `bundle_id` (app id from build), `release_version` (version name), `framework`, `platform` (android when from Gradle), and `files` (sourcemap + bundle).
111
+ Upload is sent as: `POST {uploadBaseUrl}/sourcemaps-mobile-service//v1/upload/{apiKey}/{customerId}` with form fields `bundle_id`, `release_version`, `framework`, `platform`, and `files`. Android uploads the sourcemap plus bundle when present; iOS uploads the build dSYM and archives the `.dSYM` bundle to `.zip` automatically before sending it.
104
112
 
105
113
  ## Manual upload
106
114
 
@@ -113,10 +121,17 @@ zipy-mobile-cli upload-sourcemaps debug
113
121
 
114
122
  Run this from the project root (or any directory under it that has `.zipy-mobile.json` above it).
115
123
 
124
+ For manual iOS uploads, pass the dSYM path and platform explicitly:
125
+
126
+ ```bash
127
+ ZIPY_PLATFORM=ios ZIPY_DSYM_PATH=/path/to/MyApp.app.dSYM npx zipy-mobile-cli upload-sourcemaps release
128
+ ```
129
+
116
130
  ## Requirements
117
131
 
118
132
  - Node.js >= 14
119
133
  - React Native project with Android (`android/app/build.gradle`).
134
+ - macOS/Xcode for iOS dSYM auto-upload (`ditto` is used to archive the `.dSYM` bundle before upload).
120
135
  - `zipy-mobile-cli` either as a project devDependency or installed globally (`npm install -g zipy-mobile-cli`). Gradle resolves `zipy.gradle` from node_modules first, then from global npm root.
121
136
 
122
137
  ## Command to add in your RN app (manual)
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /** Route CLI arguments to init, upload, or help flows. */
1
2
  export declare function run(argv: string[]): void;
2
3
  /**
3
4
  * Find directory containing .zipy-mobile.json (walk up from cwd).
package/dist/cli.js CHANGED
@@ -44,6 +44,8 @@ const path = __importStar(require("path"));
44
44
  const init_react_native_1 = require("./init-react-native");
45
45
  const upload_sourcemaps_1 = require("./upload-sourcemaps");
46
46
  const types_1 = require("./types");
47
+ const LOG_PREFIX = '[zipy]';
48
+ /** Route CLI arguments to init, upload, or help flows. */
47
49
  function run(argv) {
48
50
  const args = argv.slice();
49
51
  let command = null;
@@ -66,12 +68,12 @@ function run(argv) {
66
68
  }
67
69
  if (command === 'init') {
68
70
  if (platform !== 'react-native') {
69
- console.error('zipy-mobile-cli: Only "react-native" is supported. Use: zipy-mobile-cli -i react-native');
71
+ console.error(`${LOG_PREFIX} Only "react-native" is supported. Use: zipy-mobile-cli -i react-native`);
70
72
  process.exit(1);
71
73
  }
72
74
  const initOptions = parseInitOptions(args);
73
75
  (0, init_react_native_1.initReactNative)(process.cwd(), initOptions).then(() => process.exit(0), (err) => {
74
- console.error('zipy-mobile-cli:', err.message || err);
76
+ console.error(LOG_PREFIX, err.message || err);
75
77
  process.exit(1);
76
78
  });
77
79
  return;
@@ -79,16 +81,16 @@ function run(argv) {
79
81
  if (command === 'upload-sourcemaps') {
80
82
  const variant = args[1]; // release | debug
81
83
  if (!variant || !['release', 'debug'].includes(variant)) {
82
- console.error('zipy-mobile-cli: Usage: zipy-mobile-cli upload-sourcemaps <release|debug>');
84
+ console.error(`${LOG_PREFIX} Usage: zipy-mobile-cli upload-sourcemaps <release|debug>`);
83
85
  process.exit(1);
84
86
  }
85
87
  const projectRoot = findProjectRoot(process.cwd(), types_1.CONFIG_FILENAME);
86
88
  if (!projectRoot) {
87
- console.error('zipy-mobile-cli: No .zipy-mobile.json found. Run zipy-mobile-cli -i react-native first.');
89
+ console.error(`${LOG_PREFIX} No .zipy-mobile.json found. Run zipy-mobile-cli -i react-native first.`);
88
90
  process.exit(1);
89
91
  }
90
92
  (0, upload_sourcemaps_1.uploadSourcemaps)(projectRoot, variant).then(() => process.exit(0), (err) => {
91
- console.error('zipy-mobile-cli:', err.message || err);
93
+ console.error(LOG_PREFIX, err.message || err);
92
94
  process.exit(1);
93
95
  });
94
96
  return;
@@ -140,9 +142,10 @@ function findProjectRoot(cwd, configName) {
140
142
  }
141
143
  return null;
142
144
  }
145
+ /** Print the top-level CLI usage and option summary. */
143
146
  function printHelp() {
144
147
  console.log(`
145
- zipy-mobile-cli — Zipy mobile integration (sourcemap uploads on build)
148
+ zipy-mobile-cli — Zipy mobile integration (Android sourcemap uploads and iOS archive bundle/map uploads)
146
149
 
147
150
  Usage:
148
151
  zipy-mobile-cli -i react-native [--apikey KEY] [--customerId ID] [--authkey KEY]
@@ -1,10 +1,16 @@
1
- /** Default paths: outputs of react/release (same task Sentry hooks into). */
1
+ /** Default paths: outputs of the standard react/release bundle task. */
2
2
  export declare const DEFAULT_SOURCEMAP_PATHS: {
3
3
  readonly release: "android/app/build/generated/sourcemaps/react/release/index.android.bundle.map";
4
4
  readonly debug: "android/app/build/generated/sourcemaps/react/debug/index.android.bundle.map";
5
5
  };
6
6
  /** Default release bundle path: react/release output. */
7
7
  export declare const DEFAULT_BUNDLE_PATH_RELEASE = "android/app/build/generated/assets/react/release/index.android.bundle";
8
+ /** Stable iOS Zipy output folder shared by the RN bundle phase and later upload phase. */
9
+ export declare const DEFAULT_IOS_OUTPUT_DIR = "ios/build/zipy/Sourcemaps";
10
+ /** Default iOS release bundle path: written by the patched RN Xcode bundle phase. */
11
+ export declare const DEFAULT_IOS_BUNDLE_PATH_RELEASE = "ios/build/zipy/Sourcemaps/main.jsbundle";
12
+ /** Default iOS release sourcemap path: emitted by the patched RN Xcode bundle phase. */
13
+ export declare const DEFAULT_IOS_SOURCEMAP_PATH_RELEASE = "ios/build/zipy/Sourcemaps/main.jsbundle.map";
8
14
  /** Fixed upload API base URL (not prompted). */
9
15
  export declare const FIXED_UPLOAD_BASE_URL = "https://app.zipy.ai/api";
10
16
  export interface InitOptions {
@@ -25,8 +31,10 @@ export declare function writeConfig(projectRoot: string, options: InitOptions):
25
31
  */
26
32
  export declare function patchGradle(projectRoot: string): void;
27
33
  /**
28
- * Add a Run Script Build Phase to the iOS app target that runs the Zipy iOS upload script on every build/run.
29
- * Same role as zipy.gradle on Android. Patches ios/*.xcodeproj/project.pbxproj.
34
+ * Add Run Script Build Phases to the iOS app target for Zipy's archive upload flow.
35
+ * The phase stays attached to the app target, but the script itself uploads only during archive/install actions.
36
+ * We also patch the RN bundle phase so main.jsbundle.map is emitted to a stable ios/build/zipy/... path.
30
37
  */
31
38
  export declare function patchIosProject(projectRoot: string): boolean;
39
+ /** Create config and patch Android/iOS build hooks for a React Native app. */
32
40
  export declare function initReactNative(projectRoot: string, options?: InitOptions): Promise<void>;
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.FIXED_UPLOAD_BASE_URL = exports.DEFAULT_BUNDLE_PATH_RELEASE = exports.DEFAULT_SOURCEMAP_PATHS = void 0;
36
+ exports.FIXED_UPLOAD_BASE_URL = exports.DEFAULT_IOS_SOURCEMAP_PATH_RELEASE = exports.DEFAULT_IOS_BUNDLE_PATH_RELEASE = exports.DEFAULT_IOS_OUTPUT_DIR = exports.DEFAULT_BUNDLE_PATH_RELEASE = exports.DEFAULT_SOURCEMAP_PATHS = void 0;
37
37
  exports.writeConfig = writeConfig;
38
38
  exports.patchGradle = patchGradle;
39
39
  exports.patchIosProject = patchIosProject;
@@ -52,15 +52,103 @@ const IOS_XCODEPROJ_REL = 'ios';
52
52
  const GRADLE_PATCH_MARKER = 'zipy-mobile-cli/zipy.gradle';
53
53
  /** Unique string in the iOS Run Script phase; used to avoid adding twice. */
54
54
  const IOS_ZIPY_PHASE_MARKER = 'ZipySourcemapsUpload';
55
- /** Default paths: outputs of react/release (same task Sentry hooks into). */
55
+ /** Pre-bundle iOS phase that prepares the per-build Zipy bundle id before RN bundling starts. */
56
+ const IOS_ZIPY_PREPARE_MARKER = 'ZipyBundleIdPrepare';
57
+ /** Marker injected into the RN Xcode bundle phase so init can safely re-run without duplicating exports. */
58
+ const IOS_RN_SOURCEMAP_PATCH_MARKER = 'ZIPY_IOS_SOURCEMAP_PHASE=1';
59
+ /** Default paths: outputs of the standard react/release bundle task. */
56
60
  exports.DEFAULT_SOURCEMAP_PATHS = {
57
61
  release: 'android/app/build/generated/sourcemaps/react/release/index.android.bundle.map',
58
62
  debug: 'android/app/build/generated/sourcemaps/react/debug/index.android.bundle.map',
59
63
  };
60
64
  /** Default release bundle path: react/release output. */
61
65
  exports.DEFAULT_BUNDLE_PATH_RELEASE = 'android/app/build/generated/assets/react/release/index.android.bundle';
66
+ /** Stable iOS Zipy output folder shared by the RN bundle phase and later upload phase. */
67
+ exports.DEFAULT_IOS_OUTPUT_DIR = 'ios/build/zipy/Sourcemaps';
68
+ /** Default iOS release bundle path: written by the patched RN Xcode bundle phase. */
69
+ exports.DEFAULT_IOS_BUNDLE_PATH_RELEASE = `${exports.DEFAULT_IOS_OUTPUT_DIR}/main.jsbundle`;
70
+ /** Default iOS release sourcemap path: emitted by the patched RN Xcode bundle phase. */
71
+ exports.DEFAULT_IOS_SOURCEMAP_PATH_RELEASE = `${exports.DEFAULT_IOS_OUTPUT_DIR}/main.jsbundle.map`;
62
72
  /** Fixed upload API base URL (not prompted). */
63
73
  exports.FIXED_UPLOAD_BASE_URL = 'https://app.zipy.ai/api';
74
+ const LOG_PREFIX = '[zipy]';
75
+ /** Print `[zipy]`-prefixed setup logs. */
76
+ function logInfo(...args) {
77
+ console.log(LOG_PREFIX, ...args);
78
+ }
79
+ /** Find an existing PBX phase reference line by marker name. */
80
+ function getPhaseReferenceLine(content, marker) {
81
+ const phaseMatch = content.match(new RegExp(`^\\s*([A-Fa-f0-9]{24} /\\* ${marker} \\*/,)\\s*$`, 'm'));
82
+ return phaseMatch ? phaseMatch[1] : null;
83
+ }
84
+ /** Rewrite the first native target's build phase list while preserving indentation. */
85
+ function updateFirstTargetBuildPhases(content, updateLines) {
86
+ const buildPhasesRegex = /(buildPhases = \(\s*\n)([\s\S]*?)(\s*\);)/;
87
+ const match = content.match(buildPhasesRegex);
88
+ if (!match)
89
+ return content;
90
+ const body = match[2];
91
+ const bodyLines = body.split('\n').filter((line) => line.trim() !== '');
92
+ const indent = bodyLines.map((line) => line.match(/^(\s+)/)?.[1]).find((value) => typeof value === 'string') ?? '\t\t\t\t';
93
+ const closingIndent = match[3].match(/([ \t]*)\);\s*$/)?.[1] ?? '\t\t\t';
94
+ const updatedBody = updateLines(bodyLines, indent).join('\n');
95
+ return content.replace(buildPhasesRegex, `${match[1]}${updatedBody}\n${closingIndent});`);
96
+ }
97
+ /** Move a phase to the end so it runs after bundling and packaging. */
98
+ function movePhaseToEndOfFirstTarget(content, phaseReferenceLine, marker) {
99
+ return updateFirstTargetBuildPhases(content, (bodyLines, indent) => {
100
+ const nonPhaseLines = bodyLines.filter((line) => !line.includes(marker));
101
+ // Keep the upload phase at the end so Xcode has already produced the archive bundle + sourcemap before we upload them.
102
+ return [...nonPhaseLines, `${indent}${phaseReferenceLine}`];
103
+ });
104
+ }
105
+ /** Place a phase immediately before the RN bundle phase in the first target. */
106
+ function movePhaseBeforeBundleTask(content, phaseReferenceLine, marker) {
107
+ return updateFirstTargetBuildPhases(content, (bodyLines, indent) => {
108
+ const nonPhaseLines = bodyLines.filter((line) => !line.includes(marker));
109
+ const bundleIndex = nonPhaseLines.findIndex((line) => line.includes('Bundle React Native code and images'));
110
+ const phaseLine = `${indent}${phaseReferenceLine}`;
111
+ if (bundleIndex === -1) {
112
+ return [phaseLine, ...nonPhaseLines];
113
+ }
114
+ return [...nonPhaseLines.slice(0, bundleIndex), phaseLine, ...nonPhaseLines.slice(bundleIndex)];
115
+ });
116
+ }
117
+ /** Build a PBX shell phase block with escaped script contents. */
118
+ function buildIosShellPhaseBlock(phaseId, marker, scriptBody) {
119
+ const escapedScript = scriptBody.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
120
+ return `
121
+ ${phaseId} /* ${marker} */ = {
122
+ isa = PBXShellScriptBuildPhase;
123
+ buildActionMask = 2147483647;
124
+ files = (
125
+ );
126
+ inputPaths = (
127
+ );
128
+ name = "${marker}";
129
+ outputPaths = (
130
+ );
131
+ runOnlyForDeploymentPostprocessing = 0;
132
+ shellPath = /bin/sh;
133
+ shellScript = "${escapedScript}\\n";
134
+ };
135
+ `;
136
+ }
137
+ /** Insert a shell phase block into the PBX shell phase section. */
138
+ function insertShellPhaseBlock(content, phaseBlock) {
139
+ const shellSectionEnd = '/* End PBXShellScriptBuildPhase section */';
140
+ const shellSectionBegin = '/* Begin PBXShellScriptBuildPhase section */';
141
+ if (content.includes(shellSectionEnd)) {
142
+ return content.replace(shellSectionEnd, phaseBlock + '\t' + shellSectionEnd);
143
+ }
144
+ if (content.includes(shellSectionBegin)) {
145
+ return content.replace(shellSectionBegin, shellSectionBegin + phaseBlock);
146
+ }
147
+ const endProject = '/* End PBXProject section */';
148
+ const newSection = `\n\t/* Begin PBXShellScriptBuildPhase section */\n${phaseBlock}\t/* End PBXShellScriptBuildPhase section */\n`;
149
+ return content.replace(endProject, newSection + endProject);
150
+ }
151
+ /** Prompt for a setup value and fall back to the provided default. */
64
152
  function prompt(question, defaultAnswer) {
65
153
  return new Promise((resolve) => {
66
154
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -75,7 +163,30 @@ function prompt(question, defaultAnswer) {
75
163
  function isInteractive() {
76
164
  return process.stdin.isTTY === true;
77
165
  }
78
- const CONFIG_COMMENT = 'To change sourcemap or bundle paths: edit sourcemaps.android.release, sourcemaps.android.debug, and bundle.android.release (paths are relative to project root).';
166
+ const CONFIG_COMMENT = 'To change Android upload paths, edit sourcemaps.android.release, sourcemaps.android.debug, and bundle.android.release. iOS archive uploads auto-discover main.jsbundle and main.jsbundle.map from common ios/build, archive, and DerivedData locations, or you can override sourcemaps.ios.release/debug and bundle.ios.release/debug for manual uploads.';
167
+ /** Build the prefix injected into the RN iOS bundle phase. */
168
+ function buildZipyIosBundlePhasePrefix() {
169
+ return (`export ${IOS_RN_SOURCEMAP_PATCH_MARKER}\\n` +
170
+ `# Stage the React Native bundle artifacts in a stable path so the later Zipy upload phase can locate them reliably during archives.\\n` +
171
+ `export BUNDLE_FILE=\\"${'${PROJECT_DIR}'}/build/zipy/Sourcemaps/main.jsbundle\\"\\n` +
172
+ `export SOURCEMAP_FILE=\\"${'${PROJECT_DIR}'}/build/zipy/Sourcemaps/main.jsbundle.map\\"\\n` +
173
+ `mkdir -p \\"$(dirname \\"$BUNDLE_FILE\\")\\"\\n` +
174
+ `set -e\\n`);
175
+ }
176
+ /** Patch the RN bundle phase so iOS bundle artifacts land in Zipy's stable output path. */
177
+ function patchReactNativeIosBundlePhase(content) {
178
+ const bundlePhaseRegex = /(\/\* Bundle React Native code and images \*\/ = \{[\s\S]*?shellScript = ")([\s\S]*?react-native-xcode\.sh[\s\S]*?)(";\n[\s\S]*?\};)/;
179
+ const match = content.match(bundlePhaseRegex);
180
+ if (!match) {
181
+ return content;
182
+ }
183
+ const currentScript = match[2];
184
+ const desiredPrefix = buildZipyIosBundlePhasePrefix();
185
+ const setEIndex = currentScript.indexOf('set -e\\n');
186
+ const suffix = setEIndex >= 0 ? currentScript.slice(setEIndex + 'set -e\\n'.length) : currentScript;
187
+ const normalizedScript = desiredPrefix + suffix.replace(/^\n+/, '');
188
+ return content.replace(bundlePhaseRegex, `$1${normalizedScript}$3`);
189
+ }
79
190
  /**
80
191
  * Create .zipy-mobile.json with API config and sourcemap/bundle paths. Uses fixed upload URL and framework=react-native.
81
192
  */
@@ -94,11 +205,19 @@ function writeConfig(projectRoot, options) {
94
205
  release: options.sourcemapPathRelease ?? exports.DEFAULT_SOURCEMAP_PATHS.release,
95
206
  debug: options.sourcemapPathDebug ?? exports.DEFAULT_SOURCEMAP_PATHS.debug,
96
207
  },
208
+ // Keep explicit iOS release defaults so .zipy-mobile.json documents the staged archive output.
209
+ ios: {
210
+ release: exports.DEFAULT_IOS_SOURCEMAP_PATH_RELEASE,
211
+ },
97
212
  },
98
213
  bundle: {
99
214
  android: {
100
215
  release: options.bundlePathRelease ?? exports.DEFAULT_BUNDLE_PATH_RELEASE,
101
216
  },
217
+ // Keep explicit iOS release defaults so .zipy-mobile.json documents the staged archive output.
218
+ ios: {
219
+ release: exports.DEFAULT_IOS_BUNDLE_PATH_RELEASE,
220
+ },
102
221
  },
103
222
  };
104
223
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
@@ -124,8 +243,9 @@ apply from: new File(project.rootProject.projectDir.parentFile, "node_modules/zi
124
243
  fs.appendFileSync(buildGradlePath, applyBlock, 'utf8');
125
244
  }
126
245
  /**
127
- * Add a Run Script Build Phase to the iOS app target that runs the Zipy iOS upload script on every build/run.
128
- * Same role as zipy.gradle on Android. Patches ios/*.xcodeproj/project.pbxproj.
246
+ * Add Run Script Build Phases to the iOS app target for Zipy's archive upload flow.
247
+ * The phase stays attached to the app target, but the script itself uploads only during archive/install actions.
248
+ * We also patch the RN bundle phase so main.jsbundle.map is emitted to a stable ios/build/zipy/... path.
129
249
  */
130
250
  function patchIosProject(projectRoot) {
131
251
  const iosDir = path.join(projectRoot, IOS_XCODEPROJ_REL);
@@ -138,51 +258,40 @@ function patchIosProject(projectRoot) {
138
258
  if (!fs.existsSync(pbxPath))
139
259
  return false;
140
260
  let content = fs.readFileSync(pbxPath, 'utf8');
141
- if (content.includes(IOS_ZIPY_PHASE_MARKER))
142
- return true; // Already patched
143
- // 24-char hex ID (Xcode style)
144
- const phaseId = Array.from({ length: 24 }, () => Math.floor(Math.random() * 16).toString(16)).join('').toUpperCase();
145
- // Script: run zipy-ios-upload.sh from project root (SRCROOT = ios/)
146
- const scriptBody = 'cd "${SRCROOT}/.." && [ -f node_modules/zipy-mobile-cli/zipy-ios-upload.sh ] && sh node_modules/zipy-mobile-cli/zipy-ios-upload.sh || true';
147
- const escapedScript = scriptBody.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
148
- const phaseBlock = `
149
- ${phaseId} /* ZipySourcemapsUpload */ = {
150
- isa = PBXShellScriptBuildPhase;
151
- buildActionMask = 2147483647;
152
- files = (
153
- );
154
- inputPaths = (
155
- );
156
- name = "ZipySourcemapsUpload";
157
- outputPaths = (
158
- );
159
- runOnlyForDeploymentPostprocessing = 0;
160
- shellPath = /bin/sh;
161
- shellScript = "${escapedScript}\\n";
162
- };
163
- `;
164
- // Insert phase into PBXShellScriptBuildPhase section
165
- const shellSectionEnd = '/* End PBXShellScriptBuildPhase section */';
166
- const shellSectionBegin = '/* Begin PBXShellScriptBuildPhase section */';
167
- if (content.includes(shellSectionEnd)) {
168
- content = content.replace(shellSectionEnd, phaseBlock + '\t' + shellSectionEnd);
169
- }
170
- else if (content.includes(shellSectionBegin)) {
171
- content = content.replace(shellSectionBegin, shellSectionBegin + phaseBlock);
172
- }
173
- else {
174
- // No section yet: add before End PBXProject section
175
- const endProject = '/* End PBXProject section */';
176
- const newSection = `\n\t/* Begin PBXShellScriptBuildPhase section */\n${phaseBlock}\t/* End PBXShellScriptBuildPhase section */\n`;
177
- content = content.replace(endProject, newSection + endProject);
261
+ const prepareScriptBody = 'cd "${SRCROOT}/.." && [ -f node_modules/zipy-mobile-cli/zipy-ios-prepare.sh ] && sh node_modules/zipy-mobile-cli/zipy-ios-prepare.sh || true';
262
+ const uploadScriptBody = 'cd "${SRCROOT}/.." && [ -f node_modules/zipy-mobile-cli/zipy-ios-upload.sh ] && sh node_modules/zipy-mobile-cli/zipy-ios-upload.sh || true';
263
+ if (content.includes(IOS_ZIPY_PHASE_MARKER)) {
264
+ const existingPrepareReference = getPhaseReferenceLine(content, IOS_ZIPY_PREPARE_MARKER);
265
+ const existingUploadReference = getPhaseReferenceLine(content, IOS_ZIPY_PHASE_MARKER);
266
+ let updatedContent = patchReactNativeIosBundlePhase(content);
267
+ if (!existingPrepareReference) {
268
+ const preparePhaseId = Array.from({ length: 24 }, () => Math.floor(Math.random() * 16).toString(16)).join('').toUpperCase();
269
+ updatedContent = insertShellPhaseBlock(updatedContent, buildIosShellPhaseBlock(preparePhaseId, IOS_ZIPY_PREPARE_MARKER, prepareScriptBody));
270
+ updatedContent = movePhaseBeforeBundleTask(updatedContent, `${preparePhaseId} /* ${IOS_ZIPY_PREPARE_MARKER} */,`, IOS_ZIPY_PREPARE_MARKER);
271
+ }
272
+ else {
273
+ updatedContent = movePhaseBeforeBundleTask(updatedContent, existingPrepareReference, IOS_ZIPY_PREPARE_MARKER);
274
+ }
275
+ if (!existingUploadReference) {
276
+ if (updatedContent !== content) {
277
+ fs.writeFileSync(pbxPath, updatedContent, 'utf8');
278
+ }
279
+ return true;
280
+ }
281
+ updatedContent = movePhaseToEndOfFirstTarget(updatedContent, existingUploadReference, IOS_ZIPY_PHASE_MARKER);
282
+ if (updatedContent !== content) {
283
+ fs.writeFileSync(pbxPath, updatedContent, 'utf8');
284
+ }
285
+ return true;
178
286
  }
179
- // Add our phase ID to the first target's buildPhases (main app target; first buildPhases = ( in file)
180
- const buildPhasesRegex = /(buildPhases = \(\s*\n)(\s+)([A-Fa-f0-9]{24})(.*)/;
181
- const buildPhasesMatch = content.match(buildPhasesRegex);
182
- if (!buildPhasesMatch)
183
- return false;
184
- const indent = buildPhasesMatch[2];
185
- content = content.replace(buildPhasesRegex, `$1${indent}${phaseId} /* ZipySourcemapsUpload */,\n${indent}$3$4`);
287
+ // 24-char hex IDs (Xcode style)
288
+ const preparePhaseId = Array.from({ length: 24 }, () => Math.floor(Math.random() * 16).toString(16)).join('').toUpperCase();
289
+ const uploadPhaseId = Array.from({ length: 24 }, () => Math.floor(Math.random() * 16).toString(16)).join('').toUpperCase();
290
+ content = insertShellPhaseBlock(content, buildIosShellPhaseBlock(preparePhaseId, IOS_ZIPY_PREPARE_MARKER, prepareScriptBody));
291
+ content = insertShellPhaseBlock(content, buildIosShellPhaseBlock(uploadPhaseId, IOS_ZIPY_PHASE_MARKER, uploadScriptBody));
292
+ content = patchReactNativeIosBundlePhase(content);
293
+ content = movePhaseBeforeBundleTask(content, `${preparePhaseId} /* ${IOS_ZIPY_PREPARE_MARKER} */,`, IOS_ZIPY_PREPARE_MARKER);
294
+ content = movePhaseToEndOfFirstTarget(content, `${uploadPhaseId} /* ${IOS_ZIPY_PHASE_MARKER} */,`, IOS_ZIPY_PHASE_MARKER);
186
295
  fs.writeFileSync(pbxPath, content, 'utf8');
187
296
  return true;
188
297
  }
@@ -194,6 +303,7 @@ function ensureReactNativeProject(projectRoot) {
194
303
  throw new Error('Current directory does not look like a React Native project (missing android/ or package.json). Run this from the project root.');
195
304
  }
196
305
  }
306
+ /** Create config and patch Android/iOS build hooks for a React Native app. */
197
307
  async function initReactNative(projectRoot, options) {
198
308
  projectRoot = path.resolve(projectRoot);
199
309
  ensureReactNativeProject(projectRoot);
@@ -204,17 +314,19 @@ async function initReactNative(projectRoot, options) {
204
314
  let customerId = options?.customerId ?? process.env.ZIPY_CUSTOMER_ID ?? '';
205
315
  let authKey = options?.authKey ?? process.env.ZIPY_AUTH_KEY ?? '';
206
316
  if (isInteractive()) {
207
- console.log('Zipy Mobile CLI — React Native setup\n');
317
+ logInfo('React Native setup\n');
208
318
  if (!apiKey)
209
319
  apiKey = await prompt('API key (e.g. abc123key)', '');
210
320
  if (!customerId)
211
321
  customerId = await prompt('Customer ID (e.g. 99)', '');
212
322
  if (!authKey)
213
323
  authKey = await prompt('Auth key (optional)', '');
214
- console.log('\nDefault paths (relative to project root):');
215
- console.log(' sourcemaps.android.release:', sourcemapPathRelease);
216
- console.log(' sourcemaps.android.debug: ', sourcemapPathDebug);
217
- console.log(' bundle.android.release: ', bundlePathRelease);
324
+ logInfo('\nDefault paths (relative to project root):');
325
+ logInfo('sourcemaps.android.release:', sourcemapPathRelease);
326
+ logInfo('sourcemaps.android.debug:', sourcemapPathDebug);
327
+ logInfo('bundle.android.release:', bundlePathRelease);
328
+ logInfo('bundle.ios.release:', exports.DEFAULT_IOS_BUNDLE_PATH_RELEASE);
329
+ logInfo('sourcemaps.ios.release:', exports.DEFAULT_IOS_SOURCEMAP_PATH_RELEASE);
218
330
  const useDefaults = await prompt('Use these paths? (Y/n)', 'Y');
219
331
  if (useDefaults && useDefaults.toLowerCase() === 'n') {
220
332
  sourcemapPathRelease = await prompt('Path to release sourcemap', sourcemapPathRelease);
@@ -235,12 +347,12 @@ async function initReactNative(projectRoot, options) {
235
347
  customerId,
236
348
  authKey,
237
349
  });
238
- console.log('Created config:', configPath);
350
+ logInfo('Created config:', configPath);
239
351
  patchGradle(projectRoot);
240
- console.log('Patched', ANDROID_APP_BUILD_GRADLE, '— sourcemaps will upload after assembleRelease (debug not hooked).');
352
+ logInfo(`Patched ${ANDROID_APP_BUILD_GRADLE} — sourcemaps will upload after assembleRelease (debug not hooked).`);
241
353
  const iosPatched = patchIosProject(projectRoot);
242
354
  if (iosPatched) {
243
- console.log('Patched iOS project — added "ZipySourcemapsUpload" Run Script phase (runs on every build/run).');
355
+ logInfo('Patched iOS project — added "ZipySourcemapsUpload" Run Script phase and RN sourcemap export (uploads main.jsbundle + main.jsbundle.map for Xcode archives).');
244
356
  }
245
- console.log('\nDone. Next: run ./gradlew assembleRelease from android/ (or from root with -p android); for iOS, build/run from Xcode or react-native run-ios.');
357
+ logInfo('\nDone. Next: run ./gradlew assembleRelease from android/ (or from root with -p android); for iOS, Archive from Xcode to upload the generated main.jsbundle and main.jsbundle.map.');
246
358
  }
package/dist/types.d.ts CHANGED
@@ -21,12 +21,20 @@ export interface ZipyConfig {
21
21
  release: string;
22
22
  debug: string;
23
23
  };
24
+ ios?: {
25
+ release?: string;
26
+ debug?: string;
27
+ };
24
28
  };
25
29
  bundle?: {
26
30
  android: {
27
31
  release: string;
28
32
  debug?: string;
29
33
  };
34
+ ios?: {
35
+ release?: string;
36
+ debug?: string;
37
+ };
30
38
  };
31
39
  /** Ignored at runtime; explains how to edit paths (JSON does not support comments). */
32
40
  _comment?: string;
@@ -1,5 +1,9 @@
1
1
  import type { ZipyConfig, BuildVariant } from './types';
2
+ type SupportedPlatform = 'android' | 'ios';
3
+ /** Load and validate the project's Zipy config file. */
2
4
  export declare function loadConfig(projectRoot: string): ZipyConfig;
3
- export declare function getSourcemapPath(projectRoot: string, variant: BuildVariant, config: ZipyConfig): string;
4
- export declare function getBundlePath(projectRoot: string, variant: BuildVariant, config: ZipyConfig): string | null;
5
+ export declare function getSourcemapPath(projectRoot: string, platform: SupportedPlatform, variant: BuildVariant, config: ZipyConfig): string;
6
+ export declare function getBundlePath(projectRoot: string, platform: SupportedPlatform, variant: BuildVariant, config: ZipyConfig): string | null;
7
+ /** Resolve platform-specific artifacts, build fields, and send the upload request. */
5
8
  export declare function uploadSourcemaps(projectRoot: string, variant: BuildVariant): Promise<void>;
9
+ export {};
@@ -49,6 +49,35 @@ const http = __importStar(require("http"));
49
49
  const types_1 = require("./types");
50
50
  /** Upload path (no leading slash on uploadBaseUrl). Single source of truth for request and logs. */
51
51
  const UPLOAD_PATH = '/sourcemaps-mobile-service/v1/upload';
52
+ const LOG_PREFIX = '[zipy]';
53
+ /** Print `[zipy]` prefix upload logs. */
54
+ function logInfo(...args) {
55
+ console.log(LOG_PREFIX, ...args);
56
+ }
57
+ /** Print `[zipy]` prefixed upload warnings. */
58
+ function logWarn(...args) {
59
+ console.warn(LOG_PREFIX, ...args);
60
+ }
61
+ /** Escape a value so the debug curl command stays shell-safe. */
62
+ function shellEscape(value) {
63
+ return `'${value.replace(/'/g, `'\\''`)}'`;
64
+ }
65
+ /** Build a replayable curl command that mirrors the upload request. */
66
+ function buildCurlCommand(fullUrl, fields, filePaths) {
67
+ const curlParts = ['curl', '-X', 'POST', shellEscape(fullUrl)];
68
+ if (fields.key)
69
+ curlParts.push('-F', shellEscape(`key=${fields.key}`));
70
+ curlParts.push('-F', shellEscape(`bundle_id=${fields.bundle_id}`));
71
+ curlParts.push('-F', shellEscape(`release_version=${fields.release_version}`));
72
+ curlParts.push('-F', shellEscape(`framework=${fields.framework}`));
73
+ curlParts.push('-F', shellEscape(`platform=${fields.platform}`));
74
+ for (const file of filePaths) {
75
+ // Keep the printed curl close to the actual multipart payload so uploads are easy to replay locally.
76
+ curlParts.push('-F', shellEscape(`${file.fieldName}=@${file.path}`));
77
+ }
78
+ return curlParts.join(' ');
79
+ }
80
+ /** Load and validate the project's Zipy config file. */
52
81
  function loadConfig(projectRoot) {
53
82
  const configPath = path.join(projectRoot, types_1.CONFIG_FILENAME);
54
83
  if (!fs.existsSync(configPath)) {
@@ -71,24 +100,40 @@ function loadConfig(projectRoot) {
71
100
  throw new Error(`Invalid ${types_1.CONFIG_FILENAME}: ${message}`);
72
101
  }
73
102
  }
74
- function getSourcemapPath(projectRoot, variant, config) {
75
- const relativePath = config.sourcemaps?.android?.[variant];
76
- if (!relativePath) {
77
- throw new Error(`No sourcemap path configured for android variant "${variant}". Check .zipy-mobile.json "sourcemaps.android".`);
78
- }
79
- const absolutePath = path.resolve(projectRoot, relativePath);
103
+ /** Resolve an artifact path relative to the project root and assert it exists. */
104
+ function resolveArtifactPath(projectRoot, configuredPath) {
105
+ const absolutePath = path.isAbsolute(configuredPath) ? configuredPath : path.resolve(projectRoot, configuredPath);
80
106
  if (!fs.existsSync(absolutePath)) {
81
- throw new Error(`Sourcemap file not found: ${absolutePath}. Build may not have generated it yet.`);
107
+ throw new Error(`Artifact not found: ${absolutePath}. Build may not have generated it yet.`);
82
108
  }
83
109
  return absolutePath;
84
110
  }
85
- function getBundlePath(projectRoot, variant, config) {
86
- const relativePath = config.bundle?.android?.[variant];
87
- if (!relativePath)
88
- return null;
89
- const absolutePath = path.resolve(projectRoot, relativePath);
90
- if (!fs.existsSync(absolutePath))
91
- return null;
111
+ function getSourcemapPath(projectRoot, platform, variant, config) {
112
+ const configuredPath = platform === 'ios'
113
+ ? process.env.ZIPY_SOURCEMAP_PATH ?? process.env.SOURCEMAP_FILE ?? config.sourcemaps?.ios?.[variant]
114
+ : process.env.ZIPY_SOURCEMAP_PATH ?? config.sourcemaps?.android?.[variant];
115
+ if (!configuredPath) {
116
+ throw new Error(platform === 'ios'
117
+ ? `No sourcemap path configured for ios variant "${variant}". Set ZIPY_SOURCEMAP_PATH in the Xcode build phase or add ".zipy-mobile.json" "sourcemaps.ios.${variant}".`
118
+ : `No sourcemap path configured for android variant "${variant}". Check .zipy-mobile.json "sourcemaps.android".`);
119
+ }
120
+ const absolutePath = resolveArtifactPath(projectRoot, configuredPath);
121
+ if (platform === 'ios') {
122
+ logInfo('Resolved iOS sourcemap path:', absolutePath);
123
+ }
124
+ return absolutePath;
125
+ }
126
+ function getBundlePath(projectRoot, platform, variant, config) {
127
+ const configuredPath = platform === 'ios'
128
+ ? process.env.ZIPY_BUNDLE_PATH ?? config.bundle?.ios?.[variant]
129
+ : process.env.ZIPY_BUNDLE_PATH ?? config.bundle?.android?.[variant];
130
+ if (!configuredPath) {
131
+ return platform === 'ios' ? null : null;
132
+ }
133
+ const absolutePath = resolveArtifactPath(projectRoot, configuredPath);
134
+ if (platform === 'ios') {
135
+ logInfo('Resolved iOS bundle path:', absolutePath);
136
+ }
92
137
  return absolutePath;
93
138
  }
94
139
  /**
@@ -138,7 +183,7 @@ function extractIdsFromSourcemap(sourcemapPath) {
138
183
  return out;
139
184
  }
140
185
  /**
141
- * Packager source map path (Metro writes this first; it contains the Debug ID before Hermes/Sentry compose).
186
+ * Packager source map path (Metro writes this first; it contains the Debug ID before later map composition).
142
187
  * Derived from the final map path: generated/sourcemaps -> intermediates/sourcemaps, and .map -> .packager.map
143
188
  */
144
189
  function getPackagerSourcemapPath(finalSourcemapPath) {
@@ -198,13 +243,39 @@ function buildMultipartBody(fields, filePaths) {
198
243
  append(`--${boundary}--${crlf}`);
199
244
  return { body: Buffer.concat(parts), boundary };
200
245
  }
246
+ function getPlatform(platformEnv) {
247
+ return platformEnv?.toLowerCase() === 'ios' ? 'ios' : 'android';
248
+ }
249
+ /** Collect the files that should be attached for the current platform upload. */
250
+ function getUploadArtifacts(projectRoot, variant, platform, config) {
251
+ if (platform === 'ios') {
252
+ const sourcemapPath = getSourcemapPath(projectRoot, platform, variant, config);
253
+ const bundlePath = getBundlePath(projectRoot, platform, variant, config);
254
+ if (!bundlePath) {
255
+ throw new Error(`No bundle path configured for ios variant "${variant}". Set ZIPY_BUNDLE_PATH in the Xcode build phase or add ".zipy-mobile.json" "bundle.ios.${variant}".`);
256
+ }
257
+ // Upload the archived iOS JS bundle together with its source map so Zipy can deobfuscate JS stack traces.
258
+ return [
259
+ { path: sourcemapPath, fieldName: 'files', filename: path.basename(sourcemapPath) },
260
+ { path: bundlePath, fieldName: 'files', filename: path.basename(bundlePath) },
261
+ ];
262
+ }
263
+ const sourcemapPath = getSourcemapPath(projectRoot, platform, variant, config);
264
+ const artifacts = [
265
+ { path: sourcemapPath, fieldName: 'files', filename: path.basename(sourcemapPath) },
266
+ ];
267
+ const bundlePath = getBundlePath(projectRoot, platform, variant, config);
268
+ if (bundlePath)
269
+ artifacts.push({ path: bundlePath, fieldName: 'files', filename: path.basename(bundlePath) });
270
+ return artifacts;
271
+ }
201
272
  /**
202
273
  * POST multipart form to uploadBaseUrl + UPLOAD_PATH + apiKey/customerId.
203
274
  */
204
275
  function uploadMultipart(uploadBaseUrl, apiKey, customerId, uploadPath, fields, filePaths) {
205
276
  return new Promise((resolve, reject) => {
206
277
  if (!uploadBaseUrl || !apiKey || !customerId) {
207
- console.warn('zipy-mobile-cli: uploadBaseUrl, apiKey and customerId are required in .zipy-mobile.json. Skipping upload.');
278
+ logWarn('uploadBaseUrl, apiKey and customerId are required in .zipy-mobile.json. Skipping upload.');
208
279
  return resolve();
209
280
  }
210
281
  const pathname = `${uploadPath}/${encodeURIComponent(apiKey)}/${encodeURIComponent(customerId)}`;
@@ -239,6 +310,7 @@ function uploadMultipart(uploadBaseUrl, apiKey, customerId, uploadPath, fields,
239
310
  req.end();
240
311
  });
241
312
  }
313
+ /** Resolve platform-specific artifacts, build fields, and send the upload request. */
242
314
  async function uploadSourcemaps(projectRoot, variant) {
243
315
  const config = loadConfig(projectRoot);
244
316
  const uploadBaseUrl = (config.uploadBaseUrl ?? process.env.ZIPY_UPLOAD_BASE_URL ?? '').replace(/\/$/, '');
@@ -246,23 +318,31 @@ async function uploadSourcemaps(projectRoot, variant) {
246
318
  const customerId = config.customerId ?? process.env.ZIPY_CUSTOMER_ID ?? '';
247
319
  const authKey = config.authKey ?? process.env.ZIPY_AUTH_KEY ?? '';
248
320
  const framework = config.framework ?? process.env.ZIPY_FRAMEWORK ?? 'react-native';
249
- const platform = process.env.ZIPY_PLATFORM ?? 'android';
321
+ const platform = getPlatform(process.env.ZIPY_PLATFORM);
250
322
  const releaseName = process.env.ZIPY_RELEASE ?? process.env.ZIPY_RELEASE_VERSION ?? '';
251
- const sourcemapPath = getSourcemapPath(projectRoot, variant, config);
252
- const ids = extractIdsFromSourcemap(sourcemapPath);
253
- const zipyBundleIdFromGradle = process.env.ZIPY_DEBUG_ID;
254
- // Prefer the value coming from Gradle (sentinel file), then what we can read from the map(s).
255
- const zipyBundleId = zipyBundleIdFromGradle ?? ids.debugId;
256
- if (ids.bundleId)
257
- console.log('> Bundle ID from map (if present):', ids.bundleId);
258
- if (zipyBundleId)
259
- console.log('> Zipy bundle id for the build:', zipyBundleId);
260
- const filePaths = [
261
- { path: sourcemapPath, fieldName: 'files', filename: path.basename(sourcemapPath) },
262
- ];
263
- const bundlePath = getBundlePath(projectRoot, variant, config);
264
- if (bundlePath)
265
- filePaths.push({ path: bundlePath, fieldName: 'files', filename: path.basename(bundlePath) });
323
+ let zipyBundleId;
324
+ if (platform === 'android') {
325
+ const sourcemapPath = getSourcemapPath(projectRoot, platform, variant, config);
326
+ const ids = extractIdsFromSourcemap(sourcemapPath);
327
+ const zipyBundleIdFromGradle = process.env.ZIPY_DEBUG_ID;
328
+ // Android injects the per-build Zipy bundle id into the map, so prefer that over the stable app id.
329
+ zipyBundleId = zipyBundleIdFromGradle ?? ids.debugId;
330
+ if (ids.bundleId)
331
+ logInfo('Bundle ID from map (if present):', ids.bundleId);
332
+ if (zipyBundleId)
333
+ logInfo('Zipy bundle id for the build:', zipyBundleId);
334
+ }
335
+ else {
336
+ const sourcemapPath = getSourcemapPath(projectRoot, platform, variant, config);
337
+ const ids = extractIdsFromSourcemap(sourcemapPath);
338
+ // The iOS bundle phase writes a per-build Zipy debug id before bundling, so prefer that over the app id.
339
+ zipyBundleId = process.env.ZIPY_DEBUG_ID ?? ids.debugId;
340
+ if (ids.bundleId)
341
+ logInfo('Bundle ID from map (if present):', ids.bundleId);
342
+ if (zipyBundleId)
343
+ logInfo('Zipy bundle id for the build:', zipyBundleId);
344
+ }
345
+ const filePaths = getUploadArtifacts(projectRoot, variant, platform, config);
266
346
  // Form fields matching API: key (auth), bundle_id, release_version, framework, platform; then files
267
347
  // We send the Zipy bundle id as bundle_id so errors can be grouped per build on the backend.
268
348
  // Older builds used debugId naming, but the semantics are the same (per-build UUID), so we just
@@ -276,9 +356,11 @@ async function uploadSourcemaps(projectRoot, variant) {
276
356
  };
277
357
  const baseUrl = uploadBaseUrl.replace(/\/$/, '');
278
358
  const fullUrl = `${baseUrl}${UPLOAD_PATH}/${encodeURIComponent(apiKey)}/${encodeURIComponent(customerId)}`;
279
- console.log('zipy-mobile-cli: Request URL:', fullUrl);
280
- console.log('zipy-mobile-cli: Form fields:', JSON.stringify(fields, null, 2));
281
- console.log('zipy-mobile-cli: Files:', filePaths.map((f) => ({ path: f.path, filename: f.filename })));
359
+ logInfo('Platform:', platform);
360
+ logInfo('Request URL:', fullUrl);
361
+ logInfo('Form fields:', JSON.stringify(fields, null, 2));
362
+ logInfo('Files:', filePaths.map((f) => ({ path: f.path, filename: f.filename })));
363
+ logInfo('curl request:', buildCurlCommand(fullUrl, fields, filePaths));
282
364
  await uploadMultipart(uploadBaseUrl, apiKey, customerId, UPLOAD_PATH, fields, filePaths);
283
- console.log('zipy-mobile-cli: Upload done.');
365
+ logInfo('Upload done.');
284
366
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zipy-mobile-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "CLI to integrate Zipy with mobile apps — uploads sourcemaps when building (e.g. React Native Android).",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
@@ -12,30 +12,15 @@
12
12
  "prepublishOnly": "npm run build",
13
13
  "test": "node -e \"require('./dist/cli').run(['--help'])\""
14
14
  },
15
- "keywords": [
16
- "zipy",
17
- "sourcemaps",
18
- "react-native",
19
- "android",
20
- "cli"
21
- ],
15
+ "keywords": ["zipy", "sourcemaps", "react-native", "android", "cli"],
22
16
  "author": "",
23
17
  "license": "ISC",
24
18
  "engines": {
25
19
  "node": ">=14.0.0"
26
20
  },
27
- "files": [
28
- "bin",
29
- "dist",
30
- "zipy.gradle",
31
- "zipy-ios-upload.sh"
32
- ],
21
+ "files": ["bin", "dist", "zipy.gradle", "zipy-ios-prepare.sh", "zipy-ios-upload.sh"],
33
22
  "devDependencies": {
34
23
  "@types/node": "^20.10.0",
35
24
  "typescript": "^5.3.0"
36
- },
37
- "dependencies": {
38
- "undici-types": "^6.21.0"
39
- },
40
- "types": "./dist/cli.d.ts"
25
+ }
41
26
  }
@@ -0,0 +1,76 @@
1
+ #!/bin/sh
2
+ # Zipy iOS prepare hook — runs before the RN bundle phase so iOS gets a per-build Zipy bundle id
3
+ # just like Android. The later upload phase reads the sentinel and reverts debugid.cjs after upload.
4
+ set -eu
5
+
6
+ echo "[zipy] iOS bundle-id prepare phase started (ZipyBundleIdPrepare)"
7
+
8
+ if [ -n "${ZIPY_DISABLE_AUTO_UPLOAD:-}" ]; then
9
+ echo "[zipy] Skipped prepare (ZIPY_DISABLE_AUTO_UPLOAD is set)"
10
+ exit 0
11
+ fi
12
+
13
+ # Keep the prepare phase aligned with archive/install runs so local simulator builds are unaffected.
14
+ if [ -z "${ARCHIVE_PATH:-}" ] && [ "${ACTION:-}" != "install" ]; then
15
+ echo "[zipy] Skipped prepare (not an Xcode archive/install action)"
16
+ exit 0
17
+ fi
18
+
19
+ PROJECT_ROOT="${SRCROOT}/.."
20
+ cd "$PROJECT_ROOT"
21
+
22
+ ZIPY_IOS_OUTPUT_DIR="${PROJECT_ROOT}/ios/build/zipy/Sourcemaps"
23
+ ZIPY_IOS_SENTINEL_FILE="${ZIPY_IOS_OUTPUT_DIR}/zipy-debug-id.txt"
24
+ ZIPY_IOS_DEBUGID_META_FILE="${ZIPY_IOS_OUTPUT_DIR}/zipy-debugid-meta.json"
25
+ mkdir -p "$ZIPY_IOS_OUTPUT_DIR"
26
+
27
+ ZIPY_DEBUG_ID="$(node -e "console.log(require('crypto').randomUUID())")"
28
+ printf %s "$ZIPY_DEBUG_ID" > "$ZIPY_IOS_SENTINEL_FILE"
29
+ echo "[zipy] generated ZIPY_DEBUG_ID for iOS archive"
30
+
31
+ PROJECT_ROOT="$PROJECT_ROOT" ZIPY_DEBUG_ID="$ZIPY_DEBUG_ID" ZIPY_IOS_DEBUGID_META_FILE="$ZIPY_IOS_DEBUGID_META_FILE" node <<'ZIPYEOF'
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+
35
+ const projectRoot = process.env.PROJECT_ROOT || '';
36
+ const bundleId = process.env.ZIPY_DEBUG_ID || '';
37
+ const metadataFile = process.env.ZIPY_IOS_DEBUGID_META_FILE || '';
38
+ const debugIdFiles = [
39
+ path.join(projectRoot, 'node_modules/zipy-react-native/lib/commonjs/utils/debugid.cjs'),
40
+ path.join(projectRoot, 'node_modules/zipyai-react-native/lib/commonjs/utils/debugid.cjs'),
41
+ ];
42
+ const content =
43
+ '"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.BUNDLE_ID=void 0;const BUNDLE_ID=exports.BUNDLE_ID="' +
44
+ bundleId +
45
+ '";';
46
+ const metadata = [];
47
+
48
+ for (const filePath of debugIdFiles) {
49
+ if (!fs.existsSync(filePath)) continue;
50
+ try {
51
+ const stats = fs.statSync(filePath);
52
+ const originalMode = stats.mode & 0o777;
53
+ // Some package managers leave files in node_modules read-only, so temporarily add owner write
54
+ // permission before rewriting debugid.cjs for this archive build.
55
+ if ((originalMode & 0o200) === 0) {
56
+ fs.chmodSync(filePath, originalMode | 0o200);
57
+ console.log('[zipy] temporarily enabled write permission for debugid.cjs:', filePath);
58
+ }
59
+ fs.writeFileSync(filePath, content, 'utf8');
60
+ console.log('[zipy] wrote bundle id into debugid.cjs before iOS bundling:', filePath);
61
+ metadata.push({ filePath, originalMode });
62
+ } catch (error) {
63
+ const message = error instanceof Error ? error.message : String(error);
64
+ console.warn('[zipy] could not write bundle id into debugid.cjs:', filePath, message);
65
+ }
66
+ }
67
+
68
+ if (metadataFile && metadata.length > 0) {
69
+ try {
70
+ fs.writeFileSync(metadataFile, JSON.stringify(metadata, null, 2), 'utf8');
71
+ } catch (error) {
72
+ const message = error instanceof Error ? error.message : String(error);
73
+ console.warn('[zipy] could not persist debugid.cjs metadata:', metadataFile, message);
74
+ }
75
+ }
76
+ ZIPYEOF
@@ -1,11 +1,337 @@
1
1
  #!/bin/sh
2
- # Zipy iOS upload hook — runs in Xcode Run Script Build Phase on every build/run.
3
- # Same role as zipy.gradle on Android: run Zipy task after bundle.
4
- # For now just logs; later can call: cd "$PROJECT_ROOT" && npx zipy-mobile-cli upload-sourcemaps release
5
- set -e
6
- echo "[Zipy] iOS upload task ran (ZipySourcemapsUpload)"
7
- # Optional: skip when env is set (mirror Android ZIPY_DISABLE_AUTO_UPLOAD)
8
- if [ -n "$ZIPY_DISABLE_AUTO_UPLOAD" ]; then
9
- echo "[Zipy] Skipped (ZIPY_DISABLE_AUTO_UPLOAD is set)"
2
+ # Zipy iOS upload hook — runs from an Xcode Run Script Build Phase.
3
+ # Keep the phase archive-focused, but only upload during archive/install actions so local builds stay quiet.
4
+ # We stage main.jsbundle and main.jsbundle.map into ios/build/zipy/... before calling the shared uploader.
5
+ set -eu
6
+
7
+ echo "[zipy] iOS archive upload phase started (ZipySourcemapsUpload)"
8
+
9
+ # Mirror Android's opt-out behavior so local builds can skip the network call.
10
+ if [ -n "${ZIPY_DISABLE_AUTO_UPLOAD:-}" ]; then
11
+ echo "[zipy] Skipped (ZIPY_DISABLE_AUTO_UPLOAD is set)"
10
12
  exit 0
11
13
  fi
14
+
15
+ # Match the archive upload flow: only upload the archive-generated JS bundle artifacts, not every simulator/device build.
16
+ if [ -z "${ARCHIVE_PATH:-}" ] && [ "${ACTION:-}" != "install" ]; then
17
+ echo "[zipy] Skipped (not an Xcode archive/install action)"
18
+ exit 0
19
+ fi
20
+
21
+ PROJECT_ROOT="${SRCROOT}/.."
22
+ cd "$PROJECT_ROOT"
23
+
24
+ ZIPY_IOS_OUTPUT_DIR="${PROJECT_ROOT}/ios/build/zipy/Sourcemaps"
25
+ ZIPY_IOS_SENTINEL_FILE="${ZIPY_IOS_OUTPUT_DIR}/zipy-debug-id.txt"
26
+ ZIPY_IOS_DEBUGID_META_FILE="${ZIPY_IOS_OUTPUT_DIR}/zipy-debugid-meta.json"
27
+ mkdir -p "$ZIPY_IOS_OUTPUT_DIR"
28
+
29
+ cleanup_debug_id_files() {
30
+ PROJECT_ROOT="$PROJECT_ROOT" ZIPY_IOS_DEBUGID_META_FILE="$ZIPY_IOS_DEBUGID_META_FILE" node <<'ZIPYEOF'
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+
34
+ const projectRoot = process.env.PROJECT_ROOT || '';
35
+ const metadataFile = process.env.ZIPY_IOS_DEBUGID_META_FILE || '';
36
+ const debugIdFiles = [
37
+ path.join(projectRoot, 'node_modules/zipy-react-native/lib/commonjs/utils/debugid.cjs'),
38
+ path.join(projectRoot, 'node_modules/zipyai-react-native/lib/commonjs/utils/debugid.cjs'),
39
+ ];
40
+ const emptyContent =
41
+ '"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.BUNDLE_ID=void 0;const BUNDLE_ID=exports.BUNDLE_ID="";';
42
+ const metadataByPath = new Map();
43
+
44
+ if (metadataFile && fs.existsSync(metadataFile)) {
45
+ try {
46
+ const parsed = JSON.parse(fs.readFileSync(metadataFile, 'utf8'));
47
+ if (Array.isArray(parsed)) {
48
+ for (const entry of parsed) {
49
+ if (!entry || typeof entry !== 'object') continue;
50
+ const filePath = entry.filePath;
51
+ const originalMode = entry.originalMode;
52
+ if (typeof filePath === 'string' && typeof originalMode === 'number') {
53
+ metadataByPath.set(filePath, originalMode);
54
+ }
55
+ }
56
+ }
57
+ } catch (error) {
58
+ const message = error instanceof Error ? error.message : String(error);
59
+ console.warn('[zipy] could not read debugid.cjs metadata:', metadataFile, message);
60
+ }
61
+ }
62
+
63
+ for (const filePath of debugIdFiles) {
64
+ if (!fs.existsSync(filePath)) continue;
65
+ try {
66
+ const currentMode = fs.statSync(filePath).mode & 0o777;
67
+ const originalMode = metadataByPath.get(filePath) ?? currentMode;
68
+ // Ensure cleanup can always rewrite debugid.cjs even if the package file was originally read-only.
69
+ if ((currentMode & 0o200) === 0) {
70
+ fs.chmodSync(filePath, currentMode | 0o200);
71
+ console.log('[zipy] temporarily enabled write permission for debugid.cjs cleanup:', filePath);
72
+ }
73
+ fs.writeFileSync(filePath, emptyContent, 'utf8');
74
+ if (originalMode !== (fs.statSync(filePath).mode & 0o777)) {
75
+ fs.chmodSync(filePath, originalMode);
76
+ console.log('[zipy] restored original permissions for debugid.cjs:', filePath);
77
+ }
78
+ console.log('[zipy] cleared debugid.cjs after iOS archive upload:', filePath);
79
+ } catch (error) {
80
+ const message = error instanceof Error ? error.message : String(error);
81
+ console.warn('[zipy] could not clear debugid.cjs after iOS archive upload:', filePath, message);
82
+ }
83
+ }
84
+
85
+ if (metadataFile && fs.existsSync(metadataFile)) {
86
+ try {
87
+ fs.unlinkSync(metadataFile);
88
+ console.log('[zipy] removed iOS debugid metadata:', metadataFile);
89
+ } catch (error) {
90
+ const message = error instanceof Error ? error.message : String(error);
91
+ console.warn('[zipy] could not remove iOS debugid metadata:', metadataFile, message);
92
+ }
93
+ }
94
+ ZIPYEOF
95
+
96
+ if [ -f "$ZIPY_IOS_SENTINEL_FILE" ]; then
97
+ rm -f "$ZIPY_IOS_SENTINEL_FILE"
98
+ echo "[zipy] removed iOS debug-id sentinel ${ZIPY_IOS_SENTINEL_FILE}"
99
+ fi
100
+ }
101
+
102
+ # Mirror Android cleanup semantics: once the archive path is done, revert debugid.cjs even if upload fails.
103
+ trap cleanup_debug_id_files EXIT
104
+
105
+ resolve_artifact_path() {
106
+ ARTIFACT_NAME="$1"
107
+ EXPLICIT_PATH="${2:-}"
108
+ EXTRA_DIRECT_PATH="${3:-}"
109
+
110
+ ARTIFACT_NAME="$ARTIFACT_NAME" \
111
+ EXPLICIT_PATH="$EXPLICIT_PATH" \
112
+ EXTRA_DIRECT_PATH="$EXTRA_DIRECT_PATH" \
113
+ PROJECT_ROOT="$PROJECT_ROOT" \
114
+ ZIPY_IOS_OUTPUT_DIR="$ZIPY_IOS_OUTPUT_DIR" \
115
+ TARGET_BUILD_DIR="${TARGET_BUILD_DIR:-}" \
116
+ CONFIGURATION_BUILD_DIR="${CONFIGURATION_BUILD_DIR:-}" \
117
+ BUILT_PRODUCTS_DIR="${BUILT_PRODUCTS_DIR:-}" \
118
+ BUILD_DIR="${BUILD_DIR:-}" \
119
+ DERIVED_FILE_DIR="${DERIVED_FILE_DIR:-}" \
120
+ UNLOCALIZED_RESOURCES_FOLDER_PATH="${UNLOCALIZED_RESOURCES_FOLDER_PATH:-}" \
121
+ WRAPPER_NAME="${WRAPPER_NAME:-}" \
122
+ ARCHIVE_PATH="${ARCHIVE_PATH:-}" \
123
+ PROJECT_DIR="${PROJECT_DIR:-}" \
124
+ CONFIGURATION="${CONFIGURATION:-}" \
125
+ EFFECTIVE_PLATFORM_NAME="${EFFECTIVE_PLATFORM_NAME:-}" \
126
+ HOME="${HOME:-}" \
127
+ node <<'NODE'
128
+ const fs = require('fs');
129
+ const path = require('path');
130
+
131
+ const artifactName = process.env.ARTIFACT_NAME;
132
+ const explicitPath = process.env.EXPLICIT_PATH || '';
133
+ const extraDirectPath = process.env.EXTRA_DIRECT_PATH || '';
134
+
135
+ function exists(candidate) {
136
+ if (!candidate) return false;
137
+ try {
138
+ fs.accessSync(candidate);
139
+ return true;
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ function pushUnique(list, seen, candidate) {
146
+ if (!candidate || seen.has(candidate)) return;
147
+ seen.add(candidate);
148
+ list.push(candidate);
149
+ }
150
+
151
+ function buildDerivedDataRoots(homeDir) {
152
+ if (!homeDir) return [];
153
+ const derivedDataRoot = path.join(homeDir, 'Library', 'Developer', 'Xcode', 'DerivedData');
154
+ if (!exists(derivedDataRoot)) return [];
155
+
156
+ const roots = [];
157
+ try {
158
+ for (const entry of fs.readdirSync(derivedDataRoot)) {
159
+ const buildRoot = path.join(derivedDataRoot, entry, 'Build');
160
+ if (!exists(buildRoot)) continue;
161
+ roots.push(buildRoot);
162
+ roots.push(path.join(buildRoot, 'Products'));
163
+ roots.push(path.join(buildRoot, 'Intermediates.noindex'));
164
+ }
165
+ } catch {
166
+ return [];
167
+ }
168
+ return roots;
169
+ }
170
+
171
+ function searchTree(root, targetName, maxDepth) {
172
+ if (!exists(root)) return null;
173
+ const queue = [{ dir: root, depth: 0 }];
174
+ const visited = new Set();
175
+
176
+ while (queue.length > 0) {
177
+ const current = queue.shift();
178
+ if (!current || visited.has(current.dir)) continue;
179
+ visited.add(current.dir);
180
+
181
+ let entries = [];
182
+ try {
183
+ entries = fs.readdirSync(current.dir, { withFileTypes: true });
184
+ } catch {
185
+ continue;
186
+ }
187
+
188
+ for (const entry of entries) {
189
+ const fullPath = path.join(current.dir, entry.name);
190
+ if (entry.isFile() && entry.name === targetName) {
191
+ return fullPath;
192
+ }
193
+ if (!entry.isDirectory() || current.depth >= maxDepth) continue;
194
+ // Stay focused on likely build output trees so archive uploads do not walk unrelated directories.
195
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
196
+ queue.push({ dir: fullPath, depth: current.depth + 1 });
197
+ }
198
+ }
199
+
200
+ return null;
201
+ }
202
+
203
+ if (exists(explicitPath)) {
204
+ console.log(explicitPath);
205
+ process.exit(0);
206
+ }
207
+
208
+ if (exists(extraDirectPath)) {
209
+ console.log(extraDirectPath);
210
+ process.exit(0);
211
+ }
212
+
213
+ const directCandidates = [];
214
+ const directSeen = new Set();
215
+ pushUnique(directCandidates, directSeen, path.join(process.env.ZIPY_IOS_OUTPUT_DIR || '', artifactName));
216
+ pushUnique(directCandidates, directSeen, path.join(process.env.TARGET_BUILD_DIR || '', process.env.UNLOCALIZED_RESOURCES_FOLDER_PATH || '', artifactName));
217
+ pushUnique(directCandidates, directSeen, path.join(process.env.TARGET_BUILD_DIR || '', process.env.WRAPPER_NAME || '', artifactName));
218
+ pushUnique(directCandidates, directSeen, path.join(process.env.CONFIGURATION_BUILD_DIR || '', artifactName));
219
+ pushUnique(directCandidates, directSeen, path.join(process.env.BUILT_PRODUCTS_DIR || '', artifactName));
220
+ pushUnique(directCandidates, directSeen, path.join(process.env.DERIVED_FILE_DIR || '', artifactName));
221
+ pushUnique(directCandidates, directSeen, path.join(process.env.PROJECT_DIR || '', 'build', 'zipy', `${process.env.CONFIGURATION || 'Release'}${process.env.EFFECTIVE_PLATFORM_NAME || ''}`, artifactName));
222
+
223
+ for (const candidate of directCandidates) {
224
+ if (exists(candidate)) {
225
+ console.log(candidate);
226
+ process.exit(0);
227
+ }
228
+ }
229
+
230
+ const searchRoots = [];
231
+ const rootSeen = new Set();
232
+ pushUnique(searchRoots, rootSeen, process.env.TARGET_BUILD_DIR || '');
233
+ pushUnique(searchRoots, rootSeen, process.env.CONFIGURATION_BUILD_DIR || '');
234
+ pushUnique(searchRoots, rootSeen, process.env.BUILT_PRODUCTS_DIR || '');
235
+ pushUnique(searchRoots, rootSeen, process.env.BUILD_DIR || '');
236
+ pushUnique(searchRoots, rootSeen, process.env.DERIVED_FILE_DIR || '');
237
+ pushUnique(searchRoots, rootSeen, path.join(process.env.PROJECT_ROOT || '', 'ios', 'build'));
238
+ if (process.env.ARCHIVE_PATH) {
239
+ pushUnique(searchRoots, rootSeen, path.join(process.env.ARCHIVE_PATH, 'Products'));
240
+ pushUnique(searchRoots, rootSeen, path.join(process.env.ARCHIVE_PATH, 'BuildProductsPath'));
241
+ }
242
+ for (const root of buildDerivedDataRoots(process.env.HOME || '')) {
243
+ pushUnique(searchRoots, rootSeen, root);
244
+ }
245
+
246
+ for (const root of searchRoots) {
247
+ const match = searchTree(root, artifactName, 6);
248
+ if (match) {
249
+ console.log(match);
250
+ process.exit(0);
251
+ }
252
+ }
253
+
254
+ process.exit(1);
255
+ NODE
256
+ }
257
+
258
+ if ! BUNDLE_SOURCE_PATH="$(resolve_artifact_path "main.jsbundle" "${ZIPY_BUNDLE_PATH:-}" "")"; then
259
+ echo "[zipy] Skipped (main.jsbundle not found)"
260
+ echo "[zipy] TARGET_BUILD_DIR=${TARGET_BUILD_DIR:-unset}"
261
+ echo "[zipy] CONFIGURATION_BUILD_DIR=${CONFIGURATION_BUILD_DIR:-unset}"
262
+ echo "[zipy] BUILT_PRODUCTS_DIR=${BUILT_PRODUCTS_DIR:-unset}"
263
+ echo "[zipy] BUILD_DIR=${BUILD_DIR:-unset}"
264
+ echo "[zipy] UNLOCALIZED_RESOURCES_FOLDER_PATH=${UNLOCALIZED_RESOURCES_FOLDER_PATH:-unset}"
265
+ echo "[zipy] ARCHIVE_PATH=${ARCHIVE_PATH:-unset}"
266
+ echo "[zipy] WRAPPER_NAME=${WRAPPER_NAME:-unset}"
267
+ exit 0
268
+ fi
269
+
270
+ if ! SOURCEMAP_SOURCE_PATH="$(resolve_artifact_path "main.jsbundle.map" "${ZIPY_SOURCEMAP_PATH:-}" "${SOURCEMAP_FILE:-}")"; then
271
+ echo "[zipy] Skipped (main.jsbundle.map not found)"
272
+ echo "[zipy] SOURCEMAP_FILE=${SOURCEMAP_FILE:-unset}"
273
+ echo "[zipy] DERIVED_FILE_DIR=${DERIVED_FILE_DIR:-unset}"
274
+ echo "[zipy] TARGET_BUILD_DIR=${TARGET_BUILD_DIR:-unset}"
275
+ echo "[zipy] CONFIGURATION_BUILD_DIR=${CONFIGURATION_BUILD_DIR:-unset}"
276
+ echo "[zipy] BUILT_PRODUCTS_DIR=${BUILT_PRODUCTS_DIR:-unset}"
277
+ echo "[zipy] BUILD_DIR=${BUILD_DIR:-unset}"
278
+ echo "[zipy] PROJECT_DIR=${PROJECT_DIR:-unset}"
279
+ exit 0
280
+ fi
281
+
282
+ STAGED_BUNDLE_PATH="${ZIPY_IOS_OUTPUT_DIR}/main.jsbundle"
283
+ STAGED_SOURCEMAP_PATH="${ZIPY_IOS_OUTPUT_DIR}/main.jsbundle.map"
284
+ ZIPY_DEBUG_ID_VALUE=""
285
+ if [ -f "$ZIPY_IOS_SENTINEL_FILE" ]; then
286
+ ZIPY_DEBUG_ID_VALUE="$(tr -d '\n' < "$ZIPY_IOS_SENTINEL_FILE")"
287
+ fi
288
+
289
+ copy_if_needed() {
290
+ SOURCE_PATH="$1"
291
+ DEST_PATH="$2"
292
+ if [ "$SOURCE_PATH" = "$DEST_PATH" ]; then
293
+ echo "[zipy] Reusing staged artifact at ${DEST_PATH}"
294
+ return 0
295
+ fi
296
+
297
+ # If the resolver already found the file inside ios/build/zipy/..., skip the redundant copy.
298
+ if [ -e "$DEST_PATH" ] && [ "$(cd "$(dirname "$SOURCE_PATH")" && pwd)/$(basename "$SOURCE_PATH")" = "$(cd "$(dirname "$DEST_PATH")" && pwd)/$(basename "$DEST_PATH")" ]; then
299
+ echo "[zipy] Reusing staged artifact at ${DEST_PATH}"
300
+ return 0
301
+ fi
302
+
303
+ cp -f "$SOURCE_PATH" "$DEST_PATH"
304
+ }
305
+
306
+ # Copy both artifacts into ios/build/zipy/... only when they are not already staged there.
307
+ copy_if_needed "$BUNDLE_SOURCE_PATH" "$STAGED_BUNDLE_PATH"
308
+ copy_if_needed "$SOURCEMAP_SOURCE_PATH" "$STAGED_SOURCEMAP_PATH"
309
+
310
+ APP_ID="${PRODUCT_BUNDLE_IDENTIFIER:-${BUNDLE_IDENTIFIER:-unknown}}"
311
+ MARKETING_VERSION="${MARKETING_VERSION:-1.0.0}"
312
+ BUILD_NUMBER="${CURRENT_PROJECT_VERSION:-0}"
313
+ RELEASE_VERSION="${APP_ID}@${MARKETING_VERSION}+${BUILD_NUMBER}"
314
+
315
+ echo "[zipy] Uploading archived iOS bundle + sourcemap"
316
+ echo "[zipy] archive=${ARCHIVE_PATH:-unknown}"
317
+ echo "[zipy] release=${RELEASE_VERSION}"
318
+ echo "[zipy] bundle source=${BUNDLE_SOURCE_PATH}"
319
+ echo "[zipy] sourcemap source=${SOURCEMAP_SOURCE_PATH}"
320
+ echo "[zipy] bundle path=${STAGED_BUNDLE_PATH}"
321
+ echo "[zipy] sourcemap path=${STAGED_SOURCEMAP_PATH}"
322
+ if [ -n "$ZIPY_DEBUG_ID_VALUE" ]; then
323
+ echo "[zipy] using ZIPY_DEBUG_ID from ${ZIPY_IOS_SENTINEL_FILE}"
324
+ else
325
+ echo "[zipy] no ZIPY_DEBUG_ID sentinel found; upload will fall back to ids already embedded in the source map"
326
+ fi
327
+
328
+ # Pass explicit env vars so the shared CLI can reuse the same upload command for Android maps and iOS archive JS artifacts.
329
+ ZIPY_PLATFORM=ios \
330
+ ZIPY_BUNDLE_PATH="$STAGED_BUNDLE_PATH" \
331
+ ZIPY_SOURCEMAP_PATH="$STAGED_SOURCEMAP_PATH" \
332
+ ZIPY_DEBUG_ID="$ZIPY_DEBUG_ID_VALUE" \
333
+ ZIPY_APP_ID="$APP_ID" \
334
+ ZIPY_BUNDLE_ID="$APP_ID" \
335
+ ZIPY_RELEASE="$RELEASE_VERSION" \
336
+ ZIPY_RELEASE_VERSION="$RELEASE_VERSION" \
337
+ npx zipy-mobile-cli upload-sourcemaps release
package/zipy.gradle CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Zipy Mobile CLI — Gradle integration (Sentry-style).
2
+ * Zipy Mobile CLI — Gradle integration.
3
3
  * Flow: Generate bundle id first -> write it into debugid.cjs (before Metro) -> Metro bundles -> inject id into source maps -> upload.
4
4
  */
5
5
  import org.apache.tools.ant.taskdefs.condition.Os
@@ -22,7 +22,7 @@ plugins.withId('com.android.application') {
22
22
  variant.buildConfigField "String", "ZIPY_BUNDLE_ID", "\"${variant.applicationId}\""
23
23
  }
24
24
  } catch (Throwable t) {
25
- logger.warn("Zipy: could not add BuildConfig.ZIPY_BUNDLE_ID: ${t.message}")
25
+ logger.warn("[zipy] could not add BuildConfig.ZIPY_BUNDLE_ID: ${t.message}")
26
26
  }
27
27
 
28
28
  def androidComponents = extensions.getByName("androidComponents")
@@ -86,15 +86,15 @@ plugins.withId('com.android.application') {
86
86
  try {
87
87
  def content = "\"use strict\";Object.defineProperty(exports,\"__esModule\",{value:!0}),exports.BUNDLE_ID=void 0;const BUNDLE_ID=exports.BUNDLE_ID=\"${zipyBundleId}\";"
88
88
  f.text = content
89
- logger.lifecycle("Zipy: wrote bundle id into ${f.name} (before Metro)")
89
+ logger.lifecycle("[zipy] wrote bundle id into ${f.name} (before Metro)")
90
90
  } catch (Throwable e) {
91
- logger.warn("Zipy: could not write bundle id into ${f.absolutePath}: ${e.message}")
91
+ logger.warn("[zipy] could not write bundle id into ${f.absolutePath}: ${e.message}")
92
92
  }
93
93
  } else {
94
- logger.info("Zipy: debugid.cjs not found at ${f.absolutePath}, skipping")
94
+ logger.info("[zipy] debugid.cjs not found at ${f.absolutePath}, skipping")
95
95
  }
96
96
  }
97
- logger.lifecycle("Zipy: generated bundle id and set in debugid.cjs; Metro will use it when bundling")
97
+ logger.lifecycle("[zipy] generated bundle id and set in debugid.cjs; Metro will use it when bundling")
98
98
  }
99
99
 
100
100
  def zipyInjectTask = tasks.register(zipyInjectTaskName) { t ->
@@ -104,13 +104,13 @@ plugins.withId('com.android.application') {
104
104
  t.outputs.file(mapFile).optional(true)
105
105
  t.doLast {
106
106
  if (!zipySentinelFile.exists()) {
107
- logger.warn("Zipy: sentinel not found (expected from bundle task doFirst)")
107
+ logger.warn("[zipy] sentinel not found (expected from bundle task doFirst)")
108
108
  return
109
109
  }
110
110
  def zipyBundleId = zipySentinelFile.text.trim()
111
111
  if (!zipyBundleId) return
112
112
  if (!mapFile.exists()) {
113
- logger.warn("Zipy: source map not found at ${mapFile.absolutePath}, skipping inject")
113
+ logger.warn("[zipy] source map not found at ${mapFile.absolutePath}, skipping inject")
114
114
  return
115
115
  }
116
116
  try {
@@ -129,9 +129,9 @@ plugins.withId('com.android.application') {
129
129
  json.zipyBundleId = zipyBundleId
130
130
  json.zipy_bundle_id = zipyBundleId
131
131
  mapFile.text = groovy.json.JsonOutput.toJson(json)
132
- logger.lifecycle("Zipy: injected Zipy bundle id into ${mapFile.name}")
132
+ logger.lifecycle("[zipy] injected Zipy bundle id into ${mapFile.name}")
133
133
  } catch (Throwable e) {
134
- logger.warn("Zipy: could not inject into source map: ${e.message}")
134
+ logger.warn("[zipy] could not inject into source map: ${e.message}")
135
135
  }
136
136
  def packagerPath = mapFile.absolutePath.replace("/generated/sourcemaps/", "/intermediates/sourcemaps/").replaceAll(/\\.map$/, ".packager.map")
137
137
  def packagerFile = new File(packagerPath)
@@ -176,7 +176,7 @@ plugins.withId('com.android.application') {
176
176
  def sentinelId = zipySentinelFile.text.trim()
177
177
  if (sentinelId) {
178
178
  environment("ZIPY_DEBUG_ID", sentinelId)
179
- logger.lifecycle("Zipy: passing ZIPY_DEBUG_ID from sentinel for upload")
179
+ logger.lifecycle("[zipy] passing ZIPY_DEBUG_ID from sentinel for upload")
180
180
  }
181
181
  }
182
182
  }
@@ -198,13 +198,13 @@ plugins.withId('com.android.application') {
198
198
  if (f.exists()) {
199
199
  try {
200
200
  f.text = emptyContent
201
- logger.lifecycle("Zipy: cleared debugid.cjs (${f.name}) after build")
201
+ logger.lifecycle("[zipy] cleared debugid.cjs (${f.name}) after build")
202
202
  } catch (Throwable e) {
203
- logger.warn("Zipy: could not clear ${f.absolutePath}: ${e.message}")
203
+ logger.warn("[zipy] could not clear ${f.absolutePath}: ${e.message}")
204
204
  }
205
205
  }
206
206
  }
207
- logger.lifecycle("Zipy upload done for ${releaseName}")
207
+ logger.lifecycle("[zipy] upload done for ${releaseName}")
208
208
  }
209
209
  }
210
210