Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# 24 is the engines floor, matching the Capacitor 9 CLI. It also covers what
# `npm test` needs: `node --test` globs and unflagged TypeScript type stripping.
node: ['24']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
# package-lock.json is gitignored, so `npm ci` is not available here.
- run: npm install
- run: npm run lint
- run: npm test
- run: npm run build
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,37 @@ Migrate a Capacitor 8 plugin to Capacitor 9.

```
npx @capacitor/plugin-migration-v8-to-v9@latest
```
```

## Blocked install scripts

npm 12 blocks the install scripts of dependencies that are not listed in `allowScripts`,
and still exits successfully. When that happens the plugin files are migrated but the
dependency tree is incomplete, so the migration exits with an error naming the packages:

```
The plugin files were migrated, but npm blocked the install scripts of 1 package, so the migration is incomplete:
swiftlint@2.0.0

Review those packages, then approve and rebuild them:
npm install-scripts approve swiftlint
npm rebuild

The migration itself does not need to be run again.
```

Review each package before approving it. Only packages that this migration made pending
are reported. A package you had already reviewed before the migration ran — whether you
approved it or left it blocked — is left alone, even if the migration moved it to a new
version.

## Failed installs

The migration runs `npm install` after rewriting your files. If that fails, the plugin
files are still migrated but the dependency tree is incomplete, so the migration exits
non-zero and prints npm's own error.

Plugins that declare another package manager are exempt: a `packageManager` field naming
pnpm, yarn or bun, or an `only-allow` preinstall guard, makes `npm install` fail by
design. In that case the migration warns, finishes, and exits successfully — install the
dependencies with your own package manager afterwards.
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,19 @@
"prettier": "prettier \"**/*.{css,html,js,mjs,ts,json}\"",
"build": "npm run clean && tsc",
"clean": "rimraf ./dist",
"test": "echo \"Error: no test specified\" && exit 1",
"test": "node --test \"src/**/*.test.ts\"",
"prepublishOnly": "npm run build"
},
"engines": {
"node": ">=24.0.0"
},
"devEngines": {
"runtime": {
"name": "node",
"version": ">=24.0.0",
"onFail": "error"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/ionic-team/migrate-capacitor-plugin.git"
Expand Down
51 changes: 48 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ import { join, resolve } from 'path';
import { rimraf } from 'rimraf';
import { coerce, gte, lt, valid } from 'semver';

import {
diffBlockedKeys,
findInstallScriptKeys,
formatBlockedScriptsError,
npmSupportsInstallScriptsIn,
} from './install-scripts';
import type { CommandRunner } from './install-scripts';
import { logger } from './log';
import { declaresOtherPackageManager } from './package-manager';
import { runCommand } from './subprocess';

const coreVersion = '9.0.0-alpha.8';
Expand Down Expand Up @@ -276,18 +284,46 @@ export const run = async (): Promise<void> => {
}
}

const runInDir: CommandRunner = (command, args) => runCommand(command, args, { cwd: dir });

// Take the baseline before anything is removed or reinstalled: `npm install-scripts ls`
// enumerates the tree on disk, so this is the last point at which it still reports what was
// already pending before this migration started.
const supportsInstallScripts = await npmSupportsInstallScriptsIn(runInDir);
const baselineBlocked = supportsInstallScripts ? await findInstallScriptKeys(runInDir) : null;
if (supportsInstallScripts && baselineBlocked === null) {
logger.warn('Could not read the blocked install scripts of this plugin, skipping the check');
}

// start from a clean tree: npm treats what is already installed as the starting point, and an
// installed eslint 8 with @ionic/eslint-config 0.4 blocks the move to eslint 10 and 0.5
rimraf.sync(join(dir, 'node_modules'));
rimraf.sync(join(dir, 'package-lock.json'));

let installError: string | undefined;
let blockedPackages: string[] = [];
try {
await runCommand('npm', ['install'], {
...opts,
cwd: dir,
});
} catch {
if (baselineBlocked !== null) {
const blockedAfterInstall = await findInstallScriptKeys(runInDir);
if (blockedAfterInstall === null) {
logger.warn('Could not read the blocked install scripts after installing, skipping the check');
} else {
// Only report what this migration made pending. Packages that were already pending
// are the repo's own allowScripts policy, not something the migration broke.
blockedPackages = diffBlockedKeys(baselineBlocked, blockedAfterInstall);
}
}
} catch (e: any) {
logger.warn('npm install failed, please, install the dependencies using your package manager of choice');
// A repo that declares pnpm, yarn or bun fails `npm install` on purpose, so that is not
// evidence of an incomplete migration.
if (!declaresOtherPackageManager(pluginJSON)) {
installError = String(e);
}
}

if (pluginJSON.capacitor?.android?.src) {
Expand Down Expand Up @@ -377,8 +413,6 @@ export const run = async (): Promise<void> => {
}
}

logger.info('Plugin migrated to Capacitor 9!');

if (eslintUpdated) {
logger.info('');
logger.info('⚠️ Note: ESLint has been updated to v10 and @ionic/eslint-config to v0.5.0, which uses flat config.');
Expand All @@ -396,7 +430,18 @@ export const run = async (): Promise<void> => {
logger.info(
'We recommend running your formatting and linting scripts to ensure your codebase is formatted correctly.',
);
logger.info('');
}

if (installError) {
throw `The plugin files were migrated, but npm install failed, so the migration is incomplete.\nInstall the dependencies using your package manager of choice.\nThe migration itself does not need to be run again.\n\nnpm install said:\n${installError}`;
}

if (blockedPackages.length > 0) {
throw `The plugin files were migrated, but ${formatBlockedScriptsError(blockedPackages)}`;
}

logger.info('Plugin migrated to Capacitor 9!');
};

// Rules in android/proguard-rules.pro only reach apps through consumerProguardFiles and the published files.
Expand Down
Loading
Loading