codeplay-common 3.2.19 → 4.0.1

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.
@@ -1,145 +1,145 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
-
5
-
6
- const androidManifestPath = path.join("android", "app", "src", "main", "AndroidManifest.xml");
7
-
8
- // Find the vite config file
9
- const possibleConfigFiles = ['vite.config.mjs', 'vite.config.js'];
10
- let viteConfigPath;
11
-
12
- for (const configFile of possibleConfigFiles) {
13
- const fullPath = path.resolve(configFile);
14
- if (fs.existsSync(fullPath)) {
15
- viteConfigPath = fullPath;
16
- break;
17
- }
18
- }
19
-
20
- if (!viteConfigPath) {
21
- console.error('Error: No vite.config.mjs or vite.config.js file found.');
22
- process.exit(1);
23
- }
24
-
25
- try {
26
- // Read vite config file
27
- const viteConfigContent = fs.readFileSync(viteConfigPath, 'utf-8');
28
-
29
- // Extract @common alias path
30
- const aliasPattern = /'@common':\s*path\.resolve\(__dirname,\s*'(.+?)'\)/;
31
- const match = viteConfigContent.match(aliasPattern);
32
-
33
- if (!match) {
34
- console.error(`Error: @common alias not found in ${viteConfigPath}`);
35
- process.exit(1);
36
- }
37
-
38
- const commonFilePath = match[1];
39
- const resolvedCommonPath = path.resolve(__dirname, commonFilePath);
40
-
41
- // Read the common file content
42
- if (!fs.existsSync(resolvedCommonPath)) {
43
- console.error(`Error: Resolved common file does not exist: ${resolvedCommonPath}`);
44
- process.exit(1);
45
- }
46
-
47
- const commonFileContent = fs.readFileSync(resolvedCommonPath, 'utf-8');
48
-
49
- // Extract _storeid value
50
- const storeIdPattern = /export\s+let\s+_storeid\s*=\s*import\.meta\.env\.VITE_STORE_ID\s*\|\|\s*(\d+)\s*;/;
51
-
52
- const storeMatch = commonFileContent.match(storeIdPattern);
53
-
54
-
55
- if (!storeMatch) {
56
- console.error(`Error: _storeid not found in ${resolvedCommonPath}`);
57
- process.exit(1);
58
- }
59
-
60
- const _storeid = parseInt(storeMatch[1], 10);
61
-
62
- // Determine the store name based on _storeid
63
- let storeName = "";
64
- if (_storeid === 1) {
65
- storeName = "PlayStore";
66
- } else if (_storeid === 2) {
67
- storeName = "SamsungStore";
68
- } else if (_storeid === 7) {
69
- storeName = "AmazonStore";
70
- } else {
71
- console.error(`Error: Unsupported _storeid value: ${_storeid}`);
72
- process.exit(1);
73
- }
74
-
75
-
76
- // Call managePackages with the determined store name
77
- managePackages(storeName);
78
-
79
- console.log(commonFilePath, `Success - _storeid found: ${_storeid}, Store: ${storeName}`);
80
- } catch (error) {
81
- console.error('Error:', error);
82
- process.exit(1);
83
- }
84
-
85
- function managePackages(store) {
86
- console.log(`Managing packages for store: ${store}`);
87
-
88
- let install = "";
89
- let uninstall = "";
90
-
91
- //let androidManifestPath = "path/to/AndroidManifest.xml"; // Update this path
92
-
93
-
94
- let manifestContent = fs.readFileSync(androidManifestPath, 'utf-8');
95
-
96
- const permissionsToRemove = [
97
- 'com.android.vending.BILLING',
98
- 'com.samsung.android.iap.permission.BILLING'
99
- ];
100
-
101
- permissionsToRemove.forEach(permission => {
102
- const permissionRegex = new RegExp(`^\\s*<uses-permission\\s+android:name="${permission}"\\s*/?>\\s*[\r\n]?`, 'm');
103
- if (permissionRegex.test(manifestContent)) {
104
- manifestContent = manifestContent.replace(permissionRegex, '');
105
- console.log(`Removed <uses-permission android:name="${permission}" /> from AndroidManifest.xml`);
106
- }
107
- });
108
-
109
- // Write the updated content back to the file
110
- fs.writeFileSync(androidManifestPath, manifestContent, 'utf-8');
111
-
112
- if (store === "PlayStore") {
113
- install = '@revenuecat/purchases-capacitor';
114
- uninstall = 'cordova-plugin-samsungiap';
115
- } else if (store === "AmazonStore") {
116
- install = '@revenuecat/purchases-capacitor';
117
- uninstall = 'cordova-plugin-samsungiap';
118
- } else if (store === "SamsungStore") {
119
- install = 'cordova-plugin-samsungiap';
120
- uninstall = '@revenuecat/purchases-capacitor';
121
- } else {
122
- console.log("No valid store specified. Uninstalling both plugins.");
123
- try {
124
- require('child_process').execSync(`npm uninstall cordova-plugin-samsungiap`, { stdio: 'inherit' });
125
- require('child_process').execSync(`npm uninstall @revenuecat/purchases-capacitor`, { stdio: 'inherit' });
126
- console.log("Both plugins uninstalled successfully.");
127
- } catch (err) {
128
- console.error("Error uninstalling plugins:", err);
129
- }
130
- return;
131
- }
132
-
133
- console.log(`Installing ${install} and uninstalling ${uninstall} for ${store}...`);
134
- try {
135
- if (install) {
136
- require('child_process').execSync(`npm install ${install}`, { stdio: 'inherit' });
137
- }
138
- if (uninstall) {
139
- require('child_process').execSync(`npm uninstall ${uninstall}`, { stdio: 'inherit' });
140
- }
141
- console.log(`${install} installed and ${uninstall} uninstalled successfully.`);
142
- } catch (err) {
143
- console.error(`Error managing packages for ${store}:`, err);
144
- }
145
- }
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+
5
+
6
+ const androidManifestPath = path.join("android", "app", "src", "main", "AndroidManifest.xml");
7
+
8
+ // Find the vite config file
9
+ const possibleConfigFiles = ['vite.config.mjs', 'vite.config.js'];
10
+ let viteConfigPath;
11
+
12
+ for (const configFile of possibleConfigFiles) {
13
+ const fullPath = path.resolve(configFile);
14
+ if (fs.existsSync(fullPath)) {
15
+ viteConfigPath = fullPath;
16
+ break;
17
+ }
18
+ }
19
+
20
+ if (!viteConfigPath) {
21
+ console.error('Error: No vite.config.mjs or vite.config.js file found.');
22
+ process.exit(1);
23
+ }
24
+
25
+ try {
26
+ // Read vite config file
27
+ const viteConfigContent = fs.readFileSync(viteConfigPath, 'utf-8');
28
+
29
+ // Extract @common alias path
30
+ const aliasPattern = /'@common':\s*path\.resolve\(__dirname,\s*'(.+?)'\)/;
31
+ const match = viteConfigContent.match(aliasPattern);
32
+
33
+ if (!match) {
34
+ console.error(`Error: @common alias not found in ${viteConfigPath}`);
35
+ process.exit(1);
36
+ }
37
+
38
+ const commonFilePath = match[1];
39
+ const resolvedCommonPath = path.resolve(__dirname, commonFilePath);
40
+
41
+ // Read the common file content
42
+ if (!fs.existsSync(resolvedCommonPath)) {
43
+ console.error(`Error: Resolved common file does not exist: ${resolvedCommonPath}`);
44
+ process.exit(1);
45
+ }
46
+
47
+ const commonFileContent = fs.readFileSync(resolvedCommonPath, 'utf-8');
48
+
49
+ // Extract _storeid value
50
+ const storeIdPattern = /export\s+let\s+_storeid\s*=\s*import\.meta\.env\.VITE_STORE_ID\s*\|\|\s*(\d+)\s*;/;
51
+
52
+ const storeMatch = commonFileContent.match(storeIdPattern);
53
+
54
+
55
+ if (!storeMatch) {
56
+ console.error(`Error: _storeid not found in ${resolvedCommonPath}`);
57
+ process.exit(1);
58
+ }
59
+
60
+ const _storeid = parseInt(storeMatch[1], 10);
61
+
62
+ // Determine the store name based on _storeid
63
+ let storeName = "";
64
+ if (_storeid === 1) {
65
+ storeName = "PlayStore";
66
+ } else if (_storeid === 2) {
67
+ storeName = "SamsungStore";
68
+ } else if (_storeid === 7) {
69
+ storeName = "AmazonStore";
70
+ } else {
71
+ console.error(`Error: Unsupported _storeid value: ${_storeid}`);
72
+ process.exit(1);
73
+ }
74
+
75
+
76
+ // Call managePackages with the determined store name
77
+ managePackages(storeName);
78
+
79
+ console.log(commonFilePath, `Success - _storeid found: ${_storeid}, Store: ${storeName}`);
80
+ } catch (error) {
81
+ console.error('Error:', error);
82
+ process.exit(1);
83
+ }
84
+
85
+ function managePackages(store) {
86
+ console.log(`Managing packages for store: ${store}`);
87
+
88
+ let install = "";
89
+ let uninstall = "";
90
+
91
+ //let androidManifestPath = "path/to/AndroidManifest.xml"; // Update this path
92
+
93
+
94
+ let manifestContent = fs.readFileSync(androidManifestPath, 'utf-8');
95
+
96
+ const permissionsToRemove = [
97
+ 'com.android.vending.BILLING',
98
+ 'com.samsung.android.iap.permission.BILLING'
99
+ ];
100
+
101
+ permissionsToRemove.forEach(permission => {
102
+ const permissionRegex = new RegExp(`^\\s*<uses-permission\\s+android:name="${permission}"\\s*/?>\\s*[\r\n]?`, 'm');
103
+ if (permissionRegex.test(manifestContent)) {
104
+ manifestContent = manifestContent.replace(permissionRegex, '');
105
+ console.log(`Removed <uses-permission android:name="${permission}" /> from AndroidManifest.xml`);
106
+ }
107
+ });
108
+
109
+ // Write the updated content back to the file
110
+ fs.writeFileSync(androidManifestPath, manifestContent, 'utf-8');
111
+
112
+ if (store === "PlayStore") {
113
+ install = '@revenuecat/purchases-capacitor';
114
+ uninstall = 'cordova-plugin-samsungiap';
115
+ } else if (store === "AmazonStore") {
116
+ install = '@revenuecat/purchases-capacitor';
117
+ uninstall = 'cordova-plugin-samsungiap';
118
+ } else if (store === "SamsungStore") {
119
+ install = 'cordova-plugin-samsungiap';
120
+ uninstall = '@revenuecat/purchases-capacitor';
121
+ } else {
122
+ console.log("No valid store specified. Uninstalling both plugins.");
123
+ try {
124
+ require('child_process').execSync(`npm uninstall cordova-plugin-samsungiap`, { stdio: 'inherit' });
125
+ require('child_process').execSync(`npm uninstall @revenuecat/purchases-capacitor`, { stdio: 'inherit' });
126
+ console.log("Both plugins uninstalled successfully.");
127
+ } catch (err) {
128
+ console.error("Error uninstalling plugins:", err);
129
+ }
130
+ return;
131
+ }
132
+
133
+ console.log(`Installing ${install} and uninstalling ${uninstall} for ${store}...`);
134
+ try {
135
+ if (install) {
136
+ require('child_process').execSync(`npm install ${install}`, { stdio: 'inherit' });
137
+ }
138
+ if (uninstall) {
139
+ require('child_process').execSync(`npm uninstall ${uninstall}`, { stdio: 'inherit' });
140
+ }
141
+ console.log(`${install} installed and ${uninstall} uninstalled successfully.`);
142
+ } catch (err) {
143
+ console.error(`Error managing packages for ${store}:`, err);
144
+ }
145
+ }
@@ -1,7 +1,7 @@
1
- {
2
- "name": "merbin-test-app",
3
- "integrations": {
4
- "capacitor": {}
5
- },
6
- "type": "custom"
1
+ {
2
+ "name": "merbin-test-app",
3
+ "integrations": {
4
+ "capacitor": {}
5
+ },
6
+ "type": "custom"
7
7
  }
@@ -0,0 +1,241 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const rootDir = path.resolve(__dirname, '..');
5
+ const androidDir = path.join(rootDir, 'android');
6
+ const capacitorConfigPath = path.join(rootDir, 'capacitor.config.json');
7
+ const backupProfile = path.join(rootDir, 'baselineProfiles', 'baseline-prof.txt');
8
+ const generatedProfile = path.join(
9
+ androidDir,
10
+ 'app',
11
+ 'src',
12
+ 'main',
13
+ 'generated',
14
+ 'baselineProfiles',
15
+ 'baseline-prof.txt'
16
+ );
17
+
18
+ function readFile(filePath) {
19
+ return fs.readFileSync(filePath, 'utf8');
20
+ }
21
+
22
+ function writeFile(filePath, content) {
23
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
24
+ fs.writeFileSync(filePath, content);
25
+ }
26
+
27
+ function patchFile(filePath, patcher) {
28
+ const before = readFile(filePath);
29
+ const after = patcher(before);
30
+ if (after !== before) {
31
+ writeFile(filePath, after);
32
+ console.log(`Updated ${path.relative(rootDir, filePath)}`);
33
+ }
34
+ }
35
+
36
+ function insertAfter(content, marker, insertion) {
37
+ if (content.includes(insertion.trim())) return content;
38
+ const index = content.indexOf(marker);
39
+ if (index === -1) return `${content.trimEnd()}\n${insertion}\n`;
40
+ const insertAt = index + marker.length;
41
+ return `${content.slice(0, insertAt)}${insertion}${content.slice(insertAt)}`;
42
+ }
43
+
44
+ function insertBeforeClosingBrace(content, blockStart, insertion) {
45
+ if (content.includes(insertion.trim())) return content;
46
+ const startIndex = content.indexOf(blockStart);
47
+ if (startIndex === -1) return content;
48
+
49
+ let depth = 0;
50
+ for (let index = startIndex; index < content.length; index += 1) {
51
+ const char = content[index];
52
+ if (char === '{') depth += 1;
53
+ if (char === '}') {
54
+ depth -= 1;
55
+ if (depth === 0) {
56
+ return `${content.slice(0, index).trimEnd()}\n${insertion}\n${content.slice(index)}`;
57
+ }
58
+ }
59
+ }
60
+ return content;
61
+ }
62
+
63
+ function getAppIdFromCapacitorConfig() {
64
+ if (!fs.existsSync(capacitorConfigPath)) {
65
+ throw new Error(`capacitor.config.json not found at ${capacitorConfigPath}`);
66
+ }
67
+
68
+ let config;
69
+ try {
70
+ config = JSON.parse(readFile(capacitorConfigPath));
71
+ } catch (error) {
72
+ throw new Error(`Failed to parse capacitor.config.json: ${error.message}`);
73
+ }
74
+
75
+ const appId = config?.appId;
76
+ if (typeof appId !== 'string' || !appId.trim()) {
77
+ throw new Error('capacitor.config.json does not contain a valid appId.');
78
+ }
79
+
80
+ return appId.trim();
81
+ }
82
+
83
+ function sanitizeJavaPackageSegment(segment) {
84
+ const safeSegment = segment.replace(/[^a-zA-Z0-9_]/g, '_');
85
+ return safeSegment.replace(/^[0-9]/, (match) => `_${match}`) || 'app';
86
+ }
87
+
88
+ function toSafeJavaPackageName(value) {
89
+ return value
90
+ .split('.')
91
+ .map(sanitizeJavaPackageSegment)
92
+ .join('.');
93
+ }
94
+
95
+ function getBaselineProfileBuildGradle(modulePackage) {
96
+ return `apply plugin: 'com.android.test'\napply plugin: 'androidx.baselineprofile'\n\nandroid {\n namespace = \"${modulePackage}\"\n compileSdk = rootProject.ext.compileSdkVersion\n\n defaultConfig {\n minSdkVersion rootProject.ext.minSdkVersion\n targetSdkVersion rootProject.ext.targetSdkVersion\n testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n }\n\n targetProjectPath = \":app\"\n}\n\nbaselineProfile {\n useConnectedDevices true\n}\n\ndependencies {\n implementation \"androidx.benchmark:benchmark-macro-junit4:1.4.0\"\n implementation \"androidx.test.ext:junit:$androidxJunitVersion\"\n implementation \"androidx.test.uiautomator:uiautomator:2.3.0\"\n}\n`;
97
+ }
98
+
99
+ const baselineProfileManifest = `<?xml version="1.0" encoding="utf-8"?>
100
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
101
+ `;
102
+
103
+ function getBaselineProfileGenerator(modulePackage, appId) {
104
+ return `package ${modulePackage};\n\nimport androidx.benchmark.macro.junit4.BaselineProfileRule;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport kotlin.Unit;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n@RunWith(AndroidJUnit4.class)\npublic class BaselineProfileGenerator {\n private static final String PACKAGE_NAME = \"${appId}\";\n\n @Rule\n public final BaselineProfileRule baselineProfileRule = new BaselineProfileRule();\n\n @Test\n public void generateStartupProfile() {\n baselineProfileRule.collect(\n PACKAGE_NAME,\n 15,\n 3,\n null,\n true,\n true,\n scope -> {\n scope.getDevice().pressHome();\n scope.startActivityAndWait();\n scope.getDevice().waitForIdle();\n waitForWebViewStartup();\n scope.killProcess();\n return Unit.INSTANCE;\n }\n );\n }\n\n private void waitForWebViewStartup() {\n try {\n Thread.sleep(3000);\n } catch (InterruptedException exception) {\n Thread.currentThread().interrupt();\n }\n }\n}\n`;
105
+ }
106
+
107
+ function ensureBaselineProfilePlugin() {
108
+ const rootBuildGradle = path.join(androidDir, 'build.gradle');
109
+ patchFile(rootBuildGradle, (content) =>
110
+ insertAfter(
111
+ content,
112
+ "classpath 'com.google.gms:google-services:4.4.4'",
113
+ "\n classpath 'androidx.baselineprofile:androidx.baselineprofile.gradle.plugin:1.4.1'"
114
+ )
115
+ );
116
+ }
117
+
118
+ function ensureSettingsModule() {
119
+ const settingsGradle = path.join(androidDir, 'settings.gradle');
120
+ patchFile(settingsGradle, (content) =>
121
+ insertAfter(
122
+ content,
123
+ "include ':app'",
124
+ "\ninclude ':baselineprofile'\nproject(':baselineprofile').projectDir = new File('./baselineprofile/')\n"
125
+ )
126
+ );
127
+ }
128
+
129
+ function ensureAppGradle() {
130
+ const appBuildGradle = path.join(androidDir, 'app', 'build.gradle');
131
+ patchFile(appBuildGradle, (content) => {
132
+ let next = insertAfter(
133
+ content,
134
+ "apply plugin: 'com.android.application'",
135
+ "\napply plugin: 'androidx.baselineprofile'"
136
+ );
137
+
138
+ if (!next.includes('androidx.profileinstaller:profileinstaller')) {
139
+ next = insertBeforeClosingBrace(
140
+ next,
141
+ 'dependencies {',
142
+ ' implementation "androidx.profileinstaller:profileinstaller:1.4.1"'
143
+ );
144
+ }
145
+
146
+ if (!next.includes("baselineProfile project(':baselineprofile')")) {
147
+ next = insertBeforeClosingBrace(
148
+ next,
149
+ 'dependencies {',
150
+ " baselineProfile project(':baselineprofile')"
151
+ );
152
+ }
153
+
154
+ if (!next.includes('automaticGenerationDuringBuild false')) {
155
+ next = insertAfter(
156
+ next,
157
+ "apply from: 'capacitor.build.gradle'",
158
+ '\n\nbaselineProfile {\n automaticGenerationDuringBuild false\n saveInSrc true\n mergeIntoMain true\n}\n'
159
+ );
160
+ }
161
+
162
+ return next;
163
+ });
164
+ }
165
+
166
+ function ensureBaselineProfileModule(appId) {
167
+ const safeAppId = toSafeJavaPackageName(appId);
168
+ const modulePackage = `${safeAppId}.baselineprofile`;
169
+
170
+ writeFile(path.join(androidDir, 'baselineprofile', 'build.gradle'), getBaselineProfileBuildGradle(modulePackage));
171
+ writeFile(
172
+ path.join(androidDir, 'baselineprofile', 'src', 'main', 'AndroidManifest.xml'),
173
+ baselineProfileManifest
174
+ );
175
+ writeFile(
176
+ path.join(
177
+ androidDir,
178
+ 'baselineprofile',
179
+ 'src',
180
+ 'main',
181
+ 'java',
182
+ ...safeAppId.split('.'),
183
+ 'baselineprofile',
184
+ 'BaselineProfileGenerator.java'
185
+ ),
186
+ getBaselineProfileGenerator(modulePackage, appId)
187
+ );
188
+ console.log('Ensured android/baselineprofile module');
189
+ }
190
+
191
+ function syncGeneratedProfileBackup() {
192
+ const generatedExists = fs.existsSync(generatedProfile);
193
+ const backupExists = fs.existsSync(backupProfile);
194
+
195
+ if (!generatedExists && !backupExists) {
196
+ console.log('No baseline-prof.txt found yet. Run: cd android && .\\gradlew.bat :app:generateBaselineProfile --console=plain');
197
+ return;
198
+ }
199
+
200
+ if (generatedExists && !backupExists) {
201
+ fs.mkdirSync(path.dirname(backupProfile), { recursive: true });
202
+ fs.copyFileSync(generatedProfile, backupProfile);
203
+ console.log('Backed up generated baseline profile to baselineProfiles/baseline-prof.txt');
204
+ return;
205
+ }
206
+
207
+ if (!generatedExists && backupExists) {
208
+ fs.mkdirSync(path.dirname(generatedProfile), { recursive: true });
209
+ fs.copyFileSync(backupProfile, generatedProfile);
210
+ console.log('Restored baseline profile into android/app/src/main/generated/baselineProfiles');
211
+ return;
212
+ }
213
+
214
+ const generatedTime = fs.statSync(generatedProfile).mtimeMs;
215
+ const backupTime = fs.statSync(backupProfile).mtimeMs;
216
+ if (generatedTime >= backupTime) {
217
+ fs.copyFileSync(generatedProfile, backupProfile);
218
+ console.log('Updated baseline profile backup from generated profile');
219
+ } else {
220
+ fs.copyFileSync(backupProfile, generatedProfile);
221
+ console.log('Restored generated baseline profile from backup');
222
+ }
223
+ }
224
+
225
+ function main() {
226
+ const appId = getAppIdFromCapacitorConfig();
227
+
228
+ if (!fs.existsSync(androidDir)) {
229
+ console.log('Android platform folder is missing. Run `npx cap add android` first, then rerun this script.');
230
+ return;
231
+ }
232
+
233
+ ensureBaselineProfilePlugin();
234
+ ensureSettingsModule();
235
+ ensureAppGradle();
236
+ ensureBaselineProfileModule(appId);
237
+ syncGeneratedProfileBackup();
238
+ console.log('Baseline Profile setup is ready.');
239
+ }
240
+
241
+ main();
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
- {
2
- "name": "codeplay-common",
3
- "version": "3.2.19",
4
- "description": "Common build scripts and files",
5
- "scripts": {
6
- "postinstall": "node scripts/sync-files.js",
7
- "preinstall": "node scripts/uninstall.js"
8
- },
9
- "repository": {
10
- "type": "git",
11
- "url": "https://github.com/merbin2012/codeplay-common.git"
12
- },
13
- "author": "Codeplay Technologies",
14
- "license": "MIT"
15
- }
16
-
1
+ {
2
+ "name": "codeplay-common",
3
+ "version": "4.0.1",
4
+ "description": "Common build scripts and files",
5
+ "scripts": {
6
+ "postinstall": "node scripts/sync-files.js",
7
+ "preinstall": "node scripts/uninstall.js"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/merbin2012/codeplay-common.git"
12
+ },
13
+ "author": "Codeplay Technologies",
14
+ "license": "MIT"
15
+ }
16
+