@contentsquare/react-native-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,48 @@
1
+ The Content Square React Native CLI License 1.0
2
+ (c) Content Square SAS 2025
3
+
4
+ By downloading, installing, implementing or otherwise using the Content Square
5
+ SAS React Native CLI (“the Contentsquare React Native CLI”) you agree to the
6
+ terms and conditions of this License Agreement.
7
+
8
+ Contentsquare grants you a limited, royalty-free, non-exclusive,
9
+ non-transferrable, non-sublicensable, revocable copyright license to use and
10
+ reproduce, without modification, the Contentsquare React Native CLI solely to enable the
11
+ use of Contentsquare with or in your React Native platform applications, subject to the
12
+ following conditions:
13
+
14
+ Other than as permitted in this agreement, you may not distribute, display,
15
+ copy, execute publicly, make available to the public, reduce to human readable
16
+ form, lease, resale, disassemble, decompile, adapt, sublicense, host as a
17
+ service, make other commercial use of, sell, rent, lend, process, compile,
18
+ reverse engineer, combine with other software, translate, modify, or create
19
+ derivative works of the Contentsquare React Native CLI.
20
+
21
+ Contentsquare has no obligation to enable you or any of your applications to
22
+ access, interact with, or retrieve or publish content to any Contentsquare
23
+ platform or service. However, Contentsquare may provide you with such platform
24
+ services on additional terms.
25
+
26
+ Contentsquare further has no obligation to provide support, maintenance,
27
+ upgrades, modifications, or new releases of the Contentsquare React Native CLI, unless
28
+ otherwise agreed in writing.
29
+
30
+ All other rights are reserved.
31
+
32
+ TO THE FULLEST EXTENT PERMITTED BY LAW, CONTENTSQUARE MAKES NO, AND HEREBY
33
+ DISCLAIMS ANY AND ALL, REPRESENTATIONS WARRANTIES, CONDITIONS AND ALL OTHER
34
+ TERMS OF ANY KIND WHATSOEVER WITH RESPECT TO THE CONTENTSQUARE REACT NATIVE CLI AND
35
+ RELATED DOCUMENTATION, OR ADDITIONAL SERVICES, WHETHER EXPRESS OR IMPLIED, ORAL
36
+ OR WRITTEN, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OF ACCURACY, QUALITY,
37
+ PERFORMANCE, MERCHANTABILITY, NONINFRINGEMENT AND FITNESS FOR A PARTICULAR
38
+ PURPOSE. FOR THE AVOIDANCE OF DOUBT, CONTENTSQUARE DOES NOT WARRANT THAT THE
39
+ CONTENTSQUARE REACT NATIVE CLI SHALL MEET YOUR NEEDS OR BE ERROR FREE. IN NO EVENT SHALL
40
+ CONTENTSQUARE BE LIABLE FOR ANY TYPE OF DAMAGES (INCLUDING, BUT NOT LIMITED TO,
41
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
42
+ BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
43
+ IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
44
+ ARISING IN ANY WAY OUT OF THE USE OF THE CONTENTSQUARE REACT NATIVE CLI, EVEN IF ADVISED
45
+ OF THE POSSIBILITY OF SUCH DAMAGE. YOU ASSUME SOLE RESPONSIBILITY FOR RESULTS
46
+ OBTAINED FROM THE USE OF THE CONTENTSQUARE REACT NATIVE CLI BY YOU. THE CONTENTSQUARE
47
+ REACT NATIVE CLI, SCRIPTS, OR ADDITIONAL SERVICES ARE PROVIDED TO YOU ON AN “AS IS”
48
+ BASIS.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @contentsquare/react-native-cli
2
+
3
+ A Command Line Interface (CLI) tool for Contentsquare used to upload mapping files for React Native apps.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ yarn add @contentsquare/react-native-cli --dev
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ The upload command requires the following information:
14
+
15
+ - **The targeted platform**
16
+
17
+ `ios` or `android`
18
+
19
+ - **The project ID**
20
+
21
+ 1. Login to the Contentsquare platform on https://app.contentsquare.com
22
+ 2. Make sure to be on the right project
23
+ 3. The project ID can be found in the URL query parameter `project`:<br/>`https://app.contentsquare.com/#/{MODULE_NAME}?project={PROJECT_ID}&hash={HASH}]`
24
+
25
+ - **Valid API credentials**
26
+
27
+ Follow the dedicated documentation from the Help Center to get the client ID and client Secret: [How to create API credentials](https://support.contentsquare.com/hc/en-gb/articles/9292880837660-How-to-create-API-credentials).
28
+
29
+ ### Using JSON file
30
+
31
+ You can provide this information by adding a `contentsquare-cli.json` file at the root of your project.
32
+
33
+ ```json
34
+ {
35
+ "android": {
36
+ "clientId": "[ANDROID CLIENT ID]",
37
+ "clientSecret": "[ANDROID CLIENT SECRET]",
38
+ "projectId": "[ANDROID PROJECT ID]",
39
+ "sourcemapPath": "[PATH TO SOURCEMAP IN ANDROID PROJECT]"
40
+ },
41
+ "ios": {
42
+ "clientId": "[IOS CLIENT ID]",
43
+ "clientSecret": "[IOS CLIENT SECRET]",
44
+ "projectId": "[IOS PROJECT ID]",
45
+ "sourcemapPath": "[PATH TO SOURCEMAP IN IOS PROJECT]"
46
+ }
47
+ }
48
+ ```
49
+
50
+ ### Using command line arguments
51
+
52
+ Alternatively, this information can be added as arguments directly on the command line
53
+
54
+ | Option | Description |
55
+ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
56
+ | `--client-id` | The client ID of [API credentials](https://support.contentsquare.com/hc/en-gb/articles/9292880837660-How-to-create-API-credentials) |
57
+ | `--client-secret` | The client secret of [API credentials](https://support.contentsquare.com/hc/en-gb/articles/9292880837660-How-to-create-API-credentials) |
58
+ | `--config` | The path to the configuration file relative to project root |
59
+ | `--help` | Display the help message |
60
+ | `--platform` | The platform to target: `ios` or `android` |
61
+ | `--project-id` | The project ID |
62
+ | `--sourcemap-path` | The path to the sourcemap file |
63
+ | `--upload-url` | The URL of the Contentsquare platform, default value: https://api.csq.io |
package/dist/auth.js ADDED
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.authenticate = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const logger_1 = require("./logger");
9
+ const authenticate = async (clientId, clientSecret, projectId, baseUrl) => {
10
+ try {
11
+ const endpoint = `${baseUrl}/v1/oauth/token`;
12
+ logger_1.log.verbose(`Authentication details\nendpoint: ${endpoint},\nclientId: ${clientId},\nprojectId: ${projectId}`);
13
+ const response = await axios_1.default.post(endpoint, {
14
+ client_id: clientId,
15
+ client_secret: clientSecret,
16
+ project_id: projectId,
17
+ grant_type: 'client_credentials',
18
+ scope: 'symbolicator',
19
+ });
20
+ return response.data;
21
+ }
22
+ catch (error) {
23
+ throw new Error(`Authentication failed: ${error}`);
24
+ }
25
+ };
26
+ exports.authenticate = authenticate;
package/dist/config.js ADDED
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createConfig = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const logger_1 = require("./logger");
10
+ /**
11
+ * Creates a configuration object based on the provided options and config file.
12
+ * @param {Options} options - The options object.
13
+ * @param {string} configPath - The path to the config file.
14
+ * @returns {Config} - The configuration object.
15
+ */
16
+ const createConfig = (options, configPath) => {
17
+ const configFile = configPath
18
+ ? path_1.default.resolve(configPath)
19
+ : path_1.default.resolve('./contentsquare-cli.json');
20
+ let configFileValues = { android: {}, ios: {} };
21
+ if (fs_1.default.existsSync(configFile)) {
22
+ logger_1.log.verbose(`Loading config file from ${configFile}`);
23
+ configFileValues = JSON.parse(fs_1.default.readFileSync(configFile).toString());
24
+ }
25
+ else {
26
+ logger_1.log.warn(`No config file found at: ${configFile}`);
27
+ }
28
+ const defaultUploadUrl = 'https://authentication.contentsquare.com';
29
+ const getConfigValue = (platform, key) => {
30
+ var _a, _b, _c;
31
+ return options.platform === platform
32
+ ? ((_a = options[key]) === null || _a === void 0 ? void 0 : _a.toString()) || ((_b = configFileValues[platform]) === null || _b === void 0 ? void 0 : _b[key])
33
+ : (_c = configFileValues[platform]) === null || _c === void 0 ? void 0 : _c[key];
34
+ };
35
+ const config = {
36
+ android: {
37
+ clientId: getConfigValue('android', 'clientId'),
38
+ clientSecret: getConfigValue('android', 'clientSecret'),
39
+ projectId: getConfigValue('android', 'projectId'),
40
+ sourcemapPath: getConfigValue('android', 'sourcemapPath'),
41
+ uploadUrl: getConfigValue('android', 'uploadUrl') || defaultUploadUrl,
42
+ },
43
+ ios: {
44
+ clientId: getConfigValue('ios', 'clientId'),
45
+ clientSecret: getConfigValue('ios', 'clientSecret'),
46
+ projectId: getConfigValue('ios', 'projectId'),
47
+ sourcemapPath: getConfigValue('ios', 'sourcemapPath'),
48
+ uploadUrl: getConfigValue('ios', 'uploadUrl') || defaultUploadUrl,
49
+ },
50
+ };
51
+ // Log the final configuration for debug purposes
52
+ logger_1.log.verbose('Configuration:\n', JSON.stringify(config, null, 2));
53
+ return config;
54
+ };
55
+ exports.createConfig = createConfig;
package/dist/index.js ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const auth_1 = require("./auth");
6
+ const config_1 = require("./config");
7
+ const logger_1 = require("./logger");
8
+ const sourcemap_1 = require("./sourcemap");
9
+ const upload_1 = require("./upload");
10
+ const utils_1 = require("./utils");
11
+ const packageJson = require('../package.json');
12
+ const { version } = packageJson;
13
+ const program = new commander_1.Command();
14
+ program
15
+ .version(version)
16
+ .description('CLI tool from Contentsquare to upload source maps for React Native apps');
17
+ program
18
+ .command('update-sourcemap-id')
19
+ .description('Update source map ID')
20
+ .option('--platform <platform>', 'Platform (ios or android)')
21
+ .option('--verbose', 'Enable verbose logging')
22
+ .option('--working-dir <workingDir>', 'Working directory, defaults to cwd')
23
+ .action(async (options) => {
24
+ if (options.verbose) {
25
+ (0, logger_1.setLogLevel)('verbose');
26
+ }
27
+ if (options.workingDir) {
28
+ process.chdir(options.workingDir);
29
+ logger_1.log.verbose(`Changed working directory to ${process.cwd()}`);
30
+ }
31
+ const platforms = (0, utils_1.getPlatformSelection)(options.platform);
32
+ (0, sourcemap_1.updateSourcemapId)(platforms);
33
+ });
34
+ program
35
+ .command('upload-sourcemaps')
36
+ .description('Upload source maps for React Native app')
37
+ .option('--client-id <clientId>', 'Client ID of API credentials')
38
+ .option('--client-secret <clientSecret>', 'Client secret API credentials')
39
+ .option('--config <config>', 'Path to the config file relative to the root of the project')
40
+ .option('--platform <platform>', 'Platform (ios or android)')
41
+ .option('--project-id <projectId>', 'Project ID')
42
+ .option('--sourcemap-path <sourcemapPath>', 'Path to the source map file')
43
+ .option('--upload-url <uploadUrl>', 'URL for API upload endpoint')
44
+ .option('--verbose', 'Enable verbose logging')
45
+ .option('--working-dir <workingDir>', 'Working directory, defaults to cwd')
46
+ .action(async (options) => {
47
+ if (options.verbose) {
48
+ (0, logger_1.setLogLevel)('verbose');
49
+ }
50
+ if (options.workingDir) {
51
+ process.chdir(options.workingDir);
52
+ logger_1.log.verbose(`Changed working directory to ${process.cwd()}`);
53
+ }
54
+ const config = (0, config_1.createConfig)(options, options.config);
55
+ const platforms = (0, utils_1.getPlatformSelection)(options.platform);
56
+ for (const platform of platforms) {
57
+ const platformConfig = config[platform];
58
+ if (platformConfig.clientId &&
59
+ platformConfig.clientSecret &&
60
+ platformConfig.projectId &&
61
+ platformConfig.sourcemapPath &&
62
+ platformConfig.uploadUrl) {
63
+ try {
64
+ logger_1.log.verbose(`Authenticating for ${platform} ...`);
65
+ const { access_token: token, endpoint, token_type: tokenType, } = await (0, auth_1.authenticate)(platformConfig.clientId, platformConfig.clientSecret, platformConfig.projectId, (0, utils_1.ensureHttps)(platformConfig.uploadUrl));
66
+ logger_1.log.verbose(`Authentication successful for ${platform}`);
67
+ const sourcemapId = (0, sourcemap_1.readSourcemapId)(platform);
68
+ logger_1.log.verbose(`Uploading source map for ${platform} ...`);
69
+ await (0, upload_1.uploadSourcemap)(platformConfig.sourcemapPath, platform, sourcemapId, tokenType, token, (0, utils_1.ensureHttps)(endpoint));
70
+ logger_1.log.success(`Source map with ID: ${sourcemapId} uploaded successfully for ${platform}`);
71
+ }
72
+ catch (error) {
73
+ logger_1.log.error(`Failed to upload source map for ${platform}, project ID: ${platformConfig.projectId}:`, error);
74
+ }
75
+ }
76
+ else {
77
+ logger_1.log.warn(`Missing required parameters for ${platform}, skipping upload.`);
78
+ }
79
+ }
80
+ });
81
+ program.parse(process.argv);
package/dist/logger.js ADDED
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.log = exports.setLogLevel = void 0;
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ let logLevel = 'info';
10
+ const setLogLevel = (level) => {
11
+ logLevel = level;
12
+ };
13
+ exports.setLogLevel = setLogLevel;
14
+ const logLevels = {
15
+ verbose: 0,
16
+ info: 1,
17
+ success: 2,
18
+ warn: 3,
19
+ error: 4,
20
+ };
21
+ const shouldLog = (level) => {
22
+ return logLevels[level] >= logLevels[logLevel];
23
+ };
24
+ const logMessage = (level, ...messages) => {
25
+ if (shouldLog(level)) {
26
+ const color = getColor(level);
27
+ console.log(color(`${level}`), ...messages);
28
+ }
29
+ };
30
+ const getColor = (level) => {
31
+ switch (level) {
32
+ case 'verbose':
33
+ return chalk_1.default.dim;
34
+ case 'info':
35
+ return chalk_1.default.blue;
36
+ case 'success':
37
+ return chalk_1.default.green;
38
+ case 'warn':
39
+ return chalk_1.default.yellow;
40
+ case 'error':
41
+ return chalk_1.default.red;
42
+ default:
43
+ return chalk_1.default.white;
44
+ }
45
+ };
46
+ exports.log = {
47
+ verbose: (...messages) => logMessage('verbose', ...messages),
48
+ info: (...messages) => logMessage('info', ...messages),
49
+ success: (...messages) => logMessage('success', ...messages),
50
+ warn: (...messages) => logMessage('warn', ...messages),
51
+ error: (...messages) => logMessage('error', ...messages),
52
+ };
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readSourcemapId = exports.saveSourcemapId = exports.updateSourcemapId = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const uuid_1 = require("uuid");
10
+ const logger_1 = require("./logger");
11
+ const SOURCEMAP_INFO_FILE = 'contentsquare-sourcemap-info.json';
12
+ const updateSourcemapId = (platforms) => {
13
+ for (const platform of platforms) {
14
+ const sourcemapId = (0, uuid_1.v4)();
15
+ try {
16
+ (0, exports.saveSourcemapId)(platform, sourcemapId);
17
+ logger_1.log.success(`Updated source map ID for platform ${platform}: ${sourcemapId}`);
18
+ }
19
+ catch (error) {
20
+ logger_1.log.error(error);
21
+ }
22
+ }
23
+ };
24
+ exports.updateSourcemapId = updateSourcemapId;
25
+ const saveSourcemapId = (platform, id) => {
26
+ const sourcemapInfoPath = path_1.default.join(process.cwd(), SOURCEMAP_INFO_FILE);
27
+ try {
28
+ let config = {};
29
+ if (fs_1.default.existsSync(sourcemapInfoPath)) {
30
+ config = JSON.parse(fs_1.default.readFileSync(sourcemapInfoPath).toString());
31
+ }
32
+ config[platform] = { id };
33
+ fs_1.default.writeFileSync(sourcemapInfoPath, JSON.stringify(config, null, 2));
34
+ logger_1.log.verbose(`Source map ID: ${id}\nsaved successfully for platform: ${platform}\nin path:${sourcemapInfoPath}`);
35
+ }
36
+ catch (error) {
37
+ throw new Error(`Failed to save source map ID for ${platform} in ${sourcemapInfoPath}: ${error}`);
38
+ }
39
+ };
40
+ exports.saveSourcemapId = saveSourcemapId;
41
+ const readSourcemapId = (platform) => {
42
+ var _a;
43
+ const sourcemapInfoPath = path_1.default.join(process.cwd(), SOURCEMAP_INFO_FILE);
44
+ if (!fs_1.default.existsSync(sourcemapInfoPath)) {
45
+ throw new Error(`Source map info file not found at path: ${sourcemapInfoPath}.\nPlease run 'update-sourcemap-id' command before bundling.`);
46
+ }
47
+ const config = JSON.parse(fs_1.default.readFileSync(sourcemapInfoPath).toString());
48
+ const sourcemapId = (_a = config[platform]) === null || _a === void 0 ? void 0 : _a.id;
49
+ if (!sourcemapId) {
50
+ throw new Error(`Source map ID for platform '${platform}' not found in the source map info file.`);
51
+ }
52
+ return sourcemapId;
53
+ };
54
+ exports.readSourcemapId = readSourcemapId;
package/dist/upload.js ADDED
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.uploadSourcemap = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const node_zlib_1 = require("node:zlib");
10
+ const path_1 = __importDefault(require("path"));
11
+ const logger_1 = require("./logger");
12
+ const uploadSourcemap = async (filePath, platform, sourcemapId, tokenType, authToken, baseUrl) => {
13
+ const endpoint = `${baseUrl}/v1/react-symbols/${sourcemapId}?platform=${platform}`;
14
+ const fileName = path_1.default.basename(filePath);
15
+ if (!fs_1.default.existsSync(filePath)) {
16
+ logger_1.log.error(`File ${filePath} not found`);
17
+ throw new Error(`File not found: ${filePath}`);
18
+ }
19
+ const gzippedFileName = `${fileName}.gz`;
20
+ logger_1.log.verbose(`Compressing file ${fileName} to ${gzippedFileName} ...`);
21
+ // Compress the file using gzip
22
+ const gzip = (0, node_zlib_1.createGzip)();
23
+ const source = fs_1.default.createReadStream(filePath);
24
+ const destination = fs_1.default.createWriteStream(gzippedFileName);
25
+ return new Promise((resolve, reject) => {
26
+ source
27
+ .pipe(gzip)
28
+ .pipe(destination)
29
+ .on('finish', async () => {
30
+ try {
31
+ logger_1.log.verbose(`Uploading compressed file ${gzippedFileName} to ${endpoint} ...`);
32
+ const gzippedFileStream = fs_1.default.createReadStream(gzippedFileName);
33
+ await axios_1.default.post(endpoint, gzippedFileStream, {
34
+ headers: {
35
+ 'Content-Encoding': 'gzip',
36
+ 'Content-Type': 'application/octet-stream',
37
+ Authorization: `${tokenType[0].toUpperCase() + tokenType.slice(1)} ${authToken}`,
38
+ },
39
+ });
40
+ // Clean up the gzipped file
41
+ fs_1.default.unlinkSync(gzippedFileName);
42
+ logger_1.log.verbose(`File ${gzippedFileName} uploaded and removed locally.`);
43
+ resolve();
44
+ }
45
+ catch (error) {
46
+ fs_1.default.unlinkSync(gzippedFileName);
47
+ logger_1.log.error(`Error uploading file ${gzippedFileName} to ${endpoint}`);
48
+ reject(new Error(`Error uploading file: ${error}`));
49
+ }
50
+ })
51
+ .on('error', (error) => {
52
+ logger_1.log.error(`Error compressing file ${fileName}: ${error.message}`);
53
+ reject(new Error(`Error compressing file: ${error.message}`));
54
+ });
55
+ });
56
+ };
57
+ exports.uploadSourcemap = uploadSourcemap;
package/dist/utils.js ADDED
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getPlatformSelection = exports.ensureHttps = void 0;
4
+ /**
5
+ * Adds https prefix to a URL if not already present.
6
+ * @param {string} url - The URL to check and modify.
7
+ * @returns {string} - The URL with https prefix.
8
+ */
9
+ const ensureHttps = (url) => {
10
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
11
+ return `https://${url}`;
12
+ }
13
+ return url;
14
+ };
15
+ exports.ensureHttps = ensureHttps;
16
+ /**
17
+ * Returns the exclusive platform selection based on the input, defaults to both platforms.
18
+ * @param {string} platform - The platform to check.
19
+ * @returns {Array<'ios' | 'android'>} - The platform selection.
20
+ */
21
+ const getPlatformSelection = (platform) => {
22
+ return platform ? [platform] : ['ios', 'android'];
23
+ };
24
+ exports.getPlatformSelection = getPlatformSelection;
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@contentsquare/react-native-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool from Contentsquare to upload source maps for React Native apps",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "contentsquare-cli": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "/README.md",
12
+ "/LICENSE",
13
+ "/dist/",
14
+ "/bin/",
15
+ "/scripts/"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "start": "node dist/index.js",
20
+ "pack": "yarn pack --out %s-v%v.tgz",
21
+ "test": "jest",
22
+ "publish": "yarn npm publish --access public",
23
+ "publish:beta": "yarn npm publish --access public --tag beta",
24
+ "prepack": "echo 'TODO: Make it call 'yarn test' before packing'",
25
+ "postinstall": "chmod +x ./scripts/contentsquare-xcode.sh && chmod +x ./scripts/ios-config.sh"
26
+ },
27
+ "dependencies": {
28
+ "axios": "^1.15.0",
29
+ "commander": "^7.2.0",
30
+ "fs": "0.0.2",
31
+ "path": "0.12.7",
32
+ "uuid": "10.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/jest": "^29.5.13",
36
+ "@types/node": "20.14.2",
37
+ "@types/uuid": "^9.0.8",
38
+ "jest": "^29.7.0",
39
+ "ts-jest": "^29.2.5",
40
+ "typescript": "^4.0.0"
41
+ }
42
+ }
@@ -0,0 +1,35 @@
1
+ #!/bin/bash
2
+ # This script is supposed to be invoked as part of Xcode build process
3
+ # and relies on environment variables (including PWD) set by Xcode
4
+
5
+ # Print commands before executing them (useful for troubleshooting)
6
+ set -x -e
7
+
8
+ # Path to contentsquare-cli folder inside node_modules
9
+ CONTENTSQUARE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
10
+
11
+ # Path to react-native folder inside node_modules
12
+ REACT_NATIVE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../react-native" && pwd)"
13
+
14
+ # Read the path to Contentsquare cli config file, default is ./contentsquare-cli.json
15
+ CONTENTSQUARE_CONFIG_PATH=${CONTENTSQUARE_CONFIG_PATH:-"./contentsquare-cli.json"}
16
+
17
+ # check and assign NODE_BINARY env
18
+ source "$REACT_NATIVE_DIR/scripts/node-binary.sh"
19
+
20
+ # Define the path to react-native-xcode.sh
21
+ REACT_NATIVE_XCODE="$REACT_NATIVE_DIR/scripts/react-native-xcode.sh"
22
+
23
+ # Change directory to React Native project root directory, so contentsquare-cli can be called from proper location
24
+ cd ..
25
+ echo "Changed directory to: $(pwd)"
26
+
27
+ # Pre-build step: Any specific tasks you need to run before react-native-xcode.sh
28
+ "$NODE_BINARY" ./node_modules/.bin/contentsquare-cli update-sourcemap-id --platform ios
29
+
30
+ /bin/sh -c "$REACT_NATIVE_XCODE"
31
+
32
+ # Post-build step: Any tasks you want to execute after react-native-xcode.sh
33
+ "$NODE_BINARY" ./node_modules/.bin/contentsquare-cli upload-sourcemaps --config "$CONTENTSQUARE_CONFIG_PATH" --platform ios
34
+
35
+ /bin/sh -c "$CONTENTSQUARE_DIR/scripts/ios-config.sh"
@@ -0,0 +1,33 @@
1
+ import groovy.json.JsonSlurper
2
+
3
+ // Apply the local source-map.gradle file as a script
4
+ apply from: "$rootDir/../node_modules/@contentsquare/react-native-cli/scripts/source-maps.gradle"
5
+ println "Source maps gradle script applied."
6
+
7
+ android.applicationVariants.all { variant ->
8
+ def flavorName = variant.flavorName?.capitalize() ?: "NoFlavor"
9
+ def buildTypeName = variant.buildType.name.capitalize()
10
+ def buildVariant = "${flavorName}${buildTypeName}"
11
+
12
+ // Determine contentsquareConfigPath, log both custom and default paths
13
+ def contentsquareConfigPath = project.hasProperty("contentsquareConfigPath${buildVariant}") ?
14
+ project["contentsquareConfigPath${buildVariant}"] :
15
+ project.contentsquareConfigPath
16
+
17
+ def contentsquareConfigPathAdjusted = "../${contentsquareConfigPath}"
18
+ println "Using Contentsquare config path for ${buildVariant}: ${contentsquareConfigPathAdjusted}"
19
+
20
+ // Check if contentsquareConfigPathAdjusted is defined
21
+ if (contentsquareConfigPathAdjusted == null) {
22
+ println "ERROR: Contentsquare config path is not defined for ${buildVariant}."
23
+ return
24
+ }
25
+
26
+ def buildType = variant.buildType
27
+ def productFlavor = variant.productFlavors.isEmpty() ? null : variant.productFlavors[0]
28
+
29
+ def crashExtension = productFlavor?.extensions?.findByName("errorAnalysisCrash")
30
+ ?: buildType?.extensions?.findByName("errorAnalysisCrash")
31
+
32
+ crashExtension?.reactNativeCredentialsFile { file(contentsquareConfigPathAdjusted) }
33
+ }
@@ -0,0 +1,29 @@
1
+ #!/bin/bash
2
+
3
+ # Print commands before executing them (useful for troubleshooting)
4
+ set -x -e
5
+
6
+ # Read the path to Contentsquare CLI config file, default is ./contentsquare-cli.json
7
+ CONTENTSQUARE_CONFIG_PATH=${CONTENTSQUARE_CONFIG_PATH:-"./contentsquare-cli.json"}
8
+
9
+ # TODO: Have to use this workaround that depends on env var, upload-symbols script does not support --url argument
10
+ CONTENTSQUARE_STAGING=${CONTENTSQUARE_STAGING:-"false"}
11
+
12
+ extract_ios_json_value() {
13
+ local key=$1
14
+ local file=$2
15
+ grep -A 10 '"ios": {' "$file" | grep -Eo "\"$key\": \".*\"|\"$key\": [0-9]+" | sed -E "s/\"$key\": \"(.*)\"/\1/" | sed -E "s/\"$key\": ([0-9]+)/\1/"
16
+ }
17
+
18
+ # Extract values from the "ios" object in the JSON file
19
+ CLIENT_ID=$(extract_ios_json_value "clientId" "$CONTENTSQUARE_CONFIG_PATH")
20
+ CLIENT_SECRET=$(extract_ios_json_value "clientSecret" "$CONTENTSQUARE_CONFIG_PATH")
21
+ PROJECT_ID=$(extract_ios_json_value "projectId" "$CONTENTSQUARE_CONFIG_PATH")
22
+
23
+ # Contentsquare script for iOS symbol upload
24
+ # --staging requires you to be connected to the VPN
25
+ ${SRCROOT}/upload-symbols ${DWARF_DSYM_FOLDER_PATH} \
26
+ --project-id $PROJECT_ID \
27
+ --client-id $CLIENT_ID \
28
+ --client-secret $CLIENT_SECRET \
29
+ $( [ "$CONTENTSQUARE_STAGING" = "true" ] && echo "--staging" )
@@ -0,0 +1,223 @@
1
+ import org.apache.tools.ant.taskdefs.condition.Os
2
+
3
+ afterEvaluate {
4
+ def androidExtension = extensions.findByName("android")
5
+
6
+ if (androidExtension == null || androidExtension.applicationVariants == null) {
7
+ throw new GradleException("Android extension is missing or not running in the application module. Ensure 'contentsquare.gradle' is applied in the correct module, typically in 'android/app/build.gradle'.")
8
+ }
9
+
10
+ androidExtension.applicationVariants.all { variant ->
11
+ def flavorName = variant.flavorName.capitalize()
12
+ def buildTypeName = variant.buildType.name.capitalize()
13
+ def buildVariant = "${flavorName}${buildTypeName}"
14
+
15
+ println "Processing Build Variant: ${buildVariant}"
16
+
17
+ def reactConfig = getReactConfig(buildDir, variant)
18
+ def targetName = reactConfig["targetName"]
19
+ def targetPath = reactConfig["targetPath"]
20
+ def reactRoot = file(reactConfig.root)
21
+
22
+ def bundleTask = tasks.findByName(reactConfig.bundleTaskName)
23
+
24
+ if (bundleTask == null) {
25
+ logger.info("No JS bundle task found for variant=${targetName}.")
26
+ return
27
+ }
28
+ if (!bundleTask.enabled) {
29
+ logger.info("JS bundle task for variant=${targetName} is disabled.")
30
+ return
31
+ }
32
+
33
+ def serviceName = getServiceName(variant)
34
+ logger.info("Using service name=${serviceName} and version=${variant.versionName} for upload.")
35
+
36
+ def bundleAssetName = reactConfig.bundleAssetName
37
+ def sourceMapsDirectory = file("$buildDir/generated/sourcemaps/react/${targetPath}")
38
+ def sourceMapFile = file("$sourceMapsDirectory/${bundleAssetName}.map")
39
+
40
+ def updateTask = tasks.register("update${targetName}SourceMapsID", Exec) {
41
+ group = "contentsquare"
42
+ description = "Updates the source maps ID for the build."
43
+
44
+ workingDir = file(reactConfig.root)
45
+ commandLine "${getContentsquareCLIExecPath(reactConfig)}",
46
+ "update-sourcemap-id",
47
+ "--platform",
48
+ "android",
49
+ "--verbose",
50
+ "--working-dir",
51
+ reactConfig.root
52
+ }
53
+
54
+ def uploadTask = tasks.register("upload${targetName}SourceMaps") {
55
+ group = "contentsquare"
56
+ description = "Uploads source maps to Contentsquare."
57
+
58
+ // Declare inputs so Gradle knows this task depends on these files
59
+ inputs.file(sourceMapFile)
60
+ inputs.files(reactConfig.bundleFileResolver)
61
+
62
+ doLast {
63
+ def defaultConfigPath = project.ext.contentsquareConfigPath
64
+
65
+ def specificConfigKey = "contentsquareConfigPath${buildVariant}"
66
+ def specificConfigPath = project.ext.has(specificConfigKey) ? project.ext[specificConfigKey] : null
67
+
68
+ def resolvedConfigPath = specificConfigPath ? "$rootDir/${specificConfigPath}" : "$rootDir/${defaultConfigPath}"
69
+ println "Using config path for ${buildVariant}: ${resolvedConfigPath}"
70
+
71
+ def jsBundleFile = reactConfig.bundleFileResolver()
72
+ if (jsBundleFile == null || !sourceMapFile.exists()) {
73
+ throw new GradleException("Source maps or JS bundle file is missing; upload aborted.")
74
+ }
75
+
76
+ def cliPath = getContentsquareCLIExecPath(reactConfig).toString()
77
+ def configPath = resolvedConfigPath.toString()
78
+ def rootPath = reactConfig.root.toString()
79
+
80
+ def command = new ArrayList<String>()
81
+ command.add(cliPath)
82
+ command.add("upload-sourcemaps")
83
+ command.add("--config")
84
+ command.add(configPath)
85
+ command.add("--platform")
86
+ command.add("android")
87
+ command.add("--verbose")
88
+ command.add("--working-dir")
89
+ command.add(rootPath)
90
+
91
+ def processBuilder = new ProcessBuilder(command)
92
+ processBuilder.directory(reactRoot)
93
+ processBuilder.redirectErrorStream(true)
94
+
95
+ def process = processBuilder.start()
96
+ process.inputStream.eachLine { line -> println line }
97
+
98
+ def exitCode = process.waitFor()
99
+ if (exitCode != 0) {
100
+ throw new GradleException("Upload failed with exit code ${exitCode}")
101
+ } else {
102
+ println "Source maps uploaded successfully for variant=${buildVariant}."
103
+ }
104
+ }
105
+ }
106
+
107
+ bundleTask.dependsOn updateTask
108
+ bundleTask.finalizedBy uploadTask
109
+
110
+ uploadTask.configure {
111
+ mustRunAfter bundleTask
112
+ }
113
+ }
114
+ }
115
+
116
+ private def getContentsquareCLIExecPath(reactConfig) {
117
+ def defaultPath = "${reactConfig.root}/node_modules/.bin/contentsquare-cli"
118
+
119
+ def envPath = System.getenv('CONTENTSQUARE_CLI_EXEC')
120
+ if (envPath && isExecutable(envPath)) {
121
+ return envPath
122
+ } else if (envPath) {
123
+ logger.warn("Invalid CONTENTSQUARE_CLI_EXEC path. Falling back to default.", true)
124
+ }
125
+
126
+ def nodeExecutable = Os.isFamily(Os.FAMILY_WINDOWS) ? 'node.exe' : 'node'
127
+ def nodeScript = '''
128
+ const path = require('path');
129
+ const modulePath = require.resolve('@contentsquare/react-native-cli/package.json');
130
+ const cliPath = path.resolve(path.dirname(modulePath), '../../.bin/contentsquare-cli');
131
+ console.log(cliPath);
132
+ '''
133
+
134
+ return executeNodeScript(nodeExecutable, nodeScript) ?: defaultPath
135
+ }
136
+
137
+ private def executeNodeScript(String executable, String script) {
138
+ try {
139
+ def process = new ProcessBuilder(executable)
140
+ .redirectErrorStream(true)
141
+ .start()
142
+
143
+ process.outputStream.withWriter { it.write(script) }
144
+
145
+ def output = process.inputStream.text.trim()
146
+ process.waitFor()
147
+ return output
148
+ } catch (Exception e) {
149
+ logger.error("Failed to execute Node.js script: ${e.message}")
150
+ return null
151
+ }
152
+ }
153
+
154
+ private def isExecutable(String path) {
155
+ def file = new File(path)
156
+ return file.exists() && file.canExecute()
157
+ }
158
+
159
+ private def getReactConfig(File buildDir, variant) {
160
+ def config = [:]
161
+
162
+ if (project.extensions.findByName("react")) {
163
+ def bundleAssetName = project.extensions.react.bundleAssetName.get() ?: "index.android.bundle"
164
+ def targetName = variant.name.capitalize()
165
+ config.bundleTaskName = "createBundle${targetName}JsAndAssets"
166
+ config.bundleFileResolver = resolveBundleFile([
167
+ "$buildDir/generated/assets/react/${variant.name}",
168
+ "$buildDir/ASSETS/createBundle${targetName}JsAndAssets",
169
+ "$buildDir/generated/assets/createBundle${targetName}JsAndAssets"
170
+ ], bundleAssetName)
171
+ config.bundleAssetName = bundleAssetName
172
+ config.targetPath = variant.name
173
+ config.targetName = targetName
174
+ config.root = project.extensions.react.root.get() ?: "../../"
175
+ } else {
176
+ def targetName = variant.name.capitalize()
177
+ def targetPath = variant.dirName
178
+ config.bundleTaskName = "bundle${targetName}JsAndAssets"
179
+ config.bundleFileResolver = resolveBundleFile(["$buildDir/generated/assets/react/${targetPath}"], "index.android.bundle")
180
+ config.targetPath = targetPath
181
+ config.targetName = targetName
182
+ config.root = "../../"
183
+ }
184
+
185
+ return config
186
+ }
187
+
188
+ private def resolveBundleFile(List<String> directories, String fileName) {
189
+ return {
190
+ directories.collect { file("$it/$fileName") }.find { it.exists() }
191
+ }
192
+ }
193
+
194
+ private def runShellCommand(List<String> command, File workingDir) {
195
+ def outputStream = new ByteArrayOutputStream()
196
+ def errorStream = new ByteArrayOutputStream()
197
+
198
+ try {
199
+ def result = project.exec {
200
+ workingDir = workingDir
201
+ standardOutput = outputStream
202
+ errorOutput = errorStream
203
+ commandLine command
204
+ }
205
+
206
+ if (result.exitValue != 0) {
207
+ logger.error("Error: ${errorStream.toString('UTF-8')}")
208
+ result.rethrowFailure()
209
+ } else {
210
+ logger.lifecycle("Success: ${outputStream.toString('UTF-8')}")
211
+ }
212
+ } catch (Exception e) {
213
+ logger.error("Command execution failed: ${e.message}")
214
+ throw e
215
+ }
216
+ }
217
+
218
+ private def getServiceName(variant) {
219
+ if (project.hasProperty("contentsquare") && project.contentsquare.serviceName) {
220
+ return project.contentsquare.serviceName
221
+ }
222
+ return variant.applicationId
223
+ }