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 +19 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +9 -6
- package/dist/init-react-native.d.ts +11 -3
- package/dist/init-react-native.js +170 -58
- package/dist/types.d.ts +8 -0
- package/dist/upload-sourcemaps.d.ts +6 -2
- package/dist/upload-sourcemaps.js +118 -36
- package/package.json +4 -19
- package/zipy-ios-prepare.sh +76 -0
- package/zipy-ios-upload.sh +334 -8
- package/zipy.gradle +14 -14
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
|
|
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`**
|
|
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
|
|
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
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
29
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
128
|
-
*
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
//
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
content = content
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
350
|
+
logInfo('Created config:', configPath);
|
|
239
351
|
patchGradle(projectRoot);
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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(`
|
|
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
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
321
|
+
const platform = getPlatform(process.env.ZIPY_PLATFORM);
|
|
250
322
|
const releaseName = process.env.ZIPY_RELEASE ?? process.env.ZIPY_RELEASE_VERSION ?? '';
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
package/zipy-ios-upload.sh
CHANGED
|
@@ -1,11 +1,337 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# Zipy iOS upload hook — runs
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
set -
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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("
|
|
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("
|
|
89
|
+
logger.lifecycle("[zipy] wrote bundle id into ${f.name} (before Metro)")
|
|
90
90
|
} catch (Throwable e) {
|
|
91
|
-
logger.warn("
|
|
91
|
+
logger.warn("[zipy] could not write bundle id into ${f.absolutePath}: ${e.message}")
|
|
92
92
|
}
|
|
93
93
|
} else {
|
|
94
|
-
logger.info("
|
|
94
|
+
logger.info("[zipy] debugid.cjs not found at ${f.absolutePath}, skipping")
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
logger.lifecycle("
|
|
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("
|
|
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("
|
|
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("
|
|
132
|
+
logger.lifecycle("[zipy] injected Zipy bundle id into ${mapFile.name}")
|
|
133
133
|
} catch (Throwable e) {
|
|
134
|
-
logger.warn("
|
|
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("
|
|
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("
|
|
201
|
+
logger.lifecycle("[zipy] cleared debugid.cjs (${f.name}) after build")
|
|
202
202
|
} catch (Throwable e) {
|
|
203
|
-
logger.warn("
|
|
203
|
+
logger.warn("[zipy] could not clear ${f.absolutePath}: ${e.message}")
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
|
-
logger.lifecycle("
|
|
207
|
+
logger.lifecycle("[zipy] upload done for ${releaseName}")
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
|