diff --git a/cli/src/api/versions.ts b/cli/src/api/versions.ts index a06bc49907..57a84a9054 100644 --- a/cli/src/api/versions.ts +++ b/cli/src/api/versions.ts @@ -55,8 +55,10 @@ export function displayBundles( if (silent) return - if (!data.length) - throw new Error('No bundle found') + if (!data.length) { + log.info('No bundles found') + return + } const t = new Table() t.theme = Table.roundTheme diff --git a/cli/src/posthog.ts b/cli/src/posthog.ts index 27d705d8f8..df39d7a917 100644 --- a/cli/src/posthog.ts +++ b/cli/src/posthog.ts @@ -213,8 +213,11 @@ export async function capturePosthogException(payload: CapturePosthogExceptionPa const distinctId = `cli:${pack.version}:${payload.functionName}` const frames = parseExceptionFrames(serializedError.stack, payload.functionName) const topFrame = frames[0] + // Deliberately exclude the CLI version from the fingerprint so the same bug + // stays a single error-tracking issue across releases instead of minting a + // brand-new issue on every version bump. const fingerprint = [ - distinctId, + payload.functionName, payload.kind, serializedError.name || 'Error', topFrame?.function || payload.functionName, diff --git a/cli/src/utils.ts b/cli/src/utils.ts index 331e0ecc29..e1d495a310 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -2187,6 +2187,27 @@ async function calculatePlatformChecksums(dependencyFolderPath: string): Promise return { ios_checksum, android_checksum } } +// Collect every `node_modules` directory from `startDir` up to the filesystem +// root. This mirrors the parent-directory walk `getAllPackagesDependencies` +// uses to resolve versions, so that the existence check validates the same +// hoisted locations the enumeration reads from. Without this, dependencies +// hoisted to a monorepo/workspace root read as missing. +function getHoistedNodeModulesPaths(startDir: string): string[] { + const paths: string[] = [] + let currentDir = startDir + const root = path.parse(currentDir).root + while (true) { + paths.push(join(currentDir, 'node_modules')) + if (currentDir === root) + break + const parentDir = dirname(currentDir) + if (parentDir === currentDir) + break + currentDir = parentDir + } + return paths +} + export async function getLocalDependencies(packageJsonPath: string | undefined, nodeModulesString: string | undefined) { const nodeModules = nodeModulesString ? nodeModulesString @@ -2220,7 +2241,7 @@ export async function getLocalDependencies(packageJsonPath: string | undefined, } const nodeModulesPaths = nodeModules.length === 0 - ? [join(cwd(), 'node_modules')] + ? getHoistedNodeModulesPaths(cwd()) : nodeModules const anyValidPath = nodeModulesPaths.some(path => existsSync(path)) @@ -2303,7 +2324,7 @@ export async function getLocalDependencies(packageJsonPath: string | undefined, ios_checksum, android_checksum, } - })).catch(() => []) + })) if (anyInvalid || dependenciesObject.some(a => a.native === undefined)) { log.error('Missing dependencies or invalid dependencies') diff --git a/cli/test/test-native-dependencies.mjs b/cli/test/test-native-dependencies.mjs index 42a45547c1..5bb70c3dca 100644 --- a/cli/test/test-native-dependencies.mjs +++ b/cli/test/test-native-dependencies.mjs @@ -4,6 +4,7 @@ import assert from 'node:assert/strict' import { mkdirSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' +import { chdir, cwd } from 'node:process' import { getLocalDependencies } from '../src/utils.ts' const fixtureDir = join(tmpdir(), `capgo-native-dependencies-${process.pid}`) @@ -70,3 +71,48 @@ try { finally { rmSync(fixtureDir, { recursive: true, force: true }) } + +// Monorepo hoisting: dependencies installed at the workspace root node_modules +// must be discovered when no explicit --node-modules path is given, by walking +// up parent directories from the app package. +const monorepoDir = join(tmpdir(), `capgo-native-dependencies-hoist-${process.pid}`) +const appDir = join(monorepoDir, 'packages', 'app') +const rootNodeModules = join(monorepoDir, 'node_modules') +const originalCwd = cwd() + +try { + mkdirSync(appDir, { recursive: true }) + mkdirSync(rootNodeModules, { recursive: true }) + + writeJson(join(appDir, 'package.json'), { + name: 'app', + dependencies: { + '@capgo/capacitor-updater': '^8.0.0', + }, + }) + + // Hoisted to the workspace root, not the app-local node_modules. + const hoistedPackageDir = join(rootNodeModules, '@capgo', 'capacitor-updater') + mkdirSync(join(hoistedPackageDir, 'ios'), { recursive: true }) + writeJson(join(hoistedPackageDir, 'package.json'), { + name: '@capgo/capacitor-updater', + version: '8.3.0', + capacitor: { ios: { src: 'ios' } }, + }) + writeFileSync(join(hoistedPackageDir, 'ios', 'UpdaterPlugin.swift'), 'final class UpdaterPlugin {}\n') + + // Run from the app directory so the default node_modules resolution has to + // walk up to the workspace root. + chdir(appDir) + const dependencies = await getLocalDependencies(join(appDir, 'package.json'), undefined) + const updater = dependencies.find(dep => dep.name === '@capgo/capacitor-updater') + + assert.equal(updater?.version, '8.3.0') + assert.equal(updater?.native, true) + + console.log('hoisted monorepo dependencies are resolved via the parent-directory walk') +} +finally { + chdir(originalCwd) + rmSync(monorepoDir, { recursive: true, force: true }) +} diff --git a/cli/test/test-posthog-exception.mjs b/cli/test/test-posthog-exception.mjs index 828d775fb1..98a2f5d753 100644 --- a/cli/test/test-posthog-exception.mjs +++ b/cli/test/test-posthog-exception.mjs @@ -65,7 +65,10 @@ try { assert.equal(body.properties.error_kind, 'unhandled_error') assert.equal(body.properties.status, 1) assert.match(body.properties.distinct_id, /^cli:[^:]+:bundle upload$/) - assert.match(body.properties.$exception_fingerprint, /cli:[^:]+:bundle upload:unhandled_error:Error:runUpload:/) + // Fingerprint must NOT include the CLI version, so the same bug stays one + // error-tracking issue across releases. + assert.equal(body.properties.$exception_fingerprint, 'bundle upload:unhandled_error:Error:runUpload:/src/index.ts:1') + assert.doesNotMatch(body.properties.$exception_fingerprint, /cli:/) assert.equal(body.properties.$exception_list[0].type, 'Error') assert.equal(body.properties.$exception_list[0].value, 'boom') assert.equal(body.properties.$exception_list[0].mechanism.handled, true)