@luciq/react-native 18.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/CHANGELOG.md +5 -0
- package/FONTS_SETUP_GUIDE.md +521 -0
- package/Gemfile +1 -0
- package/Gemfile.lock +11 -0
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/RNLuciq.podspec +21 -0
- package/android/build.gradle +88 -0
- package/android/gradle.properties +4 -0
- package/android/jacoco.gradle +52 -0
- package/android/native.gradle +7 -0
- package/android/proguard-rules.txt +1 -0
- package/android/sourcemaps.gradle +255 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/ai/luciq/reactlibrary/ArgsRegistry.java +278 -0
- package/android/src/main/java/ai/luciq/reactlibrary/Constants.java +20 -0
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciq.java +328 -0
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqAPMModule.java +392 -0
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqBugReportingModule.java +444 -0
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqCrashReportingModule.java +169 -0
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqFeatureRequestsModule.java +98 -0
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqNetworkLoggerModule.java +195 -0
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativeModule.java +1611 -0
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativePackage.java +41 -0
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqRepliesModule.java +298 -0
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqSessionReplayModule.java +213 -0
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqSurveysModule.java +237 -0
- package/android/src/main/java/ai/luciq/reactlibrary/utils/ArrayUtil.java +167 -0
- package/android/src/main/java/ai/luciq/reactlibrary/utils/EventEmitterModule.java +35 -0
- package/android/src/main/java/ai/luciq/reactlibrary/utils/LuciqUtil.java +58 -0
- package/android/src/main/java/ai/luciq/reactlibrary/utils/MainThreadHandler.java +13 -0
- package/android/src/main/java/ai/luciq/reactlibrary/utils/MapUtil.java +171 -0
- package/android/src/main/java/ai/luciq/reactlibrary/utils/RNTouchedViewExtractor.java +167 -0
- package/android/src/main/java/ai/luciq/reactlibrary/utils/ReportUtil.java +67 -0
- package/app.plugin.js +1 -0
- package/babel.config.js +3 -0
- package/bin/commands/MigrateCommand.d.ts +6 -0
- package/bin/commands/UploadEasUpdatesSourcemaps.d.ts +2 -0
- package/bin/commands/UploadSoFiles.d.ts +6 -0
- package/bin/commands/UploadSourcemaps.d.ts +2 -0
- package/bin/config/migration-config.json +125 -0
- package/bin/index.d.ts +2 -0
- package/bin/index.js +19179 -0
- package/bin/upload/index.d.ts +4 -0
- package/bin/upload/migrate.d.ts +14 -0
- package/bin/upload/uploadEasUpdatesSourcemaps.d.ts +21 -0
- package/bin/upload/uploadSoFiles.d.ts +21 -0
- package/bin/upload/uploadSourcemaps.d.ts +21 -0
- package/cli/commands/MigrateCommand.ts +32 -0
- package/cli/commands/UploadEasUpdatesSourcemaps.ts +34 -0
- package/cli/commands/UploadSoFiles.ts +38 -0
- package/cli/commands/UploadSourcemaps.ts +40 -0
- package/cli/config/migration-config.json +125 -0
- package/cli/index.ts +21 -0
- package/cli/upload/index.ts +4 -0
- package/cli/upload/migrate.ts +271 -0
- package/cli/upload/uploadEasUpdatesSourcemaps.ts +74 -0
- package/cli/upload/uploadSoFiles.ts +112 -0
- package/cli/upload/uploadSourcemaps.ts +73 -0
- package/dangerfile.ts +44 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/models/FeatureFlag.d.ts +11 -0
- package/dist/models/FeatureFlag.js +1 -0
- package/dist/models/LuciqConfig.d.ts +42 -0
- package/dist/models/LuciqConfig.js +1 -0
- package/dist/models/NonFatalOptions.d.ts +15 -0
- package/dist/models/NonFatalOptions.js +1 -0
- package/dist/models/OverAirUpdate.d.ts +12 -0
- package/dist/models/OverAirUpdate.js +1 -0
- package/dist/models/Report.d.ts +70 -0
- package/dist/models/Report.js +109 -0
- package/dist/models/ReproConfig.d.ts +27 -0
- package/dist/models/ReproConfig.js +1 -0
- package/dist/models/SessionMetadata.d.ts +55 -0
- package/dist/models/SessionMetadata.js +1 -0
- package/dist/models/ThemeConfig.d.ts +27 -0
- package/dist/models/ThemeConfig.js +1 -0
- package/dist/models/W3cExternalTraceAttributes.d.ts +22 -0
- package/dist/models/W3cExternalTraceAttributes.js +1 -0
- package/dist/modules/APM.d.ts +77 -0
- package/dist/modules/APM.js +104 -0
- package/dist/modules/BugReporting.d.ts +138 -0
- package/dist/modules/BugReporting.js +202 -0
- package/dist/modules/CrashReporting.d.ts +19 -0
- package/dist/modules/CrashReporting.js +40 -0
- package/dist/modules/FeatureRequests.d.ts +20 -0
- package/dist/modules/FeatureRequests.js +28 -0
- package/dist/modules/Luciq.d.ts +362 -0
- package/dist/modules/Luciq.js +797 -0
- package/dist/modules/NetworkLogger.d.ts +52 -0
- package/dist/modules/NetworkLogger.js +208 -0
- package/dist/modules/Replies.d.ts +78 -0
- package/dist/modules/Replies.js +121 -0
- package/dist/modules/SessionReplay.d.ts +78 -0
- package/dist/modules/SessionReplay.js +98 -0
- package/dist/modules/Surveys.d.ts +75 -0
- package/dist/modules/Surveys.js +101 -0
- package/dist/native/NativeAPM.d.ts +18 -0
- package/dist/native/NativeAPM.js +4 -0
- package/dist/native/NativeBugReporting.d.ts +32 -0
- package/dist/native/NativeBugReporting.js +10 -0
- package/dist/native/NativeConstants.d.ts +182 -0
- package/dist/native/NativeConstants.js +1 -0
- package/dist/native/NativeCrashReporting.d.ts +18 -0
- package/dist/native/NativeCrashReporting.js +2 -0
- package/dist/native/NativeFeatureRequests.d.ts +8 -0
- package/dist/native/NativeFeatureRequests.js +2 -0
- package/dist/native/NativeLuciq.d.ts +86 -0
- package/dist/native/NativeLuciq.js +10 -0
- package/dist/native/NativeNetworkLogger.d.ts +21 -0
- package/dist/native/NativeNetworkLogger.js +14 -0
- package/dist/native/NativePackage.d.ts +21 -0
- package/dist/native/NativePackage.js +2 -0
- package/dist/native/NativeReplies.d.ts +21 -0
- package/dist/native/NativeReplies.js +8 -0
- package/dist/native/NativeSessionReplay.d.ts +16 -0
- package/dist/native/NativeSessionReplay.js +8 -0
- package/dist/native/NativeSurveys.d.ts +22 -0
- package/dist/native/NativeSurveys.js +9 -0
- package/dist/utils/AppStatesHandler.d.ts +3 -0
- package/dist/utils/AppStatesHandler.js +16 -0
- package/dist/utils/Enums.d.ts +244 -0
- package/dist/utils/Enums.js +266 -0
- package/dist/utils/FeatureFlags.d.ts +7 -0
- package/dist/utils/FeatureFlags.js +24 -0
- package/dist/utils/LuciqConstants.d.ts +14 -0
- package/dist/utils/LuciqConstants.js +15 -0
- package/dist/utils/LuciqUtils.d.ts +97 -0
- package/dist/utils/LuciqUtils.js +301 -0
- package/dist/utils/UnhandledRejectionTracking.d.ts +9 -0
- package/dist/utils/UnhandledRejectionTracking.js +99 -0
- package/dist/utils/XhrNetworkInterceptor.d.ts +39 -0
- package/dist/utils/XhrNetworkInterceptor.js +253 -0
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.js +6 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.js +39 -0
- package/expo.d.ts +1 -0
- package/expo.js +1 -0
- package/ios/RNLuciq/ArgsRegistry.h +32 -0
- package/ios/RNLuciq/ArgsRegistry.m +276 -0
- package/ios/RNLuciq/LuciqAPMBridge.h +26 -0
- package/ios/RNLuciq/LuciqAPMBridge.m +99 -0
- package/ios/RNLuciq/LuciqBugReportingBridge.h +60 -0
- package/ios/RNLuciq/LuciqBugReportingBridge.m +241 -0
- package/ios/RNLuciq/LuciqCrashReportingBridge.h +18 -0
- package/ios/RNLuciq/LuciqCrashReportingBridge.m +68 -0
- package/ios/RNLuciq/LuciqFeatureRequestsBridge.h +30 -0
- package/ios/RNLuciq/LuciqFeatureRequestsBridge.m +61 -0
- package/ios/RNLuciq/LuciqNetworkLoggerBridge.h +44 -0
- package/ios/RNLuciq/LuciqNetworkLoggerBridge.m +206 -0
- package/ios/RNLuciq/LuciqReactBridge.h +151 -0
- package/ios/RNLuciq/LuciqReactBridge.m +548 -0
- package/ios/RNLuciq/LuciqRepliesBridge.h +40 -0
- package/ios/RNLuciq/LuciqRepliesBridge.m +80 -0
- package/ios/RNLuciq/LuciqSessionReplayBridge.h +32 -0
- package/ios/RNLuciq/LuciqSessionReplayBridge.m +107 -0
- package/ios/RNLuciq/LuciqSurveysBridge.h +46 -0
- package/ios/RNLuciq/LuciqSurveysBridge.m +107 -0
- package/ios/RNLuciq/RCTConvert+LuciqEnums.h +18 -0
- package/ios/RNLuciq/RCTConvert+LuciqEnums.m +127 -0
- package/ios/RNLuciq/RNLuciq.h +35 -0
- package/ios/RNLuciq/RNLuciq.m +107 -0
- package/ios/RNLuciq/Util/LCQAPM+PrivateAPIs.h +15 -0
- package/ios/RNLuciq/Util/LCQCrashReporting+CP.h +13 -0
- package/ios/RNLuciq/Util/LCQNetworkLogger+CP.h +68 -0
- package/ios/RNLuciq/Util/Luciq+CP.h +12 -0
- package/ios/RNLuciq.xcodeproj/project.pbxproj +352 -0
- package/ios/native.rb +12 -0
- package/ios/sourcemaps.sh +120 -0
- package/migrate.js +569 -0
- package/package.json +92 -0
- package/plugin/build/index.js +42078 -0
- package/plugin/src/index.ts +5 -0
- package/plugin/src/pluginProps.ts +6 -0
- package/plugin/src/withLuciq.ts +51 -0
- package/plugin/src/withLuciqAndroid.ts +99 -0
- package/plugin/src/withLuciqIOS.ts +109 -0
- package/plugin/tsconfig.json +7 -0
- package/react-native.config.js +16 -0
- package/scripts/customize-ios-endpoints.sh +28 -0
- package/scripts/dream-11-delete-unused-features.sh +62 -0
- package/scripts/find-token.js +58 -0
- package/scripts/find-token.sh +70 -0
- package/scripts/notify-github.sh +15 -0
- package/scripts/replace.js +58 -0
- package/scripts/snapshot-comment.md +15 -0
- package/scripts/snapshot-version.sh +11 -0
- package/src/index.ts +40 -0
- package/src/models/FeatureFlag.ts +12 -0
- package/src/models/LuciqConfig.ts +48 -0
- package/src/models/NonFatalOptions.ts +16 -0
- package/src/models/OverAirUpdate.ts +14 -0
- package/src/models/Report.ts +124 -0
- package/src/models/ReproConfig.ts +31 -0
- package/src/models/SessionMetadata.ts +57 -0
- package/src/models/ThemeConfig.ts +34 -0
- package/src/models/W3cExternalTraceAttributes.ts +22 -0
- package/src/modules/APM.ts +117 -0
- package/src/modules/BugReporting.ts +254 -0
- package/src/modules/CrashReporting.ts +54 -0
- package/src/modules/FeatureRequests.ts +32 -0
- package/src/modules/Luciq.ts +934 -0
- package/src/modules/NetworkLogger.ts +270 -0
- package/src/modules/Replies.ts +137 -0
- package/src/modules/SessionReplay.ts +111 -0
- package/src/modules/Surveys.ts +118 -0
- package/src/native/NativeAPM.ts +51 -0
- package/src/native/NativeBugReporting.ts +70 -0
- package/src/native/NativeConstants.ts +215 -0
- package/src/native/NativeCrashReporting.ts +29 -0
- package/src/native/NativeFeatureRequests.ts +12 -0
- package/src/native/NativeLuciq.ts +179 -0
- package/src/native/NativeNetworkLogger.ts +42 -0
- package/src/native/NativePackage.ts +25 -0
- package/src/native/NativeReplies.ts +34 -0
- package/src/native/NativeSessionReplay.ts +21 -0
- package/src/native/NativeSurveys.ts +34 -0
- package/src/promise.d.ts +11 -0
- package/src/utils/AppStatesHandler.ts +19 -0
- package/src/utils/Enums.ts +266 -0
- package/src/utils/FeatureFlags.ts +33 -0
- package/src/utils/LuciqConstants.ts +24 -0
- package/src/utils/LuciqUtils.ts +417 -0
- package/src/utils/UnhandledRejectionTracking.ts +118 -0
- package/src/utils/XhrNetworkInterceptor.ts +333 -0
- package/src/utils/config.ts +7 -0
- package/src/utils/logger.ts +54 -0
- package/tsconfig.json +32 -0
- package/tsconfig.test.json +4 -0
- package/tsconfig.upload.json +10 -0
- package/upload/index.d.ts +4 -0
- package/upload/index.js +17314 -0
- package/upload/migrate.d.ts +14 -0
- package/upload/package.json +5 -0
- package/upload/uploadEasUpdatesSourcemaps.d.ts +21 -0
- package/upload/uploadSoFiles.d.ts +21 -0
- package/upload/uploadSourcemaps.d.ts +21 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface MigrateOptions {
|
|
2
|
+
dryRun?: boolean;
|
|
3
|
+
silent?: boolean;
|
|
4
|
+
configPath?: string;
|
|
5
|
+
skipGitCheck?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Performs migration operations on the codebase.
|
|
9
|
+
*
|
|
10
|
+
* @param opts Options for the migration process.
|
|
11
|
+
* @returns A promise that resolves to a boolean indicating whether the migration was successful.
|
|
12
|
+
*/
|
|
13
|
+
export declare const migrate: (opts: MigrateOptions) => Promise<boolean>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface UploadEasUpdatesSourcemapsOptions {
|
|
2
|
+
file: string;
|
|
3
|
+
token: string;
|
|
4
|
+
name: string;
|
|
5
|
+
code: string;
|
|
6
|
+
androidUpdateId?: string;
|
|
7
|
+
iosUpdateId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Disables logging to the console and prevents process exit on error.
|
|
10
|
+
*
|
|
11
|
+
* @default false
|
|
12
|
+
* */
|
|
13
|
+
silent?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Uploads JavaScript sourcemaps to Luciq.
|
|
17
|
+
*
|
|
18
|
+
* @param opts Options for the sourcemaps upload process.
|
|
19
|
+
* @returns A promise that resolves to a boolean indicating whether the upload was successful.
|
|
20
|
+
*/
|
|
21
|
+
export declare const UploadEasUpdatesSourcemaps: (opts: UploadEasUpdatesSourcemapsOptions) => Promise<boolean>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface UploadSoFilesOptions {
|
|
2
|
+
arch: 'x86' | 'x86_64' | 'arm64-v8a' | 'armeabi-v7a';
|
|
3
|
+
file: string;
|
|
4
|
+
token: string;
|
|
5
|
+
name: string;
|
|
6
|
+
api_key: string;
|
|
7
|
+
/**
|
|
8
|
+
* Disables logging to the console and prevents process exit on error.
|
|
9
|
+
*
|
|
10
|
+
* @default false
|
|
11
|
+
* */
|
|
12
|
+
silent?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Uploads NDK `.so` files to Luciq.
|
|
16
|
+
*
|
|
17
|
+
* @param opts Options for the `.so` files upload process.
|
|
18
|
+
* @returns A promise that resolves to a boolean indicating whether the upload was successful.
|
|
19
|
+
*/
|
|
20
|
+
export declare const uploadSoFiles: (opts: UploadSoFilesOptions) => Promise<boolean>;
|
|
21
|
+
export declare const assert: (condition: unknown, message: string, silent?: boolean) => boolean;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface UploadSourcemapsOptions {
|
|
2
|
+
platform: 'android' | 'ios';
|
|
3
|
+
file: string;
|
|
4
|
+
token: string;
|
|
5
|
+
name: string;
|
|
6
|
+
code: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Disables logging to the console and prevents process exit on error.
|
|
10
|
+
*
|
|
11
|
+
* @default false
|
|
12
|
+
* */
|
|
13
|
+
silent?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Uploads JavaScript sourcemaps to Luciq.
|
|
17
|
+
*
|
|
18
|
+
* @param opts Options for the sourcemaps upload process.
|
|
19
|
+
* @returns A promise that resolves to a boolean indicating whether the upload was successful.
|
|
20
|
+
*/
|
|
21
|
+
export declare const uploadSourcemaps: (opts: UploadSourcemapsOptions) => Promise<boolean>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Command, Option } from 'commander';
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import { migrate, MigrateOptions } from '../upload';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This script performs migration operations on the codebase.
|
|
7
|
+
* Usage: node migrate.js [--dry-run]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const MigrateCommand = new Command();
|
|
11
|
+
|
|
12
|
+
MigrateCommand.name('migrate')
|
|
13
|
+
.addOption(
|
|
14
|
+
new Option('--dry-run', 'Show what would be changed without making actual changes').default(
|
|
15
|
+
false,
|
|
16
|
+
),
|
|
17
|
+
)
|
|
18
|
+
.addOption(
|
|
19
|
+
new Option('--config <path>', 'Path to custom migration configuration file').default(undefined),
|
|
20
|
+
)
|
|
21
|
+
.addOption(new Option('--silent', 'Run migration silently without console output').default(false))
|
|
22
|
+
.addOption(
|
|
23
|
+
new Option(
|
|
24
|
+
'--skip-git-check',
|
|
25
|
+
'Skip git status check and continue with uncommitted changes',
|
|
26
|
+
).default(false),
|
|
27
|
+
)
|
|
28
|
+
.action(function (this: Command) {
|
|
29
|
+
const options = this.opts<MigrateOptions>();
|
|
30
|
+
migrate(options);
|
|
31
|
+
})
|
|
32
|
+
.showHelpAfterError();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Command, Option } from 'commander';
|
|
2
|
+
import { UploadEasUpdatesSourcemaps, UploadEasUpdatesSourcemapsOptions } from '../upload';
|
|
3
|
+
|
|
4
|
+
export const uploadEasUpdatesSourcemapsCommand = new Command();
|
|
5
|
+
|
|
6
|
+
uploadEasUpdatesSourcemapsCommand
|
|
7
|
+
.name('upload-eas-updates-sourcemaps')
|
|
8
|
+
.addOption(
|
|
9
|
+
new Option('-f, --file <path>', 'The path of eas update folder')
|
|
10
|
+
.makeOptionMandatory()
|
|
11
|
+
.default('dist'),
|
|
12
|
+
)
|
|
13
|
+
.addOption(
|
|
14
|
+
new Option('-t, --token <value>', 'Your App Token')
|
|
15
|
+
.env('LUCIQ_APP_TOKEN')
|
|
16
|
+
.makeOptionMandatory(),
|
|
17
|
+
)
|
|
18
|
+
.addOption(
|
|
19
|
+
new Option('-n, --name <value>', 'The app version name')
|
|
20
|
+
.env('LUCIQ_APP_VERSION_NAME')
|
|
21
|
+
.makeOptionMandatory(),
|
|
22
|
+
)
|
|
23
|
+
.addOption(
|
|
24
|
+
new Option('-c, --code <value>', 'The app version code')
|
|
25
|
+
.env('LUCIQ_APP_VERSION_CODE')
|
|
26
|
+
.makeOptionMandatory(),
|
|
27
|
+
)
|
|
28
|
+
.addOption(new Option('--androidUpdateId <value>', 'The Android Update Id from Eas Update'))
|
|
29
|
+
.addOption(new Option('--iosUpdateId <value>', 'The iOS Update Id from Eas Update'))
|
|
30
|
+
.action(function (this: Command) {
|
|
31
|
+
const options = this.opts<UploadEasUpdatesSourcemapsOptions>();
|
|
32
|
+
UploadEasUpdatesSourcemaps(options);
|
|
33
|
+
})
|
|
34
|
+
.showHelpAfterError();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Command, Option } from 'commander';
|
|
2
|
+
import { uploadSoFiles, UploadSoFilesOptions } from '../upload/uploadSoFiles';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This script uploads .so files to the specified endpoint used in NDK crash reporting.
|
|
6
|
+
* Usage: node upload-so-files.js --arch <arch> --file <path> --api_key <key> --token <token> --name <name>
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const UploadSoFilesCommand = new Command();
|
|
10
|
+
|
|
11
|
+
UploadSoFilesCommand.name('upload-so-files')
|
|
12
|
+
.addOption(
|
|
13
|
+
new Option('-arch, --arch <value>', 'arch')
|
|
14
|
+
.choices(['x86', 'x86_64', 'arm64-v8a', 'armeabi-v7a'])
|
|
15
|
+
.makeOptionMandatory(),
|
|
16
|
+
)
|
|
17
|
+
.addOption(
|
|
18
|
+
new Option(
|
|
19
|
+
'-f, --file <path>',
|
|
20
|
+
'The path of the symbol files in Zip format',
|
|
21
|
+
).makeOptionMandatory(),
|
|
22
|
+
)
|
|
23
|
+
.addOption(new Option('--api_key <value>', 'Your App key').makeOptionMandatory())
|
|
24
|
+
.addOption(
|
|
25
|
+
new Option('-t, --token <value>', 'Your App Token')
|
|
26
|
+
.env('LUCIQ_APP_TOKEN')
|
|
27
|
+
.makeOptionMandatory(),
|
|
28
|
+
)
|
|
29
|
+
.addOption(
|
|
30
|
+
new Option('-n, --name <value>', 'The app version name')
|
|
31
|
+
.env('LUCIQ_APP_VERSION_NAME')
|
|
32
|
+
.makeOptionMandatory(),
|
|
33
|
+
)
|
|
34
|
+
.action(function (this: Command) {
|
|
35
|
+
const options = this.opts<UploadSoFilesOptions>();
|
|
36
|
+
uploadSoFiles(options);
|
|
37
|
+
})
|
|
38
|
+
.showHelpAfterError();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command, Option } from 'commander';
|
|
2
|
+
import { uploadSourcemaps, UploadSourcemapsOptions } from '../upload/uploadSourcemaps';
|
|
3
|
+
|
|
4
|
+
export const uploadSourcemapsCommand = new Command();
|
|
5
|
+
|
|
6
|
+
uploadSourcemapsCommand
|
|
7
|
+
.name('upload-sourcemaps')
|
|
8
|
+
.addOption(
|
|
9
|
+
new Option('-p, --platform <value>', 'Platform')
|
|
10
|
+
.choices(['ios', 'android'])
|
|
11
|
+
.makeOptionMandatory(),
|
|
12
|
+
)
|
|
13
|
+
.addOption(
|
|
14
|
+
new Option('-f, --file <path>', 'The path of the source map file').makeOptionMandatory(),
|
|
15
|
+
)
|
|
16
|
+
.addOption(
|
|
17
|
+
new Option('-t, --token <value>', 'Your App Token')
|
|
18
|
+
.env('LUCIQ_APP_TOKEN')
|
|
19
|
+
.makeOptionMandatory(),
|
|
20
|
+
)
|
|
21
|
+
.addOption(
|
|
22
|
+
new Option('-n, --name <value>', 'The app version name')
|
|
23
|
+
.env('LUCIQ_APP_VERSION_NAME')
|
|
24
|
+
.makeOptionMandatory(),
|
|
25
|
+
)
|
|
26
|
+
.addOption(
|
|
27
|
+
new Option('-c, --code <value>', 'The app version code')
|
|
28
|
+
.env('LUCIQ_APP_VERSION_CODE')
|
|
29
|
+
.makeOptionMandatory(),
|
|
30
|
+
)
|
|
31
|
+
.addOption(
|
|
32
|
+
new Option('-l, --label <value>', "The CodePush label if it's a CodePush release").env(
|
|
33
|
+
'LUCIQ_APP_VERSION_LABEL',
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
.action(function (this: Command) {
|
|
37
|
+
const options = this.opts<UploadSourcemapsOptions>();
|
|
38
|
+
uploadSourcemaps(options);
|
|
39
|
+
})
|
|
40
|
+
.showHelpAfterError();
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
{
|
|
2
|
+
"migrationMethods": [
|
|
3
|
+
{
|
|
4
|
+
"name": "Dependency Version Updates",
|
|
5
|
+
"description": "Update dependency versions from Instabug to Luciq (runs first)",
|
|
6
|
+
"priority": 1,
|
|
7
|
+
"searchReplace": [
|
|
8
|
+
{
|
|
9
|
+
"search": "com\\.instabug\\.library:instabug:[0-9]+\\.[0-9]+\\.[0-9]+",
|
|
10
|
+
"replacement": "ai.luciq.library:luciq:18.0.0",
|
|
11
|
+
"isRegex": true
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"search": "com\\.instabug\\.library:instabug:[0-9]+\\.[0-9]+\\.[0-9]+-.*",
|
|
15
|
+
"replacement": "ai.luciq.library:luciq:18.0.0",
|
|
16
|
+
"isRegex": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"search": "com\\.instabug\\.library:instabug:[0-9]+\\.[0-9]+\\.[0-9]+-SNAPSHOT",
|
|
20
|
+
"replacement": "ai.luciq.library:luciq:18.0.0",
|
|
21
|
+
"isRegex": true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"search": "\"instabug-react-native\":\\s*\"[^\"]+\"",
|
|
25
|
+
"replacement": "\"@luciq/react-native\": \"^18.0.0\"",
|
|
26
|
+
"isRegex": true
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"search": "'instabug-react-native':\\s*'[^']+'",
|
|
30
|
+
"replacement": "'@luciq/react-native': '^18.0.0'",
|
|
31
|
+
"isRegex": true
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"targetExtensions": [".gradle", ".properties", ".xml", ".json", ".js", ".ts"],
|
|
35
|
+
"ignoredDirs": ["node_modules", "build", "Pods", "vendor", ".git", "dist", "coverage"],
|
|
36
|
+
"enabled": true
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "Package Name Updates",
|
|
40
|
+
"description": "Update package names and references",
|
|
41
|
+
"priority": 2,
|
|
42
|
+
"searchReplace": [
|
|
43
|
+
{ "search": "com.instabug", "replacement": "ai.luciq" },
|
|
44
|
+
{ "search": "com.instabug.reactlibrary", "replacement": "ai.luciq.reactlibrary" },
|
|
45
|
+
{ "search": "RNInstabug", "replacement": "RNLuciq" },
|
|
46
|
+
{ "search": "rninstabug", "replacement": "rnluciq" },
|
|
47
|
+
{ "search": "instabug-reactnative", "replacement": "@luciq/react-native" },
|
|
48
|
+
{ "search": "instabug-react-native", "replacement": "@luciq/react-native" }
|
|
49
|
+
],
|
|
50
|
+
"targetExtensions": [".java", ".kt", ".ts", ".tsx", ".js", ".jsx", ".json"],
|
|
51
|
+
"ignoredDirs": ["node_modules", "build", "Pods", "vendor", ".git", "dist", "coverage"],
|
|
52
|
+
"enabled": true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "Instabug to Luciq",
|
|
56
|
+
"description": "Replace all instances of Instabug with Luciq",
|
|
57
|
+
"priority": 3,
|
|
58
|
+
"searchReplace": [
|
|
59
|
+
{ "search": "Instabug", "replacement": "Luciq" },
|
|
60
|
+
{ "search": "instabug", "replacement": "luciq" },
|
|
61
|
+
{ "search": "INSTABUG", "replacement": "LUCIQ" },
|
|
62
|
+
{ "search": "IBG", "replacement": "LCQ" },
|
|
63
|
+
{ "search": "ibg", "replacement": "lcq" }
|
|
64
|
+
],
|
|
65
|
+
"targetExtensions": [".h", ".m", ".java", ".kt", ".ts", ".tsx", ".js", ".jsx"],
|
|
66
|
+
"ignoredDirs": [
|
|
67
|
+
"node_modules",
|
|
68
|
+
"build",
|
|
69
|
+
"Pods",
|
|
70
|
+
"vendor",
|
|
71
|
+
".git",
|
|
72
|
+
"dist",
|
|
73
|
+
"coverage",
|
|
74
|
+
".next",
|
|
75
|
+
".nuxt",
|
|
76
|
+
"target"
|
|
77
|
+
],
|
|
78
|
+
"enabled": true
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "Cleanup",
|
|
82
|
+
"description": "Clean up file extensions and references",
|
|
83
|
+
"priority": 4,
|
|
84
|
+
"searchReplace": [
|
|
85
|
+
{ "search": ".instabug", "replacement": ".luciq" },
|
|
86
|
+
{ "search": "_instabug", "replacement": "_luciq" },
|
|
87
|
+
{ "search": "instabug_", "replacement": "luciq_" }
|
|
88
|
+
],
|
|
89
|
+
"targetExtensions": [".ts", ".tsx", ".js", ".jsx"],
|
|
90
|
+
"ignoredDirs": ["node_modules", "build", "Pods", "vendor", ".git", "dist", "coverage"],
|
|
91
|
+
"enabled": true
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "IOS File changes objective c",
|
|
95
|
+
"description": "IOS Objective C files",
|
|
96
|
+
"priority": 5,
|
|
97
|
+
"searchReplace": [
|
|
98
|
+
{ "search": "Instabug", "replacement": "Luciq" },
|
|
99
|
+
{ "search": "instabug", "replacement": "luciq" },
|
|
100
|
+
{ "search": "INSTABUG", "replacement": "LUCIQ" },
|
|
101
|
+
{ "search": "IBG", "replacement": "LCQ" }
|
|
102
|
+
],
|
|
103
|
+
"targetExtensions": [".h", ".m"],
|
|
104
|
+
"ignoredDirs": ["node_modules", "build", "Pods", "vendor", ".git", "dist", "coverage"],
|
|
105
|
+
"enabled": true
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "IOS File changes objective swift",
|
|
109
|
+
"description": "IOS Swift files",
|
|
110
|
+
"priority": 6,
|
|
111
|
+
"searchReplace": [
|
|
112
|
+
{ "search": "Instabug", "replacement": "Luciq" },
|
|
113
|
+
{ "search": "instabug", "replacement": "luciq" },
|
|
114
|
+
{ "search": "INSTABUG", "replacement": "LUCIQ" }
|
|
115
|
+
],
|
|
116
|
+
"targetExtensions": [".swift"],
|
|
117
|
+
"ignoredDirs": ["node_modules", "build", "Pods", "vendor", ".git", "dist", "coverage"],
|
|
118
|
+
"enabled": true
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
"defaultOptions": {
|
|
122
|
+
"dryRun": false,
|
|
123
|
+
"silent": false
|
|
124
|
+
}
|
|
125
|
+
}
|
package/cli/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
|
|
4
|
+
import { uploadSourcemapsCommand } from './commands/UploadSourcemaps';
|
|
5
|
+
import { UploadSoFilesCommand } from './commands/UploadSoFiles';
|
|
6
|
+
import { uploadEasUpdatesSourcemapsCommand } from './commands/UploadEasUpdatesSourcemaps';
|
|
7
|
+
import { MigrateCommand } from './commands/MigrateCommand';
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('luciq')
|
|
13
|
+
.version('1.0.0-beta1')
|
|
14
|
+
.description('A CLI for uploading source maps to Luciq dashboard.')
|
|
15
|
+
.usage('[command]')
|
|
16
|
+
.addCommand(uploadSourcemapsCommand)
|
|
17
|
+
.addCommand(UploadSoFilesCommand)
|
|
18
|
+
.addCommand(uploadEasUpdatesSourcemapsCommand)
|
|
19
|
+
.addCommand(MigrateCommand);
|
|
20
|
+
|
|
21
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import * as readline from 'readline';
|
|
5
|
+
|
|
6
|
+
interface SearchReplaceRule {
|
|
7
|
+
search: string;
|
|
8
|
+
replacement: string;
|
|
9
|
+
isRegex?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface MigrationMethod {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
searchReplace: SearchReplaceRule[];
|
|
16
|
+
targetExtensions: string[];
|
|
17
|
+
ignoredDirs: string[];
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
priority?: number; // Lower number = higher priority
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface MigrationConfig {
|
|
23
|
+
migrationMethods: MigrationMethod[];
|
|
24
|
+
defaultOptions: {
|
|
25
|
+
dryRun: boolean;
|
|
26
|
+
silent: boolean;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface MigrateOptions {
|
|
31
|
+
dryRun?: boolean;
|
|
32
|
+
silent?: boolean;
|
|
33
|
+
configPath?: string;
|
|
34
|
+
skipGitCheck?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Loads migration configuration from JSON file
|
|
39
|
+
*/
|
|
40
|
+
function loadMigrationConfig(configPath?: string): MigrationMethod[] {
|
|
41
|
+
const defaultConfigPath = path.join(__dirname, 'config/migration-config.json');
|
|
42
|
+
const configFilePath = configPath || defaultConfigPath;
|
|
43
|
+
|
|
44
|
+
if (!fs.existsSync(configFilePath)) {
|
|
45
|
+
throw new Error(`Configuration file not found: ${configFilePath}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const config: MigrationConfig = JSON.parse(fs.readFileSync(configFilePath, 'utf8'));
|
|
49
|
+
return config.migrationMethods;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Performs migration operations on the codebase.
|
|
54
|
+
*
|
|
55
|
+
* @param opts Options for the migration process.
|
|
56
|
+
* @returns A promise that resolves to a boolean indicating whether the migration was successful.
|
|
57
|
+
*/
|
|
58
|
+
export const migrate = async (opts: MigrateOptions): Promise<boolean> => {
|
|
59
|
+
try {
|
|
60
|
+
const allMethods = loadMigrationConfig(opts.configPath);
|
|
61
|
+
const enabledMethods = allMethods.filter((m) => m.enabled);
|
|
62
|
+
|
|
63
|
+
if (enabledMethods.length === 0) {
|
|
64
|
+
if (!opts.silent) {
|
|
65
|
+
console.error('❌ No migration methods are enabled');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check git status unless skipped
|
|
72
|
+
if (!opts.skipGitCheck) {
|
|
73
|
+
const shouldSkip = await checkGitStatus(opts.silent);
|
|
74
|
+
if (!shouldSkip) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Sort methods by priority (lower number = higher priority)
|
|
80
|
+
const sortedMethods = enabledMethods.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
81
|
+
|
|
82
|
+
if (!opts.silent) {
|
|
83
|
+
console.log('🔄 Starting migration with all enabled methods');
|
|
84
|
+
console.log(`📝 Found ${sortedMethods.length} enabled method(s):`);
|
|
85
|
+
sortedMethods.forEach((method) => {
|
|
86
|
+
console.log(` - ${method.name}: ${method.description}`);
|
|
87
|
+
});
|
|
88
|
+
if (opts.dryRun) {
|
|
89
|
+
console.log('🔍 Dry run mode enabled - no changes will be made');
|
|
90
|
+
}
|
|
91
|
+
console.log('================================');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await executeAllMethods(sortedMethods, opts.dryRun || false, opts.silent);
|
|
95
|
+
|
|
96
|
+
if (!opts.silent) {
|
|
97
|
+
if (opts.dryRun) {
|
|
98
|
+
console.log('\n🔍 Dry run completed. No changes were made.');
|
|
99
|
+
} else {
|
|
100
|
+
console.log('\n🎉 All migrations completed successfully!');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return true;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (!opts.silent) {
|
|
107
|
+
console.error('❌ Migration failed:', error);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
async function checkGitStatus(silent?: boolean): Promise<boolean> {
|
|
115
|
+
try {
|
|
116
|
+
const status = execSync('git status --porcelain').toString();
|
|
117
|
+
if (status.trim()) {
|
|
118
|
+
if (silent) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.warn('⚠️ Uncommitted changes detected.');
|
|
123
|
+
console.log('Current changes:');
|
|
124
|
+
console.log(status);
|
|
125
|
+
|
|
126
|
+
const rl = readline.createInterface({
|
|
127
|
+
input: process.stdin,
|
|
128
|
+
output: process.stdout,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const answer = await new Promise<string>((resolve) => {
|
|
132
|
+
rl.question('Do you want to continue anyway? (y/N): ', resolve);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
rl.close();
|
|
136
|
+
|
|
137
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
138
|
+
console.log('Migration cancelled.');
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log('Continuing with migration...');
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (!silent) {
|
|
147
|
+
console.warn('⚠️ Git check failed, continuing without git validation...');
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isIgnored(filePath: string, ignoredDirs: string[]): boolean {
|
|
154
|
+
return ignoredDirs.some((dir) => filePath.split(path.sep).includes(dir));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function casePreservingReplace(str: string, searchReplace: SearchReplaceRule[]): string {
|
|
158
|
+
let result = str;
|
|
159
|
+
|
|
160
|
+
for (const { search, replacement, isRegex } of searchReplace) {
|
|
161
|
+
if (isRegex) {
|
|
162
|
+
// For regex patterns, use direct replacement without case preservation
|
|
163
|
+
result = result.replace(new RegExp(search, 'g'), replacement);
|
|
164
|
+
} else {
|
|
165
|
+
// For literal strings, preserve case
|
|
166
|
+
result = result.replace(new RegExp(search, 'gi'), (match) => {
|
|
167
|
+
if (match === match.toUpperCase()) {
|
|
168
|
+
return replacement.toUpperCase();
|
|
169
|
+
}
|
|
170
|
+
if (match[0] === match[0].toUpperCase()) {
|
|
171
|
+
return replacement[0].toUpperCase() + replacement.slice(1);
|
|
172
|
+
}
|
|
173
|
+
return replacement.toLowerCase();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function processFile(
|
|
182
|
+
filePath: string,
|
|
183
|
+
method: MigrationMethod,
|
|
184
|
+
dryRun: boolean,
|
|
185
|
+
silent?: boolean,
|
|
186
|
+
): void {
|
|
187
|
+
if (isIgnored(filePath, method.ignoredDirs)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const fileExt = path.extname(filePath);
|
|
193
|
+
|
|
194
|
+
if (!method.targetExtensions.includes(fileExt)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
199
|
+
const newContent = casePreservingReplace(content, method.searchReplace);
|
|
200
|
+
|
|
201
|
+
if (newContent !== content) {
|
|
202
|
+
if (dryRun) {
|
|
203
|
+
if (!silent) {
|
|
204
|
+
console.log(`📝 [${method.name}] Would update content: ${filePath}`);
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
210
|
+
if (!silent) {
|
|
211
|
+
console.log(`📝 [${method.name}] Updated content: ${filePath}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (!silent) {
|
|
216
|
+
console.error(`❌ Error processing file ${filePath}:`, error);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function walkDirectory(
|
|
222
|
+
dirPath: string,
|
|
223
|
+
method: MigrationMethod,
|
|
224
|
+
dryRun: boolean,
|
|
225
|
+
silent?: boolean,
|
|
226
|
+
): void {
|
|
227
|
+
if (isIgnored(dirPath, method.ignoredDirs)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
233
|
+
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
236
|
+
if (entry.isDirectory()) {
|
|
237
|
+
walkDirectory(fullPath, method, dryRun, silent);
|
|
238
|
+
} else {
|
|
239
|
+
processFile(fullPath, method, dryRun, silent);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (!silent) {
|
|
244
|
+
console.error(`❌ Error walking directory ${dirPath}:`, error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function executeAllMethods(
|
|
250
|
+
methods: MigrationMethod[],
|
|
251
|
+
dryRun: boolean,
|
|
252
|
+
silent?: boolean,
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
for (const method of methods) {
|
|
255
|
+
await executeMethod(method, dryRun, silent);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function executeMethod(
|
|
260
|
+
method: MigrationMethod,
|
|
261
|
+
dryRun: boolean,
|
|
262
|
+
silent?: boolean,
|
|
263
|
+
): Promise<void> {
|
|
264
|
+
const startDir = process.cwd();
|
|
265
|
+
|
|
266
|
+
if (!silent) {
|
|
267
|
+
console.log(`🚀 Executing method: ${method.name}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
walkDirectory(startDir, method, dryRun, silent);
|
|
271
|
+
}
|