@capacitor-plus/cli 8.5.2 → 8.6.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.
Binary file
Binary file
Binary file
@@ -709,13 +709,31 @@ export interface PluginsConfig {
709
709
  *
710
710
  * This option is only supported on Android.
711
711
  *
712
- * `css` = Injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
712
+ * `native` = (recommended) For older Chromium versions (< v140) this embeds the webview with padding and sets the `env(safe-area-inset-*)` variables to `0px`. For newer Chromium versions (>= v140) this makes sure the webview adheres to the `viewport-fit` meta tag. If set to `viewport-fit="cover"` this will make the webview edge-to-edge and the `env(safe-area-inset-*)` variables will contain the correct values. With those values you could set padding for example to make sure the webview is shown correctly.
713
713
  *
714
- * `disable` = Disable CSS variables injection.
714
+ * `css` = This is the same as `native`, but it also injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
715
+ *
716
+ * `disable` = (not recommended) Disable safe area insets handling completely.
717
+ * This shifts the responsibility from Capacitor to your own code to handle the insets.
718
+ * Be aware that this might result in a visually broken UI if your native app code and the content loaded into the webview do not correctly handle safe area insets.
715
719
  *
716
720
  * @default "css"
717
721
  */
718
- insetsHandling?: 'css' | 'disable';
722
+ insetsHandling?: 'native' | 'css' | 'disable';
723
+ /**
724
+ * Set an initial value for the to be detected `viewport-fit=` meta tag value.
725
+ * For most apps that support edge-to-edge this value will eventually be `cover`.
726
+ * Therefore you might want to set this value to `cover` to help prevent layout jumps and glitches.
727
+ * If you know the value to be `cover` initially, you can set it here.
728
+ * The value will always end up correctly, no matter what you set here,
729
+ * as long as `insetsHandling` is set to `native` or `css`.
730
+ * It only exists to help prevent layout jumps and glitches.
731
+ *
732
+ * This option is only supported on Android.
733
+ *
734
+ * @default undefined
735
+ */
736
+ initialViewportFitValueHint?: 'auto' | 'contain' | 'cover';
719
737
  /**
720
738
  * The style of the text and icons of the system bars.
721
739
  *
package/dist/index.js CHANGED
@@ -105,24 +105,14 @@ function runProgram(config) {
105
105
  .option('--configuration <name>', 'Configuration name of the iOS Scheme')
106
106
  .option('--keystorealiaspass <keystoreAliasPass>', 'Password for the Key Alias')
107
107
  .addOption(new commander_1.Option('--androidreleasetype <androidreleasetype>', 'Android release type; APK or AAB').choices([
108
- 'AAB',
109
- 'APK',
108
+ ...telemetry_1.TELEMETRY_SAFE_CHOICES.androidreleasetype,
110
109
  ]))
111
110
  .addOption(new commander_1.Option('--signing-type <signingtype>', 'Program used to sign apps (default: jarsigner)').choices([
112
- 'apksigner',
113
- 'jarsigner',
111
+ ...telemetry_1.TELEMETRY_SAFE_CHOICES.signingType,
114
112
  ]))
115
113
  .addOption(new commander_1.Option('--xcode-team-id <xcodeTeamID>', 'The Developer team to use for building and exporting the archive'))
116
- .addOption(new commander_1.Option('--xcode-export-method <xcodeExportMethod>', 'Describes how xcodebuild should export the archive (default: app-store-connect)').choices([
117
- 'app-store-connect',
118
- 'release-testing',
119
- 'enterprise',
120
- 'debugging',
121
- 'developer-id',
122
- 'mac-application',
123
- 'validation',
124
- ]))
125
- .addOption(new commander_1.Option('--xcode-signing-style <xcodeSigningStyle>', 'The iOS signing style to use when building the app for distribution (default: automatic)').choices(['automatic', 'manual']))
114
+ .addOption(new commander_1.Option('--xcode-export-method <xcodeExportMethod>', 'Describes how xcodebuild should export the archive (default: app-store-connect)').choices([...telemetry_1.TELEMETRY_SAFE_CHOICES.xcodeExportMethod]))
115
+ .addOption(new commander_1.Option('--xcode-signing-style <xcodeSigningStyle>', 'The iOS signing style to use when building the app for distribution (default: automatic)').choices([...telemetry_1.TELEMETRY_SAFE_CHOICES.xcodeSigningStyle]))
126
116
  .addOption(new commander_1.Option('--xcode-signing-certificate <xcodeSigningCertificate>', 'A certificate name, SHA-1 hash, or automatic selector to use for signing for iOS builds'))
127
117
  .addOption(new commander_1.Option('--xcode-provisioning-profile <xcodeProvisioningProfile>', 'A provisioning profile name or UUID for iOS builds'))
128
118
  .action((0, cli_1.wrapAction)((0, telemetry_1.telemetryAction)(config, async (platform, { scheme, flavor, keystorepath, keystorepass, keystorealias, keystorealiaspass, androidreleasetype, signingType, configuration, xcodeTeamId, xcodeExportMethod, xcodeSigningStyle, xcodeSigningCertificate, xcodeProvisioningProfile, }) => {
@@ -546,6 +546,7 @@ async function copyPluginsNativeFiles(config, cordovaPlugins) {
546
546
  const sourceFiles = (0, plugin_1.getPlatformElement)(p, platform, 'source-file');
547
547
  const headerFiles = (0, plugin_1.getPlatformElement)(p, platform, 'header-file');
548
548
  const codeFiles = sourceFiles.concat(headerFiles);
549
+ const pluginSuppliesAppDelegate = codeFiles.some((codeFile) => codeFile.$.src.split('/').pop() === 'AppDelegate.h');
549
550
  const frameworks = (0, plugin_1.getPlatformElement)(p, platform, 'framework');
550
551
  let sourcesFolderName = 'sources';
551
552
  if (!isSPM && (0, cordova_1.needsStaticPod)(p)) {
@@ -592,6 +593,10 @@ async function copyPluginsNativeFiles(config, cordovaPlugins) {
592
593
  fileContent = fileContent.replace(/self.webView.superview/g, 'self.viewController.view');
593
594
  await (0, fs_extra_1.writeFile)(fileDest, fileContent, { encoding: 'utf-8' });
594
595
  }
596
+ if (isSPM && !pluginSuppliesAppDelegate && fileContent.includes(`#import "AppDelegate.h"`)) {
597
+ fileContent = fileContent.replaceAll(`#import "AppDelegate.h"`, `#import <Cordova/AppDelegate.h>`);
598
+ await (0, fs_extra_1.writeFile)(fileDest, fileContent, { encoding: 'utf-8' });
599
+ }
595
600
  }
596
601
  }
597
602
  }
@@ -57,7 +57,9 @@ async function copy(config, platformName, inline = false) {
57
57
  usesLiveUpdates = true;
58
58
  }
59
59
  let usesSSLPinning = false;
60
- if (allPlugins.filter((plugin) => plugin.id === '@ionic-enterprise/ssl-pinning' || plugin.id === '@capacitor/ssl-pinning').length > 0) {
60
+ if (allPlugins.filter((plugin) => plugin.id === '@ionic-enterprise/ssl-pinning' ||
61
+ plugin.id === '@capacitor/ssl-pinning' ||
62
+ plugin.id === '@capacitor-community/ssl-pinning').length > 0) {
61
63
  usesSSLPinning = true;
62
64
  }
63
65
  if (platformName === config.ios.name) {
@@ -10,6 +10,7 @@ const doctor_2 = require("../ios/doctor");
10
10
  const log_1 = require("../log");
11
11
  const emoji_1 = require("../util/emoji");
12
12
  const node_1 = require("../util/node");
13
+ const npm_1 = require("../util/npm");
13
14
  const subprocess_1 = require("../util/subprocess");
14
15
  async function doctorCommand(config, selectedPlatformName) {
15
16
  log_1.output.write(`${(0, emoji_1.emoji)('💊', '')} ${colors_1.default.strong('Capacitor Doctor')} ${(0, emoji_1.emoji)('💊', '')} \n\n`);
@@ -21,7 +22,7 @@ async function doctorCommand(config, selectedPlatformName) {
21
22
  }
22
23
  exports.doctorCommand = doctorCommand;
23
24
  async function doctorCore(config) {
24
- const [cliVersionPlus, coreVersionPlus, androidVersionPlus, iosVersionPlus, cliVersion, coreVersion, androidVersion, iosVersion,] = await Promise.all([
25
+ const [cliVersionPlus, coreVersionPlus, androidVersionPlus, iosVersionPlus, cliVersion, coreVersion, androidVersion, iosVersion, blockedInstallScripts,] = await Promise.all([
25
26
  (0, subprocess_1.getCommandOutput)('npm', ['info', '@capacitor-plus/cli', 'version']),
26
27
  (0, subprocess_1.getCommandOutput)('npm', ['info', '@capacitor-plus/core', 'version']),
27
28
  (0, subprocess_1.getCommandOutput)('npm', ['info', '@capacitor-plus/android', 'version']),
@@ -30,6 +31,7 @@ async function doctorCore(config) {
30
31
  (0, subprocess_1.getCommandOutput)('npm', ['info', '@capacitor/core', 'version']),
31
32
  (0, subprocess_1.getCommandOutput)('npm', ['info', '@capacitor/android', 'version']),
32
33
  (0, subprocess_1.getCommandOutput)('npm', ['info', '@capacitor/ios', 'version']),
34
+ (0, npm_1.getBlockedInstallScripts)(config.app.rootDir),
33
35
  ]);
34
36
  log_1.output.write(`${colors_1.default.strong('Latest Dependencies:')}\n\n` +
35
37
  ` @capacitor-plus/cli: ${colors_1.default.weak(cliVersionPlus !== null && cliVersionPlus !== void 0 ? cliVersionPlus : 'unknown')}\n` +
@@ -43,6 +45,9 @@ async function doctorCore(config) {
43
45
  `${colors_1.default.strong('Installed Dependencies:')}\n\n`);
44
46
  await printInstalledPackages(config);
45
47
  log_1.output.write('\n');
48
+ if (blockedInstallScripts !== null) {
49
+ (0, npm_1.logBlockedInstallScripts)(blockedInstallScripts);
50
+ }
46
51
  }
47
52
  exports.doctorCore = doctorCore;
48
53
  async function printInstalledPackages(config) {
@@ -12,6 +12,7 @@ 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");
15
16
  const subprocess_1 = require("../util/subprocess");
16
17
  const template_1 = require("../util/template");
17
18
  const migrate_uiscene_1 = require("./migrate-uiscene");
@@ -84,8 +85,10 @@ const pluginVersion = '^8.0.0';
84
85
  const gradleVersion = '8.14.3';
85
86
  const iOSVersion = '15';
86
87
  const kotlinVersion = '2.2.20';
87
- let installFailed = false;
88
88
  async function migrateCommand(config, noprompt, packagemanager) {
89
+ let installFailed = false;
90
+ let scriptsBlocked = false;
91
+ let scriptsUnverified = false;
89
92
  if (config === null) {
90
93
  (0, errors_1.fatal)('Config data missing');
91
94
  }
@@ -153,6 +156,16 @@ async function migrateCommand(config, noprompt, packagemanager) {
153
156
  log_1.logger.error(`${installerType} install failed. Try deleting node_modules folder and running ${colors_1.default.input(`${installerType} install --force`)} manually.`);
154
157
  installFailed = true;
155
158
  }
159
+ if (runNpmInstall && !installFailed) {
160
+ const blocked = await (0, npm_1.getBlockedInstallScripts)(config.app.rootDir, installerType);
161
+ if (blocked === null) {
162
+ scriptsUnverified = true;
163
+ }
164
+ else if (blocked.length > 0) {
165
+ (0, npm_1.logBlockedInstallScripts)(blocked);
166
+ scriptsBlocked = (0, npm_1.hasBlockedCapacitorScripts)(blocked);
167
+ }
168
+ }
156
169
  // Update iOS Projects
157
170
  if ((allDependencies['@capacitor-plus/ios'] || allDependencies['@capacitor/ios']) &&
158
171
  (0, fs_extra_1.existsSync)(config.ios.platformDirAbs)) {
@@ -277,9 +290,12 @@ async function migrateCommand(config, noprompt, packagemanager) {
277
290
  await (0, common_1.runTask)(`Writing breaking changes.`, () => {
278
291
  return writeBreakingChanges();
279
292
  });
280
- if (!installFailed) {
293
+ if (!installFailed && !scriptsBlocked && !scriptsUnverified) {
281
294
  (0, log_1.logSuccess)(`Migration to Capacitor ${coreVersion} is complete. Run and test your app!`);
282
295
  }
296
+ else if (scriptsBlocked) {
297
+ 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')}.`);
298
+ }
283
299
  else {
284
300
  log_1.logger.warn(`Migration to Capacitor ${coreVersion} is incomplete. Check the log messages for more information.`);
285
301
  }
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.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"));
@@ -15,6 +15,104 @@ const debug = (0, debug_1.default)('capacitor:telemetry');
15
15
  const THANK_YOU = `\nThank you for helping improve Capacitor by sharing anonymous usage data! 💖` +
16
16
  `\nInformation about the data we collect is available on our website: ${colors_1.default.strong('https://capacitorjs.com/docs/next/cli/telemetry')}` +
17
17
  `\nYou can disable telemetry at any time by using the ${colors_1.default.input('npx cap telemetry off')} command.`;
18
+ /**
19
+ * The option values that are safe to report, keyed by the option's resolved
20
+ * name (commander's `attributeName()`, so `--signing-type` is `signingType`).
21
+ * This is an allowlist by design: a denylist of password-like names would leak
22
+ * the first sensitive option someone adds.
23
+ *
24
+ * The option declarations in `index.ts` spread these same lists into
25
+ * `.choices()`, so an option cannot offer a choice this does not list.
26
+ */
27
+ exports.TELEMETRY_SAFE_CHOICES = {
28
+ androidreleasetype: ['AAB', 'APK'],
29
+ signingType: ['apksigner', 'jarsigner'],
30
+ xcodeExportMethod: [
31
+ 'app-store-connect',
32
+ 'release-testing',
33
+ 'enterprise',
34
+ 'debugging',
35
+ 'developer-id',
36
+ 'mac-application',
37
+ 'validation',
38
+ ],
39
+ xcodeSigningStyle: ['automatic', 'manual'],
40
+ };
41
+ const telemetrySafeChoices = new Map(Object.entries(exports.TELEMETRY_SAFE_CHOICES).map(([name, choices]) => [name, new Set(choices)]));
42
+ /**
43
+ * Positional argument values that are safe to report. Everything else is
44
+ * dropped, since arguments like `cap init`'s app name are user content.
45
+ */
46
+ const TELEMETRY_ARGUMENT_ALLOWLIST = new Set(['android', 'ios', 'web']);
47
+ /**
48
+ * Upper bound on the reported error, so a failed Gradle or Xcode build cannot
49
+ * ship its entire log as a metric.
50
+ */
51
+ const TELEMETRY_ERROR_MAX_LENGTH = 500;
52
+ const TELEMETRY_COMMAND_FAILED = 'command_failed';
53
+ /**
54
+ * Lines from a failed subprocess that are safe to report. Build-setting dumps,
55
+ * environment echoes, and arbitrary log lines can still carry secrets after
56
+ * value redaction, so only tool error summaries are kept.
57
+ */
58
+ function pickAllowlistedTelemetryErrorLines(redacted) {
59
+ const lines = redacted.split('\n').filter((line) => {
60
+ const trimmed = line.trim();
61
+ if (!trimmed) {
62
+ return false;
63
+ }
64
+ if (/^Build settings from command line:/i.test(trimmed)) {
65
+ return false;
66
+ }
67
+ if (/^\S+\s=/.test(trimmed)) {
68
+ return false;
69
+ }
70
+ if (/^Error:/i.test(trimmed)) {
71
+ return true;
72
+ }
73
+ if (/^FAILURE:/i.test(trimmed)) {
74
+ return true;
75
+ }
76
+ if (/platform has not been added/i.test(trimmed)) {
77
+ return true;
78
+ }
79
+ if (/no device matched target/i.test(trimmed)) {
80
+ return true;
81
+ }
82
+ if (/keystore password was not accepted/i.test(trimmed)) {
83
+ return true;
84
+ }
85
+ if (/^[\w.-]+: error:/i.test(trimmed)) {
86
+ return true;
87
+ }
88
+ if (/^jarsigner:/i.test(trimmed)) {
89
+ return true;
90
+ }
91
+ if (/^xcodebuild:/i.test(trimmed)) {
92
+ return true;
93
+ }
94
+ return false;
95
+ });
96
+ return lines.join('\n');
97
+ }
98
+ /**
99
+ * Shortest value worth scrubbing out of a reported error. An empty string would
100
+ * match between every character, and a one- or two-character option value--a
101
+ * port, a target index--appears throughout unrelated output, so redacting
102
+ * either destroys the error instead of sanitizing it.
103
+ */
104
+ const TELEMETRY_REDACTION_MIN_LENGTH = 3;
105
+ /**
106
+ * The characters a dependency spec may contain to be reported as-is. A version
107
+ * range has none of the `:/@` a URL or path spec needs, so specs such as
108
+ * `git+https://x-access-token:<token>@github.com/acme/plugin.git` and
109
+ * `file:/Users/<name>/dev/plugin` are reported as {@link NON_REGISTRY_VERSION}
110
+ * instead of shipping a credential or a local path. The pnpm and yarn workspace
111
+ * protocols are allowed through as the only exception, since they resolve
112
+ * within the repo and so cannot carry either.
113
+ */
114
+ const TELEMETRY_SAFE_VERSION_PATTERN = /^(?:(?:workspace|catalog):)?[\w.\-+^~><=|* ]*$/;
115
+ const NON_REGISTRY_VERSION = 'non-registry';
18
116
  function telemetryAction(config, action) {
19
117
  return async (...actionArgs) => {
20
118
  const start = new Date();
@@ -41,15 +139,16 @@ function telemetryAction(config, action) {
41
139
  const capacitorPackages = packages.filter(([k]) => k.startsWith('@capacitor-plus/'));
42
140
  const versions = capacitorPackages.map(([k, v]) => [
43
141
  `${k.replace(/^@capacitor-plus\//, '').replace(/-/g, '_')}_version`,
44
- v,
142
+ getTelemetryVersion(v),
45
143
  ]);
144
+ const options = getTelemetryOptions(cmd);
46
145
  const data = {
47
146
  app_id: await getAppIdentifier(config),
48
147
  command,
49
- arguments: cmd.args.join(' '),
50
- options: JSON.stringify(cmd.opts()),
148
+ arguments: getTelemetryArguments(cmd),
149
+ options: JSON.stringify(options),
51
150
  duration,
52
- error: error ? (error.message ? error.message : String(error)) : null,
151
+ error: getTelemetryError(cmd, error, config, options),
53
152
  node_version: process.version,
54
153
  os: config.cli.os,
55
154
  ios_package_manager: await getIOSPackageManager(config),
@@ -72,6 +171,90 @@ function telemetryAction(config, action) {
72
171
  };
73
172
  }
74
173
  exports.telemetryAction = telemetryAction;
174
+ /**
175
+ * Build the reported option map: every option name the command resolved, but
176
+ * only the values that cannot carry a secret. Booleans are kept because they
177
+ * cannot encode one, allowlisted options are kept when the value is one this
178
+ * file lists, and every other value--keystore passwords, paths, signing
179
+ * identities--becomes `null`.
180
+ */
181
+ function getTelemetryOptions(cmd) {
182
+ return Object.fromEntries(Object.entries(cmd.opts()).map(([name, value]) => [name, isTelemetrySafeValue(name, value) ? value : null]));
183
+ }
184
+ exports.getTelemetryOptions = getTelemetryOptions;
185
+ function isTelemetrySafeValue(name, value) {
186
+ var _a, _b;
187
+ if (typeof value === 'boolean') {
188
+ return true;
189
+ }
190
+ return typeof value === 'string' && ((_b = (_a = telemetrySafeChoices.get(name)) === null || _a === void 0 ? void 0 : _a.has(value)) !== null && _b !== void 0 ? _b : false);
191
+ }
192
+ /**
193
+ * The signing credentials `capacitor.config` can supply. `cap build` falls back
194
+ * to the config for each of these when the flag is absent, and the resolved
195
+ * value still reaches the `xcodebuild` and `jarsigner` command lines--so it can
196
+ * come back in their output without ever having appeared in `cmd.opts()`.
197
+ */
198
+ function getConfiguredCredentials(config) {
199
+ return [
200
+ config.android.buildOptions.keystorePath,
201
+ config.android.buildOptions.keystorePassword,
202
+ config.android.buildOptions.keystoreAlias,
203
+ config.android.buildOptions.keystoreAliasPassword,
204
+ config.ios.buildOptions.teamId,
205
+ config.ios.buildOptions.signingCertificate,
206
+ config.ios.buildOptions.provisioningProfile,
207
+ ].filter((value) => typeof value === 'string');
208
+ }
209
+ /**
210
+ * Build the reported error, scrubbed of every option value and positional
211
+ * argument the rest of the payload drops, plus the signing credentials the
212
+ * config can supply in place of a flag, then truncated.
213
+ *
214
+ * Failed subprocesses rethrow the tool's combined output, which echoes back
215
+ * the very values that were redacted out of the option map--`xcodebuild`
216
+ * prints its command-line build settings, `jarsigner` prints the keystore it
217
+ * was pointed at.
218
+ */
219
+ function getTelemetryError(cmd, error, config, options = getTelemetryOptions(cmd)) {
220
+ if (!error) {
221
+ return null;
222
+ }
223
+ const values = cmd.opts();
224
+ const redacted = [
225
+ ...Object.entries(options)
226
+ .filter(([, value]) => value === null)
227
+ .map(([name]) => values[name]),
228
+ ...cmd.args.filter((arg) => !TELEMETRY_ARGUMENT_ALLOWLIST.has(arg)),
229
+ ...getConfiguredCredentials(config),
230
+ ]
231
+ .filter((value) => typeof value === 'string' && value.length >= TELEMETRY_REDACTION_MIN_LENGTH)
232
+ .sort((a, b) => b.length - a.length);
233
+ const scrubbed = redacted.reduce((message, value) => message.replaceAll(value, '<redacted>'), error.message ? error.message : String(error));
234
+ const allowlisted = pickAllowlistedTelemetryErrorLines(scrubbed);
235
+ if (!allowlisted) {
236
+ return TELEMETRY_COMMAND_FAILED;
237
+ }
238
+ return allowlisted.slice(0, TELEMETRY_ERROR_MAX_LENGTH);
239
+ }
240
+ exports.getTelemetryError = getTelemetryError;
241
+ /**
242
+ * Build the reported positional arguments, keeping only allowlisted values.
243
+ */
244
+ function getTelemetryArguments(cmd) {
245
+ return cmd.args.filter((arg) => TELEMETRY_ARGUMENT_ALLOWLIST.has(arg)).join(' ');
246
+ }
247
+ exports.getTelemetryArguments = getTelemetryArguments;
248
+ /**
249
+ * Build the reported version of a Capacitor dependency, keeping the spec only
250
+ * when it is a version range. Specs that point somewhere--a git URL, a local
251
+ * path--are reported as being just that, since they can carry a token or a
252
+ * username.
253
+ */
254
+ function getTelemetryVersion(spec) {
255
+ return spec && TELEMETRY_SAFE_VERSION_PATTERN.test(spec) ? spec : NON_REGISTRY_VERSION;
256
+ }
257
+ exports.getTelemetryVersion = getTelemetryVersion;
75
258
  /**
76
259
  * If telemetry is enabled, send a metric via IPC to a forked process for uploading.
77
260
  */
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logBlockedInstallScripts = exports.hasBlockedCapacitorScripts = exports.parseBlockedInstallScripts = exports.getBlockedInstallScripts = exports.isNpmProject = exports.logAmbiguousLockfiles = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs_extra_1 = require("fs-extra");
6
+ const path_1 = require("path");
7
+ const colors_1 = tslib_1.__importDefault(require("../colors"));
8
+ const log_1 = require("../log");
9
+ const subprocess_1 = require("./subprocess");
10
+ const LOCKFILE_MANAGERS = [
11
+ ['package-lock.json', 'npm'],
12
+ ['npm-shrinkwrap.json', 'npm'],
13
+ ['pnpm-lock.yaml', 'pnpm'],
14
+ ['yarn.lock', 'yarn'],
15
+ ['bun.lock', 'bun'],
16
+ ['bun.lockb', 'bun'],
17
+ ];
18
+ function detectLockfileManagers(rootDir) {
19
+ const managers = new Set();
20
+ for (const [lockfile, manager] of LOCKFILE_MANAGERS) {
21
+ if ((0, fs_extra_1.existsSync)((0, path_1.join)(rootDir, lockfile))) {
22
+ managers.add(manager);
23
+ }
24
+ }
25
+ return managers;
26
+ }
27
+ function hasNpmLockfileDisabled(rootDir) {
28
+ const npmrcPath = (0, path_1.join)(rootDir, '.npmrc');
29
+ if (!(0, fs_extra_1.existsSync)(npmrcPath)) {
30
+ return false;
31
+ }
32
+ try {
33
+ for (const line of (0, fs_extra_1.readFileSync)(npmrcPath, 'utf-8').split('\n')) {
34
+ const trimmed = line.trim();
35
+ if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith(';')) {
36
+ continue;
37
+ }
38
+ const match = trimmed.match(/^package-lock\s*=\s*(false|0)\s*$/i);
39
+ if (match) {
40
+ return true;
41
+ }
42
+ }
43
+ }
44
+ catch (e) {
45
+ return false;
46
+ }
47
+ return false;
48
+ }
49
+ function logAmbiguousLockfiles(rootDir) {
50
+ const managers = detectLockfileManagers(rootDir);
51
+ if (managers.size <= 1) {
52
+ return;
53
+ }
54
+ try {
55
+ const { packageManager } = (0, fs_extra_1.readJSONSync)((0, path_1.join)(rootDir, 'package.json'));
56
+ if (typeof packageManager === 'string') {
57
+ return;
58
+ }
59
+ }
60
+ catch (e) {
61
+ // fall through
62
+ }
63
+ log_1.output.write(`${colors_1.default.strong('Ambiguous package manager:')}\n\n` +
64
+ ` Multiple lockfiles are present (${[...managers].join(', ')}), so blocked install scripts cannot be checked reliably.\n` +
65
+ ` Remove stale lockfiles or set ${colors_1.default.input('packageManager')} in package.json.\n\n`);
66
+ }
67
+ exports.logAmbiguousLockfiles = logAmbiguousLockfiles;
68
+ /**
69
+ * `npm install-scripts ls` compares the tree against npm's own `allowScripts` map, which yarn and
70
+ * pnpm never write. It therefore reports every install script in a yarn or pnpm project as
71
+ * blocked, even when that package manager ran it, so the check has to know who did the install.
72
+ *
73
+ * Only a positive signal counts. `rootDir` is the app package, which in a workspace holds neither
74
+ * a lockfile nor a `packageManager` field, and guessing npm there reports blocked scripts for an
75
+ * install npm never performed.
76
+ */
77
+ async function isNpmProject(rootDir) {
78
+ try {
79
+ const { packageManager } = await (0, fs_extra_1.readJSON)((0, path_1.join)(rootDir, 'package.json'));
80
+ if (typeof packageManager === 'string') {
81
+ return packageManager.split('@')[0] === 'npm';
82
+ }
83
+ }
84
+ catch (e) {
85
+ // no package.json, or an unreadable one: fall through to the lockfiles
86
+ }
87
+ const managers = detectLockfileManagers(rootDir);
88
+ if (managers.size === 1) {
89
+ return managers.has('npm');
90
+ }
91
+ if (managers.size > 1) {
92
+ return false;
93
+ }
94
+ if ((0, fs_extra_1.existsSync)((0, path_1.join)(rootDir, 'package.json')) && hasNpmLockfileDisabled(rootDir)) {
95
+ return true;
96
+ }
97
+ return false;
98
+ }
99
+ exports.isNpmProject = isNpmProject;
100
+ /**
101
+ * npm 12 blocks dependency install scripts by default and still exits 0, so the only reliable
102
+ * signal is to ask npm directly. `npm install-scripts ls` lists every package whose scripts are
103
+ * still awaiting a decision, and does not exist before npm 12, where the command exits non-zero
104
+ * and `getCommandOutput` hands back null.
105
+ *
106
+ * Pass `packageManager` when the caller already knows which one ran the install; otherwise it is
107
+ * inferred from the project.
108
+ */
109
+ async function getBlockedInstallScripts(rootDir, packageManager) {
110
+ if (packageManager === undefined) {
111
+ logAmbiguousLockfiles(rootDir);
112
+ }
113
+ const isNpm = packageManager === undefined ? await isNpmProject(rootDir) : packageManager.toLowerCase() === 'npm';
114
+ if (!isNpm) {
115
+ return [];
116
+ }
117
+ const npmVersionOutput = await (0, subprocess_1.getCommandOutput)('npm', ['--version'], { cwd: rootDir });
118
+ if (npmVersionOutput === null) {
119
+ log_1.output.write(`${colors_1.default.strong('Blocked install scripts:')}\n\n` +
120
+ ` Could not determine the npm version, so blocked install scripts were not checked.\n\n`);
121
+ return null;
122
+ }
123
+ const npmMajor = parseInt(npmVersionOutput.split('.')[0], 10);
124
+ if (!Number.isFinite(npmMajor) || npmMajor < 12) {
125
+ return [];
126
+ }
127
+ const commandOutput = await (0, subprocess_1.getCommandOutput)('npm', ['install-scripts', 'ls', '--json', '--all=false'], {
128
+ cwd: rootDir,
129
+ });
130
+ if (commandOutput === null) {
131
+ log_1.output.write(`${colors_1.default.strong('Blocked install scripts:')}\n\n` +
132
+ ` Could not query npm for blocked install scripts. ` +
133
+ `If ${colors_1.default.input('all=true')} is set in ${colors_1.default.input('.npmrc')}, run ${colors_1.default.input('npm install-scripts ls --all=false')} manually.\n\n`);
134
+ return null;
135
+ }
136
+ return parseBlockedInstallScripts(commandOutput);
137
+ }
138
+ exports.getBlockedInstallScripts = getBlockedInstallScripts;
139
+ function parseBlockedInstallScripts(commandOutput) {
140
+ if (!commandOutput) {
141
+ return [];
142
+ }
143
+ let parsed;
144
+ try {
145
+ parsed = JSON.parse(commandOutput);
146
+ }
147
+ catch (e) {
148
+ return [];
149
+ }
150
+ if (!Array.isArray(parsed === null || parsed === void 0 ? void 0 : parsed.allowScripts)) {
151
+ return [];
152
+ }
153
+ const blocked = [];
154
+ for (const entry of parsed.allowScripts) {
155
+ const name = entry === null || entry === void 0 ? void 0 : entry.name;
156
+ if (typeof name !== 'string' || !Array.isArray(entry.changes)) {
157
+ continue;
158
+ }
159
+ for (const change of entry.changes) {
160
+ if ((change === null || change === void 0 ? void 0 : change.change) !== 'pending' || typeof change.key !== 'string') {
161
+ continue;
162
+ }
163
+ blocked.push({
164
+ name,
165
+ version: change.key.startsWith(`${name}@`) ? change.key.slice(name.length + 1) : change.key,
166
+ });
167
+ }
168
+ }
169
+ return blocked;
170
+ }
171
+ exports.parseBlockedInstallScripts = parseBlockedInstallScripts;
172
+ /**
173
+ * npm 12 denies install scripts by default, so a typical project has pending entries for packages
174
+ * like esbuild or sharp that a Capacitor migration has no bearing on. Only Capacitor's own blocked
175
+ * scripts say anything about whether the migration finished.
176
+ */
177
+ function hasBlockedCapacitorScripts(blocked) {
178
+ return blocked.some(({ name }) => name.startsWith('@capacitor-plus/') || name.startsWith('@capacitor/'));
179
+ }
180
+ exports.hasBlockedCapacitorScripts = hasBlockedCapacitorScripts;
181
+ function logBlockedInstallScripts(blocked) {
182
+ if (blocked.length === 0) {
183
+ return;
184
+ }
185
+ const names = [...new Set(blocked.map(({ name }) => name))];
186
+ const packages = blocked.map(({ name, version }) => ` ${colors_1.default.strong(`${name}@${version}`)}\n`).join('');
187
+ const approvals = names.map((name) => ` ${colors_1.default.input(`npm install-scripts approve ${name}`)}\n`).join('');
188
+ log_1.output.write(`${colors_1.default.strong('Blocked Install Scripts:')}\n\n` +
189
+ ` npm blocked install scripts for ${names.length === 1 ? '1 package' : `${names.length} packages`}. ` +
190
+ `Features that rely on them may not work.\n\n` +
191
+ packages +
192
+ `\n To allow them, run:\n\n` +
193
+ approvals +
194
+ ` ${colors_1.default.input('npm rebuild')}\n\n` +
195
+ ` ${colors_1.default.weak('Approvals are pinned to an exact version, so repeat this after upgrading the package.')}\n\n`);
196
+ }
197
+ exports.logBlockedInstallScripts = logBlockedInstallScripts;
package/dist/util/spm.js CHANGED
@@ -125,7 +125,9 @@ let package = Package(
125
125
  const options = packageOptions[plugin.id];
126
126
  const symlink = options === null || options === void 0 ? void 0 : options.symlink;
127
127
  const symlinkFolder = (0, path_1.join)('symlinks', plugin.name);
128
- const relPath = (0, fs_1.convertToUnixPath)(symlink ? symlinkFolder : (0, path_1.relative)(config.ios.nativeXcodeProjDirAbs, plugin.rootPath));
128
+ const relPath = symlink
129
+ ? (0, fs_1.convertToUnixPath)((0, path_1.join)('symlinks', plugin.name))
130
+ : (0, fs_1.convertToUnixPath)((0, path_1.relative)(config.ios.nativeXcodeProjDirAbs, plugin.rootPath));
129
131
  if (symlink) {
130
132
  await (0, fs_extra_1.ensureSymlink)(plugin.rootPath, (0, path_1.resolve)(config.ios.nativeProjectDirAbs, 'CapApp-SPM', symlinkFolder));
131
133
  }
@@ -231,6 +233,51 @@ function hasSceneManifest(config) {
231
233
  return entries['UIApplicationSceneManifest'] !== undefined;
232
234
  }
233
235
  exports.hasSceneManifest = hasSceneManifest;
236
+ function storyboardResourceExists(nativeTargetDirAbs, storyboardName) {
237
+ const fileName = `${storyboardName}.storyboard`;
238
+ const stack = [nativeTargetDirAbs];
239
+ while (stack.length > 0) {
240
+ const dir = stack.pop();
241
+ for (const entry of (0, fs_extra_1.readdirSync)(dir, { withFileTypes: true })) {
242
+ if (entry.isFile() && entry.name === fileName) {
243
+ return true;
244
+ }
245
+ if (entry.isDirectory()) {
246
+ stack.push((0, path_1.resolve)(dir, entry.name));
247
+ }
248
+ }
249
+ }
250
+ return false;
251
+ }
252
+ function resolvedSceneStoryboardFile(entries, nativeTargetDirAbs) {
253
+ const mainStoryboard = entries['UIMainStoryboardFile'];
254
+ if (typeof mainStoryboard === 'string' && mainStoryboard.trim() !== '') {
255
+ if (storyboardResourceExists(nativeTargetDirAbs, mainStoryboard)) {
256
+ return mainStoryboard;
257
+ }
258
+ }
259
+ return undefined;
260
+ }
261
+ function syncSceneStoryboardInManifest(entries, nativeTargetDirAbs) {
262
+ const manifest = entries['UIApplicationSceneManifest'];
263
+ if (!manifest) {
264
+ return false;
265
+ }
266
+ const configs = manifest['UISceneConfigurations'];
267
+ const roleArray = configs === null || configs === void 0 ? void 0 : configs['UIWindowSceneSessionRoleApplication'];
268
+ if (!(roleArray === null || roleArray === void 0 ? void 0 : roleArray.length)) {
269
+ return false;
270
+ }
271
+ const storyboard = resolvedSceneStoryboardFile(entries, nativeTargetDirAbs);
272
+ let changed = false;
273
+ for (const role of roleArray) {
274
+ if (storyboard && role['UISceneStoryboardFile'] === undefined) {
275
+ role['UISceneStoryboardFile'] = storyboard;
276
+ changed = true;
277
+ }
278
+ }
279
+ return changed;
280
+ }
234
281
  async function addSceneManifestIfNeeded(config) {
235
282
  const infoPlist = (0, path_1.resolve)(config.ios.nativeTargetDirAbs, 'Info.plist');
236
283
  if (!(0, fs_extra_1.existsSync)(infoPlist)) {
@@ -239,18 +286,26 @@ async function addSceneManifestIfNeeded(config) {
239
286
  }
240
287
  const entries = (0, plist_1.parse)((0, fs_extra_1.readFileSync)(infoPlist, 'utf-8'));
241
288
  if (entries['UIApplicationSceneManifest'] !== undefined) {
242
- log_1.logger.warn('Found UIApplicationSceneManifest in ' + infoPlist + ', skipping.');
289
+ if (syncSceneStoryboardInManifest(entries, config.ios.nativeTargetDirAbs)) {
290
+ (0, fs_extra_1.writeFileSync)(infoPlist, (0, plist_1.build)(entries));
291
+ }
292
+ else {
293
+ log_1.logger.warn('Found UIApplicationSceneManifest in ' + infoPlist + ', skipping.');
294
+ }
243
295
  return;
244
296
  }
297
+ const sceneConfiguration = {
298
+ UISceneConfigurationName: 'Default Configuration',
299
+ UISceneDelegateClassName: '$(PRODUCT_MODULE_NAME).SceneDelegate',
300
+ };
301
+ const storyboard = resolvedSceneStoryboardFile(entries, config.ios.nativeTargetDirAbs);
302
+ if (storyboard) {
303
+ sceneConfiguration['UISceneStoryboardFile'] = storyboard;
304
+ }
245
305
  entries['UIApplicationSceneManifest'] = {
246
306
  UIApplicationSupportsMultipleScenes: false,
247
307
  UISceneConfigurations: {
248
- UIWindowSceneSessionRoleApplication: [
249
- {
250
- UISceneConfigurationName: 'Default Configuration',
251
- UISceneDelegateClassName: '$(PRODUCT_MODULE_NAME).SceneDelegate',
252
- },
253
- ],
308
+ UIWindowSceneSessionRoleApplication: [sceneConfiguration],
254
309
  },
255
310
  };
256
311
  (0, fs_extra_1.writeFileSync)(infoPlist, (0, plist_1.build)(entries));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capacitor-plus/cli",
3
- "version": "8.5.2",
3
+ "version": "8.6.1",
4
4
  "description": "Capacitor+: Enhanced Capacitor with automated upstream sync - Cross-platform apps with JavaScript and the web",
5
5
  "homepage": "https://capgo.app/docs/plugins/capacitor-plus/",
6
6
  "author": "Capgo Team <support@capgo.app> (https://capgo.app)",
@@ -60,8 +60,7 @@
60
60
  "semver": "^7.6.3",
61
61
  "tar": "^7.5.3",
62
62
  "tslib": "^2.8.1",
63
- "typescript": "~5.0.2",
64
- "xcode": "^3.0.1",
63
+ "xcode": "^4.0.0",
65
64
  "xml2js": "^0.6.2"
66
65
  },
67
66
  "devDependencies": {