@capacitor/cli 9.0.0-alpha.8 → 9.0.0-beta.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/assets/android-template.tar.gz +0 -0
- package/assets/capacitor-cordova-android-plugins.tar.gz +0 -0
- package/assets/capacitor-cordova-ios-plugins.tar.gz +0 -0
- package/assets/ios-pods-template.tar.gz +0 -0
- package/assets/ios-spm-template.tar.gz +0 -0
- package/dist/android/network-security-config.js +230 -0
- package/dist/android/update.js +5 -1
- package/dist/cordova.js +8 -17
- package/dist/declarations.d.ts +8 -41
- package/dist/index.js +4 -14
- package/dist/ios/update.js +8 -18
- package/dist/ipc.js +51 -11
- package/dist/run-context/declared.js +54 -0
- package/dist/run-context/index.js +111 -0
- package/dist/run-context/registry.js +45 -0
- package/dist/run-context/signals.js +115 -0
- package/dist/run-context/types.js +2 -0
- package/dist/run-context/unknown.js +46 -0
- package/dist/run-context/version.js +74 -0
- package/dist/tasks/copy.js +3 -1
- package/dist/tasks/doctor.js +4 -1
- package/dist/tasks/migrate-debug-xcconfig.js +60 -0
- package/dist/tasks/migrate-spm.js +0 -1
- package/dist/tasks/migrate-swiftui.js +344 -54
- package/dist/tasks/migrate-to-modern-project.js +27 -0
- package/dist/tasks/migrate.js +40 -2
- package/dist/tasks/run.js +8 -3
- package/dist/tasks/telemetry.js +8 -1
- package/dist/telemetry.js +303 -57
- package/dist/util/capacitor-package.js +68 -146
- package/dist/util/cordova-ios.js +9 -11
- package/dist/util/npm.js +120 -0
- package/dist/util/spm.js +35 -25
- package/dist/util/xcode.js +193 -2
- package/package.json +3 -2
package/dist/tasks/migrate.js
CHANGED
|
@@ -12,10 +12,14 @@ const errors_1 = require("../errors");
|
|
|
12
12
|
const common_2 = require("../ios/common");
|
|
13
13
|
const log_1 = require("../log");
|
|
14
14
|
const fs_1 = require("../util/fs");
|
|
15
|
+
const npm_1 = require("../util/npm");
|
|
16
|
+
const spm_1 = require("../util/spm");
|
|
15
17
|
const subprocess_1 = require("../util/subprocess");
|
|
16
18
|
const template_1 = require("../util/template");
|
|
19
|
+
const migrate_debug_xcconfig_1 = require("./migrate-debug-xcconfig");
|
|
17
20
|
const migrate_edge_to_edge_1 = require("./migrate-edge-to-edge");
|
|
18
21
|
const migrate_swiftui_1 = require("./migrate-swiftui");
|
|
22
|
+
const migrate_to_modern_project_1 = require("./migrate-to-modern-project");
|
|
19
23
|
let allDependencies = {};
|
|
20
24
|
const libs = ['@capacitor/core', '@capacitor/cli', '@capacitor/ios', '@capacitor/android'];
|
|
21
25
|
const plugins = [
|
|
@@ -47,9 +51,10 @@ const plugins = [
|
|
|
47
51
|
];
|
|
48
52
|
const coreVersion = 'next';
|
|
49
53
|
const pluginVersion = 'next';
|
|
50
|
-
const gradleVersion = '9.
|
|
54
|
+
const gradleVersion = '9.8.2';
|
|
51
55
|
const iOSVersion = '16';
|
|
52
56
|
let installFailed = false;
|
|
57
|
+
let scriptsBlocked = false;
|
|
53
58
|
// AGP 9 changed the defaults of these properties and Android Studio's AGP Upgrade Assistant
|
|
54
59
|
// writes them into gradle.properties as compatibility shims. Capacitor apps need none of them,
|
|
55
60
|
// most are deprecated and will be removed in AGP 10, and some (android.builtInKotlin,
|
|
@@ -135,6 +140,13 @@ async function migrateCommand(config, noprompt, packagemanager) {
|
|
|
135
140
|
log_1.logger.error(`${installerType} install failed. Try deleting node_modules folder and running ${colors_1.default.input(`${installerType} install --force`)} manually.`);
|
|
136
141
|
installFailed = true;
|
|
137
142
|
}
|
|
143
|
+
if (runNpmInstall && !installFailed) {
|
|
144
|
+
const blocked = await (0, npm_1.getBlockedInstallScripts)(config.app.rootDir, installerType);
|
|
145
|
+
if (blocked.length > 0) {
|
|
146
|
+
(0, npm_1.logBlockedInstallScripts)(blocked);
|
|
147
|
+
scriptsBlocked = (0, npm_1.hasBlockedCapacitorScripts)(blocked);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
138
150
|
// Update iOS Projects
|
|
139
151
|
if (allDependencies['@capacitor/ios'] && (0, fs_extra_1.existsSync)(config.ios.platformDirAbs)) {
|
|
140
152
|
const currentiOSVersion = (0, common_2.getMajoriOSVersion)(config);
|
|
@@ -156,7 +168,18 @@ async function migrateCommand(config, noprompt, packagemanager) {
|
|
|
156
168
|
await (0, common_1.runTask)(`Migrating AppDelegate.swift`, () => {
|
|
157
169
|
return updateAppDelegate((0, path_1.join)(config.ios.nativeTargetDirAbs, 'AppDelegate.swift'));
|
|
158
170
|
});
|
|
171
|
+
await (0, common_1.runTask)(`Removing debug.xcconfig.`, () => {
|
|
172
|
+
return (0, migrate_debug_xcconfig_1.removeDebugXcconfig)(config);
|
|
173
|
+
});
|
|
174
|
+
await (0, common_1.runTask)(`Migrating to recommended project settings`, () => {
|
|
175
|
+
return (0, migrate_to_modern_project_1.migrateToModernProject)(config);
|
|
176
|
+
});
|
|
159
177
|
await (0, migrate_swiftui_1.migrateToSwiftUI)(config);
|
|
178
|
+
if ((await config.ios.packageManager) === 'SPM') {
|
|
179
|
+
await (0, common_1.runTask)('Ensuring Package.resolved is in CapApp-SPM/.gitignore.', () => {
|
|
180
|
+
return (0, spm_1.ensureSPMGitignoreEntry)(config);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
160
183
|
}
|
|
161
184
|
if (!installFailed) {
|
|
162
185
|
await (0, common_1.runTask)(`Running cap sync.`, () => {
|
|
@@ -242,6 +265,9 @@ async function migrateCommand(config, noprompt, packagemanager) {
|
|
|
242
265
|
return;
|
|
243
266
|
}
|
|
244
267
|
txt = txt.replace(/= {2}'/g, `= '`);
|
|
268
|
+
if (txt.includes('androidxCoreKTXVersion')) {
|
|
269
|
+
log_1.logger.warn(`androidxCoreKTXVersion might not be needed anymore, check your plugins`);
|
|
270
|
+
}
|
|
245
271
|
(0, fs_extra_1.writeFileSync)(variablesPath, txt, { encoding: 'utf-8' });
|
|
246
272
|
for (const variable of Object.keys(variablesAndClasspaths.variables)) {
|
|
247
273
|
let replaceStart = `${variable} = '`;
|
|
@@ -272,6 +298,7 @@ async function migrateCommand(config, noprompt, packagemanager) {
|
|
|
272
298
|
firebaseMessagingVersion: '25.0.1',
|
|
273
299
|
playServicesLocationVersion: '21.4.0',
|
|
274
300
|
androidxBrowserVersion: '1.10.0',
|
|
301
|
+
androidxCoreKTXVersion: '1.19.1',
|
|
275
302
|
androidxMaterialVersion: '1.14.0',
|
|
276
303
|
androidxExifInterfaceVersion: '1.4.2',
|
|
277
304
|
googleMapsPlayServicesVersion: '20.0.0',
|
|
@@ -292,9 +319,12 @@ async function migrateCommand(config, noprompt, packagemanager) {
|
|
|
292
319
|
await (0, common_1.runTask)(`Writing breaking changes.`, () => {
|
|
293
320
|
return writeBreakingChanges();
|
|
294
321
|
});
|
|
295
|
-
if (!installFailed) {
|
|
322
|
+
if (!installFailed && !scriptsBlocked) {
|
|
296
323
|
(0, log_1.logSuccess)(`Migration to Capacitor ${coreVersion} is complete. Run and test your app!`);
|
|
297
324
|
}
|
|
325
|
+
else if (scriptsBlocked) {
|
|
326
|
+
log_1.logger.warn(`Migration to Capacitor ${coreVersion} is incomplete because npm blocked install scripts. Approve them as described above, then run ${colors_1.default.input('npx cap sync')}.`);
|
|
327
|
+
}
|
|
298
328
|
else {
|
|
299
329
|
log_1.logger.warn(`Migration to Capacitor ${coreVersion} is incomplete. Check the log messages for more information.`);
|
|
300
330
|
}
|
|
@@ -475,6 +505,13 @@ async function updateAppBuildGradle(filename) {
|
|
|
475
505
|
}
|
|
476
506
|
let replaced = txt;
|
|
477
507
|
replaced = replaced.replace(`proguard-android.txt`, `proguard-android-optimize.txt`);
|
|
508
|
+
const releaseMinify = /(\brelease\s*\{[^}]*?minifyEnabled\s+)(true|false)/;
|
|
509
|
+
if (releaseMinify.test(replaced)) {
|
|
510
|
+
replaced = replaced.replace(releaseMinify, '$1true');
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
log_1.logger.warn(`Unable to set minifyEnabled true in the release block of ${filename}. Set it manually to turn on R8.`);
|
|
514
|
+
}
|
|
478
515
|
replaced = replaced.replace(' targetSdkVersion rootProject.ext.targetSdkVersion\n', '');
|
|
479
516
|
replaced = replaced.replace(` implementation project(':capacitor-cordova-android-plugins')\n`, '');
|
|
480
517
|
replaced = replaced.replace(`repositories {
|
|
@@ -649,5 +686,6 @@ exports.__testables = {
|
|
|
649
686
|
removeAGPMigrationProperties,
|
|
650
687
|
removeJCenter,
|
|
651
688
|
removeJCenterFromGradleFile,
|
|
689
|
+
updateAppBuildGradle,
|
|
652
690
|
updateGradleProperties,
|
|
653
691
|
};
|
package/dist/tasks/run.js
CHANGED
|
@@ -67,15 +67,17 @@ async function runCommand(config, selectedPlatformName, options) {
|
|
|
67
67
|
}
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
|
+
let cordovaPlugins;
|
|
70
71
|
try {
|
|
71
72
|
if (options.sync) {
|
|
72
73
|
await (0, sync_1.sync)(config, platformName, false, true);
|
|
73
74
|
}
|
|
74
|
-
const
|
|
75
|
+
const plugins = await (0, cordova_1.getCordovaPlugins)(config, platformName);
|
|
76
|
+
cordovaPlugins = plugins;
|
|
75
77
|
if (options.url) {
|
|
76
78
|
await livereload_1.CapLiveReloadHelper.editCapConfigForLiveReload(config, platformName, options);
|
|
77
79
|
if (platformName === config.android.name) {
|
|
78
|
-
await
|
|
80
|
+
await (0, cordova_1.writeCordovaAndroidManifest)(plugins, config, platformName, true);
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
await run(config, platformName, options);
|
|
@@ -84,7 +86,7 @@ async function runCommand(config, selectedPlatformName, options) {
|
|
|
84
86
|
.then(async () => {
|
|
85
87
|
await livereload_1.CapLiveReloadHelper.revertCapConfigForLiveReload();
|
|
86
88
|
if (platformName === config.android.name) {
|
|
87
|
-
await (0, cordova_1.writeCordovaAndroidManifest)(
|
|
89
|
+
await (0, cordova_1.writeCordovaAndroidManifest)(plugins, config, platformName, false);
|
|
88
90
|
}
|
|
89
91
|
})
|
|
90
92
|
.then(() => process.exit());
|
|
@@ -95,6 +97,9 @@ async function runCommand(config, selectedPlatformName, options) {
|
|
|
95
97
|
catch (e) {
|
|
96
98
|
if (options.url) {
|
|
97
99
|
await livereload_1.CapLiveReloadHelper.revertCapConfigForLiveReload();
|
|
100
|
+
if (cordovaPlugins && platformName === config.android.name) {
|
|
101
|
+
await (0, cordova_1.writeCordovaAndroidManifest)(cordovaPlugins, config, platformName, false).catch((revertError) => log_1.logger.warn(`Could not revert the live reload cleartext configuration: ${revertError}`));
|
|
102
|
+
}
|
|
98
103
|
}
|
|
99
104
|
if (!(0, errors_1.isFatal)(e)) {
|
|
100
105
|
(0, errors_1.fatal)((_a = e.stack) !== null && _a !== void 0 ? _a : e);
|
package/dist/tasks/telemetry.js
CHANGED
|
@@ -6,6 +6,7 @@ const colors_1 = tslib_1.__importDefault(require("../colors"));
|
|
|
6
6
|
const errors_1 = require("../errors");
|
|
7
7
|
const log_1 = require("../log");
|
|
8
8
|
const sysconfig_1 = require("../sysconfig");
|
|
9
|
+
const telemetry_1 = require("../telemetry");
|
|
9
10
|
const THANK_YOU = `\nThank you for helping to make Capacitor better! 💖` +
|
|
10
11
|
`\nInformation about the data we collect is available on our website: ${colors_1.default.strong('https://capacitorjs.com/docs/next/cli/telemetry')}\n`;
|
|
11
12
|
async function telemetryCommand(onOrOff) {
|
|
@@ -24,7 +25,13 @@ async function telemetryCommand(onOrOff) {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
else {
|
|
27
|
-
log_1.logger.info(`Telemetry is ${colors_1.default.strong(sysconfig.telemetry ? '
|
|
28
|
+
log_1.logger.info(`Telemetry is ${colors_1.default.strong(sysconfig.telemetry === false ? 'off' : 'on')}`);
|
|
29
|
+
}
|
|
30
|
+
// The stored setting is reported above because that is what this command
|
|
31
|
+
// writes, but the environment wins at send time. Saying so avoids the
|
|
32
|
+
// command contradicting what the CLI actually does.
|
|
33
|
+
if ((0, telemetry_1.isTelemetryDisabledByEnv)(process.env)) {
|
|
34
|
+
log_1.logger.info(`Telemetry is disabled for this environment by ${colors_1.default.input('CAP_TELEMETRY_DISABLED')}`);
|
|
28
35
|
}
|
|
29
36
|
}
|
|
30
37
|
exports.telemetryCommand = telemetryCommand;
|
package/dist/telemetry.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getIOSPackageManager = exports.sendMetric = exports.telemetryAction = void 0;
|
|
3
|
+
exports.getIOSPackageManager = exports.getProjectIdentity = exports.runContextFields = exports.isTelemetryEnabled = exports.isTelemetryDisabledByEnv = exports.needsAwaitedDelivery = exports.sendMetric = exports.getTelemetryVersion = exports.getTelemetryArguments = exports.getTelemetryError = exports.getTelemetryOptions = exports.telemetryAction = exports.TELEMETRY_SAFE_CHOICES = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const commander_1 = require("commander");
|
|
6
6
|
const debug_1 = tslib_1.__importDefault(require("debug"));
|
|
@@ -8,6 +8,7 @@ const fs_extra_1 = require("fs-extra");
|
|
|
8
8
|
const colors_1 = tslib_1.__importDefault(require("./colors"));
|
|
9
9
|
const ipc_1 = require("./ipc");
|
|
10
10
|
const log_1 = require("./log");
|
|
11
|
+
const run_context_1 = require("./run-context");
|
|
11
12
|
const sysconfig_1 = require("./sysconfig");
|
|
12
13
|
const subprocess_1 = require("./util/subprocess");
|
|
13
14
|
const term_1 = require("./util/term");
|
|
@@ -15,6 +16,58 @@ const debug = (0, debug_1.default)('capacitor:telemetry');
|
|
|
15
16
|
const THANK_YOU = `\nThank you for helping improve Capacitor by sharing anonymous usage data! 💖` +
|
|
16
17
|
`\nInformation about the data we collect is available on our website: ${colors_1.default.strong('https://capacitorjs.com/docs/next/cli/telemetry')}` +
|
|
17
18
|
`\nYou can disable telemetry at any time by using the ${colors_1.default.input('npx cap telemetry off')} command.`;
|
|
19
|
+
/**
|
|
20
|
+
* The option values that are safe to report, keyed by the option's resolved
|
|
21
|
+
* name (commander's `attributeName()`, so `--signing-type` is `signingType`).
|
|
22
|
+
* This is an allowlist by design: a denylist of password-like names would leak
|
|
23
|
+
* the first sensitive option someone adds.
|
|
24
|
+
*
|
|
25
|
+
* The option declarations in `index.ts` spread these same lists into
|
|
26
|
+
* `.choices()`, so an option cannot offer a choice this does not list.
|
|
27
|
+
*/
|
|
28
|
+
exports.TELEMETRY_SAFE_CHOICES = {
|
|
29
|
+
androidreleasetype: ['AAB', 'APK'],
|
|
30
|
+
signingType: ['apksigner', 'jarsigner'],
|
|
31
|
+
xcodeExportMethod: [
|
|
32
|
+
'app-store-connect',
|
|
33
|
+
'release-testing',
|
|
34
|
+
'enterprise',
|
|
35
|
+
'debugging',
|
|
36
|
+
'developer-id',
|
|
37
|
+
'mac-application',
|
|
38
|
+
'validation',
|
|
39
|
+
],
|
|
40
|
+
xcodeSigningStyle: ['automatic', 'manual'],
|
|
41
|
+
};
|
|
42
|
+
const telemetrySafeChoices = new Map(Object.entries(exports.TELEMETRY_SAFE_CHOICES).map(([name, choices]) => [name, new Set(choices)]));
|
|
43
|
+
/**
|
|
44
|
+
* Positional argument values that are safe to report. Everything else is
|
|
45
|
+
* dropped, since arguments like `cap init`'s app name are user content.
|
|
46
|
+
*/
|
|
47
|
+
const TELEMETRY_ARGUMENT_ALLOWLIST = new Set(['android', 'ios', 'web']);
|
|
48
|
+
/**
|
|
49
|
+
* Upper bound on the reported error, so a failed Gradle or Xcode build cannot
|
|
50
|
+
* ship its entire log as a metric.
|
|
51
|
+
*/
|
|
52
|
+
const TELEMETRY_ERROR_MAX_LENGTH = 500;
|
|
53
|
+
/**
|
|
54
|
+
* Shortest value worth scrubbing out of a reported error. An empty string would
|
|
55
|
+
* match between every character, and a one- or two-character option value--a
|
|
56
|
+
* port, a target index--appears throughout unrelated output, so redacting
|
|
57
|
+
* either destroys the error instead of sanitizing it.
|
|
58
|
+
*/
|
|
59
|
+
const TELEMETRY_REDACTION_MIN_LENGTH = 3;
|
|
60
|
+
/**
|
|
61
|
+
* The characters a dependency spec may contain to be reported as-is. A version
|
|
62
|
+
* range has none of the `:/@` a URL or path spec needs, so specs such as
|
|
63
|
+
* `git+https://x-access-token:<token>@github.com/acme/plugin.git` and
|
|
64
|
+
* `file:/Users/<name>/dev/plugin` are reported as {@link NON_REGISTRY_VERSION}
|
|
65
|
+
* instead of shipping a credential or a local path. The pnpm and yarn workspace
|
|
66
|
+
* protocols are allowed through as the only exception, since they resolve
|
|
67
|
+
* within the repo and so cannot carry either.
|
|
68
|
+
*/
|
|
69
|
+
const TELEMETRY_SAFE_VERSION_PATTERN = /^(?:(?:workspace|catalog):)?[\w.\-+^~><=|* ]*$/;
|
|
70
|
+
const NON_REGISTRY_VERSION = 'non-registry';
|
|
18
71
|
function telemetryAction(config, action) {
|
|
19
72
|
return async (...actionArgs) => {
|
|
20
73
|
const start = new Date();
|
|
@@ -22,7 +75,6 @@ function telemetryAction(config, action) {
|
|
|
22
75
|
// element or second to last if there are additional options (via `.allowUnknownOption()`)
|
|
23
76
|
const lastArg = actionArgs[actionArgs.length - 1];
|
|
24
77
|
const cmd = lastArg instanceof commander_1.Command ? lastArg : actionArgs[actionArgs.length - 2];
|
|
25
|
-
const command = getFullCommandName(cmd);
|
|
26
78
|
let error;
|
|
27
79
|
try {
|
|
28
80
|
await action(...actionArgs);
|
|
@@ -31,66 +83,263 @@ function telemetryAction(config, action) {
|
|
|
31
83
|
error = e;
|
|
32
84
|
}
|
|
33
85
|
const end = new Date();
|
|
34
|
-
|
|
35
|
-
const packages = Object.entries({
|
|
36
|
-
...config.app.package.devDependencies,
|
|
37
|
-
...config.app.package.dependencies,
|
|
38
|
-
});
|
|
39
|
-
// Only collect packages in the capacitor org:
|
|
40
|
-
// https://www.npmjs.com/org/capacitor
|
|
41
|
-
const capacitorPackages = packages.filter(([k]) => k.startsWith('@capacitor/'));
|
|
42
|
-
const versions = capacitorPackages.map(([k, v]) => [
|
|
43
|
-
`${k.replace(/^@capacitor\//, '').replace(/-/g, '_')}_version`,
|
|
44
|
-
v,
|
|
45
|
-
]);
|
|
46
|
-
const data = {
|
|
47
|
-
app_id: await getAppIdentifier(config),
|
|
48
|
-
command,
|
|
49
|
-
arguments: cmd.args.join(' '),
|
|
50
|
-
options: JSON.stringify(cmd.opts()),
|
|
51
|
-
duration,
|
|
52
|
-
error: error ? (error.message ? error.message : String(error)) : null,
|
|
53
|
-
node_version: process.version,
|
|
54
|
-
os: config.cli.os,
|
|
55
|
-
ios_package_manager: await getIOSPackageManager(config),
|
|
56
|
-
...Object.fromEntries(versions),
|
|
57
|
-
};
|
|
58
|
-
debug('metric payload: %O', data);
|
|
59
|
-
if ((0, term_1.isInteractive)()) {
|
|
60
|
-
let sysconfig = await (0, sysconfig_1.readConfig)();
|
|
61
|
-
if (!error && typeof sysconfig.telemetry === 'undefined') {
|
|
62
|
-
// Telemetry is opt-out; turn telemetry on then inform the user how to opt-out.
|
|
63
|
-
sysconfig = { ...sysconfig, telemetry: true };
|
|
64
|
-
await (0, sysconfig_1.writeConfig)(sysconfig);
|
|
65
|
-
log_1.output.write(THANK_YOU);
|
|
66
|
-
}
|
|
67
|
-
await sendMetric(sysconfig, 'capacitor_cli_command', data);
|
|
68
|
-
}
|
|
86
|
+
await reportCommand(config, cmd, end.getTime() - start.getTime(), error);
|
|
69
87
|
if (error) {
|
|
70
88
|
throw error;
|
|
71
89
|
}
|
|
72
90
|
};
|
|
73
91
|
}
|
|
74
92
|
exports.telemetryAction = telemetryAction;
|
|
93
|
+
/**
|
|
94
|
+
* Build and deliver the metric for one completed command.
|
|
95
|
+
*/
|
|
96
|
+
async function reportCommand(config, cmd, duration, error) {
|
|
97
|
+
// Read before detection and before stored consent, so the opt-out holds in
|
|
98
|
+
// environments with no filesystem to have stored one.
|
|
99
|
+
if (isTelemetryDisabledByEnv(process.env)) {
|
|
100
|
+
debug('CAP_TELEMETRY_DISABLED is set--not collecting');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const runContext = (0, run_context_1.detectRunContext)();
|
|
104
|
+
let sysconfig = await (0, sysconfig_1.readConfig)();
|
|
105
|
+
// The notice and the persisted default only mean anything where a person can
|
|
106
|
+
// see them, so a headless run leaves the choice unmade rather than
|
|
107
|
+
// committing one it was never offered.
|
|
108
|
+
if ((0, term_1.isInteractive)() && !error && typeof sysconfig.telemetry === 'undefined') {
|
|
109
|
+
// Telemetry is opt-out; turn telemetry on then inform the user how to opt-out.
|
|
110
|
+
sysconfig = { ...sysconfig, telemetry: true };
|
|
111
|
+
await (0, sysconfig_1.writeConfig)(sysconfig);
|
|
112
|
+
log_1.output.write(THANK_YOU);
|
|
113
|
+
}
|
|
114
|
+
// Settled before the payload is built rather than at delivery: identifying
|
|
115
|
+
// the project walks the whole commit graph.
|
|
116
|
+
if (!isTelemetryEnabled(process.env, sysconfig, runContext)) {
|
|
117
|
+
debug('Telemetry is off for this run--not collecting');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const packages = Object.entries({
|
|
121
|
+
...config.app.package.devDependencies,
|
|
122
|
+
...config.app.package.dependencies,
|
|
123
|
+
});
|
|
124
|
+
// Only collect packages in the capacitor org:
|
|
125
|
+
// https://www.npmjs.com/org/capacitor
|
|
126
|
+
const capacitorPackages = packages.filter(([k]) => k.startsWith('@capacitor/'));
|
|
127
|
+
const versions = capacitorPackages.map(([k, v]) => [
|
|
128
|
+
`${k.replace(/^@capacitor\//, '').replace(/-/g, '_')}_version`,
|
|
129
|
+
getTelemetryVersion(v),
|
|
130
|
+
]);
|
|
131
|
+
// Both identifiers derive from this, and `git rev-list --max-parents=0`
|
|
132
|
+
// walks the whole history — so it is fetched once for the pair.
|
|
133
|
+
const firstCommit = await getFirstCommit(config);
|
|
134
|
+
const options = getTelemetryOptions(cmd);
|
|
135
|
+
const data = {
|
|
136
|
+
app_id: await getAppIdentifier(firstCommit),
|
|
137
|
+
...(await getProjectIdentity(firstCommit, runContext)),
|
|
138
|
+
command: getFullCommandName(cmd),
|
|
139
|
+
arguments: getTelemetryArguments(cmd),
|
|
140
|
+
options: JSON.stringify(options),
|
|
141
|
+
duration,
|
|
142
|
+
error: getTelemetryError(cmd, error, config, options),
|
|
143
|
+
node_version: process.version,
|
|
144
|
+
os: config.cli.os,
|
|
145
|
+
ios_package_manager: await getIOSPackageManager(config),
|
|
146
|
+
...runContextFields(runContext),
|
|
147
|
+
...Object.fromEntries(versions),
|
|
148
|
+
};
|
|
149
|
+
debug('metric payload: %O', data);
|
|
150
|
+
await sendMetric(sysconfig, 'capacitor_cli_command', data, runContext);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Build the reported option map: every option name the command resolved, but
|
|
154
|
+
* only the values that cannot carry a secret. Booleans are kept because they
|
|
155
|
+
* cannot encode one, allowlisted options are kept when the value is one this
|
|
156
|
+
* file lists, and every other value--keystore passwords, paths, signing
|
|
157
|
+
* identities--becomes `null`.
|
|
158
|
+
*/
|
|
159
|
+
function getTelemetryOptions(cmd) {
|
|
160
|
+
return Object.fromEntries(Object.entries(cmd.opts()).map(([name, value]) => [name, isTelemetrySafeValue(name, value) ? value : null]));
|
|
161
|
+
}
|
|
162
|
+
exports.getTelemetryOptions = getTelemetryOptions;
|
|
163
|
+
function isTelemetrySafeValue(name, value) {
|
|
164
|
+
var _a, _b;
|
|
165
|
+
if (typeof value === 'boolean') {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
return typeof value === 'string' && ((_b = (_a = telemetrySafeChoices.get(name)) === null || _a === void 0 ? void 0 : _a.has(value)) !== null && _b !== void 0 ? _b : false);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The signing credentials `capacitor.config` can supply. `cap build` falls back
|
|
172
|
+
* to the config for each of these when the flag is absent, and the resolved
|
|
173
|
+
* value still reaches the `xcodebuild` and `jarsigner` command lines--so it can
|
|
174
|
+
* come back in their output without ever having appeared in `cmd.opts()`.
|
|
175
|
+
*/
|
|
176
|
+
function getConfiguredCredentials(config) {
|
|
177
|
+
return [
|
|
178
|
+
config.android.buildOptions.keystorePath,
|
|
179
|
+
config.android.buildOptions.keystorePassword,
|
|
180
|
+
config.android.buildOptions.keystoreAlias,
|
|
181
|
+
config.android.buildOptions.keystoreAliasPassword,
|
|
182
|
+
config.ios.buildOptions.teamId,
|
|
183
|
+
config.ios.buildOptions.signingCertificate,
|
|
184
|
+
config.ios.buildOptions.provisioningProfile,
|
|
185
|
+
].filter((value) => typeof value === 'string');
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Build the reported error, scrubbed of every option value and positional
|
|
189
|
+
* argument the rest of the payload drops, plus the signing credentials the
|
|
190
|
+
* config can supply in place of a flag, then truncated.
|
|
191
|
+
*
|
|
192
|
+
* Failed subprocesses rethrow the tool's combined output, which echoes back
|
|
193
|
+
* the very values that were redacted out of the option map--`xcodebuild`
|
|
194
|
+
* prints its command-line build settings, `jarsigner` prints the keystore it
|
|
195
|
+
* was pointed at.
|
|
196
|
+
*/
|
|
197
|
+
function getTelemetryError(cmd, error, config, options = getTelemetryOptions(cmd)) {
|
|
198
|
+
if (!error) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const values = cmd.opts();
|
|
202
|
+
const redacted = [
|
|
203
|
+
...Object.entries(options)
|
|
204
|
+
.filter(([, value]) => value === null)
|
|
205
|
+
.map(([name]) => values[name]),
|
|
206
|
+
...cmd.args.filter((arg) => !TELEMETRY_ARGUMENT_ALLOWLIST.has(arg)),
|
|
207
|
+
...getConfiguredCredentials(config),
|
|
208
|
+
]
|
|
209
|
+
.filter((value) => typeof value === 'string' && value.length >= TELEMETRY_REDACTION_MIN_LENGTH)
|
|
210
|
+
.sort((a, b) => b.length - a.length);
|
|
211
|
+
const message = redacted.reduce((message, value) => message.replaceAll(value, '<redacted>'), error.message ? error.message : String(error));
|
|
212
|
+
return message.slice(0, TELEMETRY_ERROR_MAX_LENGTH);
|
|
213
|
+
}
|
|
214
|
+
exports.getTelemetryError = getTelemetryError;
|
|
215
|
+
/**
|
|
216
|
+
* Build the reported positional arguments, keeping only allowlisted values.
|
|
217
|
+
*/
|
|
218
|
+
function getTelemetryArguments(cmd) {
|
|
219
|
+
return cmd.args.filter((arg) => TELEMETRY_ARGUMENT_ALLOWLIST.has(arg)).join(' ');
|
|
220
|
+
}
|
|
221
|
+
exports.getTelemetryArguments = getTelemetryArguments;
|
|
222
|
+
/**
|
|
223
|
+
* Build the reported version of a Capacitor dependency, keeping the spec only
|
|
224
|
+
* when it is a version range. Specs that point somewhere--a git URL, a local
|
|
225
|
+
* path--are reported as being just that, since they can carry a token or a
|
|
226
|
+
* username.
|
|
227
|
+
*/
|
|
228
|
+
function getTelemetryVersion(spec) {
|
|
229
|
+
return spec && TELEMETRY_SAFE_VERSION_PATTERN.test(spec) ? spec : NON_REGISTRY_VERSION;
|
|
230
|
+
}
|
|
231
|
+
exports.getTelemetryVersion = getTelemetryVersion;
|
|
75
232
|
/**
|
|
76
233
|
* If telemetry is enabled, send a metric via IPC to a forked process for uploading.
|
|
77
234
|
*/
|
|
78
|
-
async function sendMetric(sysconfig, name, data) {
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
235
|
+
async function sendMetric(sysconfig, name, data, runContext) {
|
|
236
|
+
if (!isTelemetryEnabled(process.env, sysconfig, runContext)) {
|
|
237
|
+
debug('Telemetry is off (user choice, unshown notice, opt-out variable, or CI)--not sending metric');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const message = {
|
|
241
|
+
name,
|
|
242
|
+
timestamp: new Date().toISOString(),
|
|
243
|
+
session_id: sysconfig.machine,
|
|
244
|
+
source: 'capacitor_cli',
|
|
245
|
+
value: data,
|
|
246
|
+
};
|
|
247
|
+
if (needsAwaitedDelivery(runContext)) {
|
|
248
|
+
await (0, ipc_1.deliver)(message);
|
|
88
249
|
}
|
|
89
250
|
else {
|
|
90
|
-
|
|
251
|
+
await (0, ipc_1.send)({ type: 'telemetry', data: message });
|
|
91
252
|
}
|
|
92
253
|
}
|
|
93
254
|
exports.sendMetric = sendMetric;
|
|
255
|
+
/**
|
|
256
|
+
* Whether the POST has to finish before the CLI exits.
|
|
257
|
+
*
|
|
258
|
+
* Delivery normally goes to a forked process and costs the command nothing.
|
|
259
|
+
* That process is not `detached` and still has to cold-start Node before it
|
|
260
|
+
* reaches the network, so an environment reclaimed the moment the CLI returns
|
|
261
|
+
* takes it with it mid-request. Anything that outlives the command — a
|
|
262
|
+
* harness on a developer's machine included — detaches like a human run.
|
|
263
|
+
*/
|
|
264
|
+
function needsAwaitedDelivery(runContext) {
|
|
265
|
+
return (0, run_context_1.isAgentDriven)(runContext) && runContext.surface === 'cloud-sandbox';
|
|
266
|
+
}
|
|
267
|
+
exports.needsAwaitedDelivery = needsAwaitedDelivery;
|
|
268
|
+
function isTelemetryDisabledByEnv(env) {
|
|
269
|
+
return Boolean(env.CAP_TELEMETRY_DISABLED);
|
|
270
|
+
}
|
|
271
|
+
exports.isTelemetryDisabledByEnv = isTelemetryDisabledByEnv;
|
|
272
|
+
/**
|
|
273
|
+
* Whether a metric may be delivered for this run.
|
|
274
|
+
*
|
|
275
|
+
* An explicit choice always wins. Only the default for an unmade choice
|
|
276
|
+
* depends on the run: the notice is printed at a terminal and nowhere else, so
|
|
277
|
+
* a first run inside a Docker build or a git hook has had no chance to show it
|
|
278
|
+
* and reporting there would put the data ahead of the disclosure. An agent
|
|
279
|
+
* sandbox never reaches that path at all, which is why `undefined` means yes
|
|
280
|
+
* there and no everywhere else.
|
|
281
|
+
*/
|
|
282
|
+
function isTelemetryEnabled(env, sysconfig, runContext) {
|
|
283
|
+
if (isTelemetryDisabledByEnv(env)) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
if (!runContext.report) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
if (typeof sysconfig.telemetry === 'boolean') {
|
|
290
|
+
return sysconfig.telemetry;
|
|
291
|
+
}
|
|
292
|
+
return (0, run_context_1.isAgentDriven)(runContext);
|
|
293
|
+
}
|
|
294
|
+
exports.isTelemetryEnabled = isTelemetryEnabled;
|
|
295
|
+
/**
|
|
296
|
+
* Map the run context onto payload fields. Agent keys are omitted rather than
|
|
297
|
+
* nulled when no agent was detected, so "no agent" and "an agent we couldn't
|
|
298
|
+
* name" stay distinguishable downstream.
|
|
299
|
+
*/
|
|
300
|
+
function runContextFields(runContext) {
|
|
301
|
+
return {
|
|
302
|
+
surface: runContext.surface,
|
|
303
|
+
confidence: runContext.confidence,
|
|
304
|
+
...(runContext.agentId ? { agent_id: runContext.agentId } : {}),
|
|
305
|
+
...(runContext.agentVersion ? { agent_version: runContext.agentVersion } : {}),
|
|
306
|
+
...(runContext.agentDetectedBy ? { agent_detected_by: runContext.agentDetectedBy } : {}),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
exports.runContextFields = runContextFields;
|
|
310
|
+
/**
|
|
311
|
+
* Identify the project this run belongs to, so repeated runs against one app
|
|
312
|
+
* count once however many machines they came from.
|
|
313
|
+
*
|
|
314
|
+
* The repository's first commit is the same on every machine, which makes it
|
|
315
|
+
* the preferred key — though where there is one this is `app_id` under a
|
|
316
|
+
* different hash, and `project_id_source` is what tells the two apart. The
|
|
317
|
+
* case this exists for is a freshly scaffolded app with no commit yet, where
|
|
318
|
+
* the harness session identifier stands in — hashed here, never transmitted,
|
|
319
|
+
* which is why the run context carries only the variable's name.
|
|
320
|
+
*/
|
|
321
|
+
async function getProjectIdentity(firstCommit, runContext, env = process.env) {
|
|
322
|
+
if (firstCommit) {
|
|
323
|
+
return { project_id: await hash(firstCommit), project_id_source: 'repository' };
|
|
324
|
+
}
|
|
325
|
+
const session = runContext.sessionVar ? env[runContext.sessionVar] : undefined;
|
|
326
|
+
if (session) {
|
|
327
|
+
return { project_id: await hash(session), project_id_source: 'session' };
|
|
328
|
+
}
|
|
329
|
+
debug('Could not obtain a project identifier');
|
|
330
|
+
return { project_id: null, project_id_source: 'none' };
|
|
331
|
+
}
|
|
332
|
+
exports.getProjectIdentity = getProjectIdentity;
|
|
333
|
+
async function hash(value) {
|
|
334
|
+
const { createHash } = await Promise.resolve().then(() => tslib_1.__importStar(require('crypto')));
|
|
335
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
336
|
+
}
|
|
337
|
+
async function getFirstCommit(config) {
|
|
338
|
+
const output = await (0, subprocess_1.getCommandOutput)('git', ['rev-list', '--max-parents=0', 'HEAD'], {
|
|
339
|
+
cwd: config.app.rootDir,
|
|
340
|
+
});
|
|
341
|
+
return (output === null || output === void 0 ? void 0 : output.split('\n')[0]) || undefined;
|
|
342
|
+
}
|
|
94
343
|
/**
|
|
95
344
|
* Resolve the iOS dependency manager for telemetry. Returns 'unknown' for
|
|
96
345
|
* non-iOS projects or when detection throws, so callers never have to handle
|
|
@@ -110,20 +359,17 @@ async function getIOSPackageManager(config) {
|
|
|
110
359
|
}
|
|
111
360
|
exports.getIOSPackageManager = getIOSPackageManager;
|
|
112
361
|
/**
|
|
113
|
-
* Get a unique anonymous identifier for this app
|
|
362
|
+
* Get a unique anonymous identifier for this app from the repository's first
|
|
363
|
+
* commit, which should be universally unique.
|
|
114
364
|
*/
|
|
115
|
-
async function getAppIdentifier(
|
|
116
|
-
|
|
117
|
-
// get the first commit hash, which should be universally unique
|
|
118
|
-
const output = await (0, subprocess_1.getCommandOutput)('git', ['rev-list', '--max-parents=0', 'HEAD'], { cwd: config.app.rootDir });
|
|
119
|
-
const firstLine = output === null || output === void 0 ? void 0 : output.split('\n')[0];
|
|
120
|
-
if (!firstLine) {
|
|
365
|
+
async function getAppIdentifier(firstCommit) {
|
|
366
|
+
if (!firstCommit) {
|
|
121
367
|
debug('Could not obtain unique app identifier');
|
|
122
368
|
return null;
|
|
123
369
|
}
|
|
370
|
+
const { createHash } = await Promise.resolve().then(() => tslib_1.__importStar(require('crypto')));
|
|
124
371
|
// use sha1 to create a one-way hash to anonymize
|
|
125
|
-
|
|
126
|
-
return id;
|
|
372
|
+
return createHash('sha1').update(firstCommit).digest('hex');
|
|
127
373
|
}
|
|
128
374
|
/**
|
|
129
375
|
* Walk through the command's parent tree and construct a space-separated name.
|