diff --git a/.changeset/fix-global-wallet-dynamic-peer-drift.md b/.changeset/fix-global-wallet-dynamic-peer-drift.md new file mode 100644 index 000000000..e2d1b422c --- /dev/null +++ b/.changeset/fix-global-wallet-dynamic-peer-drift.md @@ -0,0 +1,11 @@ +--- +'@sei-js/sei-global-wallet': patch +--- + +Document that `@dynamic-labs/ethereum-aa` has to match the `@dynamic-labs/global-wallet-client` version npm resolves, and keep the release checks on that resolved version instead of a constant. + +Dynamic declares `@dynamic-labs/ethereum-aa` as an exact peer of its client and pins its internal packages to the client's version, so the two move together on every patch. `@dynamic-labs/global-wallet-client` is a `^4.96.3` dependency here, which means a Dynamic patch inside that range changes the peer version consumers need. Pinning an older `@dynamic-labs/ethereum-aa` than the resolved client does not fail the install: npm cannot place the client's exact peer beside the older root copy, so it nests the client under this package and duplicates the whole Dynamic runtime. The [Optional peer versions](https://github.com/sei-protocol/sei-js/blob/main/packages/sei-global-wallet/README.md#optional-peer-versions) table now states this and shows how to read the version the resolved client asks for. + +The consumer verifier resolved `4.96.3` regardless of what the range resolved to, so Dynamic publishing `@dynamic-labs/global-wallet-client@4.96.4` turned the nightly consumer run red on a duplicated Dynamic subtree rather than on any change in this repository. It now resolves the declared range against the registry, pins that client and the peer version it requests in each full consumer, and reports both, so a Dynamic patch is exercised the way an application receives it while a peer pin moving outside this package's published range still fails. A client that npm nests instead of hoisting is now reported as such, rather than as an unresolved dependency. + +No published dependency or peer range changes. diff --git a/packages/sei-global-wallet/README.md b/packages/sei-global-wallet/README.md index c511d9df9..86fbd0365 100644 --- a/packages/sei-global-wallet/README.md +++ b/packages/sei-global-wallet/README.md @@ -119,12 +119,12 @@ Both are `configurable` and `writable`, and neither is installed when the consum ## Optional peer versions -Install only the peers needed by the subpaths your application uses. The declared ranges stay deliberately wide, because an optional peer still fails `npm install` with `ERESOLVE` once your application has the package at a version outside the range. The **verified** column is Dynamic 4.96.3's own peer contract, which is the set this package's release checks run against. +Install only the peers needed by the subpaths your application uses. The declared ranges stay deliberately wide, because an optional peer still fails `npm install` with `ERESOLVE` once your application has the package at a version outside the range. The **verified** column is the version set the release checks install; the Dynamic AA entry tracks the client version resolved from the registry. | Peer | Declared range | Verified against | Needed by | | --- | --- | --- | --- | | `viem` | `^2.7.12` | `2.45.3` | `./zerodev`, Dynamic AA | -| `@dynamic-labs/ethereum-aa` | `^4.15.0` | `4.96.3` | `./zerodev` | +| `@dynamic-labs/ethereum-aa` | `^4.15.0` | `4.96.3+` (exact client match) | `./zerodev` | | `@zerodev/sdk` | `^5.4.36` | `5.5.7` | `./zerodev` | | `@solana/web3.js` | `^1.92.1` | `1.98.1` | `./solana` | | `@solana/wallet-standard-features` | `^1.2.0` | `^1.2.0` | `./solana` | @@ -133,7 +133,9 @@ Install only the peers needed by the subpaths your application uses. The declare | `@wallet-standard/wallet` | `^1.1.0` | `^1.1.0` | `./solana` | > [!NOTE] -> Dynamic 4.96.3 pins several of these exactly for itself, so a version outside the verified column can still be rejected by Dynamic's own peer contract during install, and is not covered by this package's checks. Prefer the verified versions; the wide ranges exist so that upgrading this package never breaks an install on its own. +> Dynamic pins several of these exactly for itself, so a version outside the verified column can still be rejected by Dynamic's own peer contract during install, and is not covered by this package's checks. Prefer the verified versions; the wide ranges exist so that upgrading this package never breaks an install on its own. +> +> `@dynamic-labs/ethereum-aa` is the strictest of them, because Dynamic pins it to its client's exact version. Since `@dynamic-labs/global-wallet-client` is a range here, pinning an older `@dynamic-labs/ethereum-aa` than the client npm resolves does not fail the install: npm cannot place that exact peer beside your older copy, so it nests a second Dynamic runtime under this package instead. Keep it on the exact version the resolved client asks for; `bun run test:sei-global-wallet-release` prints both versions at startup. > > Dynamic also declares an optional `zksync-sso@0.2.0` peer for its zkSync path. This package does not redeclare it, so npm surfaces that requirement from Dynamic directly. @@ -144,7 +146,7 @@ Two dependencies exist for transitive resolution rather than for this package's The root, `./eip6963`, and `./ethereum` entrypoints need no optional peer at all, including for type resolution. The release verifier typechecks them with `skipLibCheck: false` in a consumer that installs nothing but this package, so a published declaration that referenced a type from an uninstalled peer would fail the check. -`@dynamic-labs/global-wallet-client` is a `^4.96.3` dependency rather than an exact pin, so applications inherit Dynamic's transitive fixes without waiting for a release here. This repository's lockfile pins the exact version it tests. +`@dynamic-labs/global-wallet-client` is a `^4.96.3` dependency rather than an exact pin, so applications inherit Dynamic's transitive fixes without waiting for a release here. This repository's lockfile pins the exact version it tests. The consumer checks instead resolve that range against the registry on every run, so a Dynamic patch published inside it is exercised the way an application would actually receive it. A full Bun lockfile regeneration showed that the previous isolated Dynamic 4.96.1 subtree beneath `@dynamic-labs-wallet/browser-wallet-client@1.0.92` was stale: its `^4.81.0` ranges resolve compatibly without it. The verifier now asserts that no `@dynamic-labs/*` package resolves to more than one version, in npm and Bun graphs and in browser bundles, which stays meaningful across Dynamic upgrades. diff --git a/scripts/check-sei-global-wallet.ts b/scripts/check-sei-global-wallet.ts index a17a28a03..afe04f59f 100644 --- a/scripts/check-sei-global-wallet.ts +++ b/scripts/check-sei-global-wallet.ts @@ -5,7 +5,8 @@ import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import vm from 'node:vm'; -import { collectDynamicLineVersions, findDynamicLineConflicts, formatDynamicLineConflicts } from './dynamic-package-lock.js'; +import { highestVersion, normalizeNpmViewVersions, parseNpmViewResult } from './dynamic-package-contract.js'; +import { collectDynamicLineVersions, findDynamicLineConflicts, formatDynamicLineConflicts, listDynamicPackageInstallations } from './dynamic-package-lock.js'; interface ProcessResult { exitCode: number; @@ -33,7 +34,6 @@ interface PackageLock { const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const packageDir = join(root, 'packages/sei-global-wallet'); -const temporaryRoot = await mkdtemp(join(tmpdir(), 'sei-global-wallet-release-')); // Documented in packages/sei-global-wallet/README.md: skips the clean npm // consumers and the whole Bun path, so it never substitutes for a full run. const fastCheck = process.env.SEI_GLOBAL_WALLET_FAST_CHECK === '1'; @@ -45,30 +45,6 @@ const manifest = JSON.parse(await readFile(join(packageDir, 'package.json'), 'ut }; const dynamicRange = manifest.dependencies['@dynamic-labs/global-wallet-client']; -// The versions the consumers are built against: Dynamic 4.96.3's own peer -// contract. The published peer ranges stay wider than these on purpose, so an -// application that already carries one of these packages keeps resolving. -const testedPeerVersions = { - '@dynamic-labs/ethereum-aa': '4.96.3', - '@solana/wallet-standard-features': '^1.2.0', - '@solana/web3.js': '1.98.1', - '@wallet-standard/base': '^1.0.1', - '@wallet-standard/features': '^1.0.3', - '@wallet-standard/wallet': '^1.1.0', - '@zerodev/sdk': '5.5.7', - viem: '2.45.3', - 'zksync-sso': '0.2.0' -} as const; - -// Guards the two from drifting: every version the harness installs has to be -// allowed by the range the package publishes. -for (const [name, tested] of Object.entries(testedPeerVersions)) { - const declared = manifest.peerDependencies[name]; - if (!declared || tested.startsWith('^')) continue; - - assert(Bun.semver.satisfies(tested, declared), `Harness installs ${name}@${tested}, which the published ${name}@${declared} peer range excludes`); -} - // The published manifest carries a range so consumers inherit upstream fixes; // exactness belongs to the lockfile, not the contract. const assertSatisfiesDynamicRange = (version: string | undefined, label: string) => { @@ -116,6 +92,69 @@ const parseJsonOutput = (output: string): T => { const writeJson = (path: string, value: unknown) => writeFile(path, `${JSON.stringify(value, null, 2)}\n`); +const viewJson = async (specifier: string, field: string): Promise => { + const command = ['npm', 'view', specifier, field, '--json']; + return parseNpmViewResult(await run(command, root, true), command.join(' ')); +}; + +const viewVersions = async (specifier: string) => normalizeNpmViewVersions(await viewJson(specifier, 'version')); + +/** + * Dynamic pins `@dynamic-labs/ethereum-aa` as an exact peer of the client and + * pins its internal packages to the client's version, so the AA version these + * consumers install has to track whatever `dynamicRange` resolves to now rather + * than a constant here. A stale pin does not fail the install: npm cannot place + * the newer client's exact peer next to an older root copy, so it nests the + * client under this package and duplicates the whole Dynamic runtime instead. + */ +const resolveDynamicContract = async () => { + const clientVersions = await viewVersions(`@dynamic-labs/global-wallet-client@${dynamicRange}`); + assert(clientVersions.length > 0, `No @dynamic-labs/global-wallet-client version satisfies the declared ${dynamicRange}`); + const client = highestVersion(clientVersions); + + const peers = await viewJson>(`@dynamic-labs/global-wallet-client@${client}`, 'peerDependencies'); + const aaRange = peers?.['@dynamic-labs/ethereum-aa']; + assert(aaRange, `@dynamic-labs/global-wallet-client@${client} no longer declares an @dynamic-labs/ethereum-aa peer`); + + // Resolved to a concrete version, not passed through as a range, so the lock + // assertions below stay exact even if Dynamic ever loosens the peer. + const aaVersions = await viewVersions(`@dynamic-labs/ethereum-aa@${aaRange}`); + assert(aaVersions.length > 0, `No @dynamic-labs/ethereum-aa version satisfies the ${aaRange} peer of @dynamic-labs/global-wallet-client@${client}`); + + return { client, ethereumAa: highestVersion(aaVersions) }; +}; + +// AA follows the resolved Dynamic client's exact peer; the remaining versions +// are the fixed compatibility set these consumers exercise. The published peer +// ranges stay wider on purpose, so an application that already carries one of +// these packages keeps resolving. +const makeTestedPeerVersions = (ethereumAa: string) => + ({ + '@dynamic-labs/ethereum-aa': ethereumAa, + '@solana/wallet-standard-features': '^1.2.0', + '@solana/web3.js': '1.98.1', + '@wallet-standard/base': '^1.0.1', + '@wallet-standard/features': '^1.0.3', + '@wallet-standard/wallet': '^1.1.0', + '@zerodev/sdk': '5.5.7', + viem: '2.45.3', + 'zksync-sso': '0.2.0' + }) as const; + +type TestedPeerVersions = ReturnType; + +const assertTestedPeersFitPublishedRanges = (testedPeerVersions: TestedPeerVersions) => { + // Guards the two from drifting: every version the harness installs has to be + // allowed by the range the package publishes. This is what catches Dynamic + // moving its own peer pin outside the range published here. + for (const [name, tested] of Object.entries(testedPeerVersions)) { + const declared = manifest.peerDependencies[name]; + if (!declared || tested.startsWith('^')) continue; + + assert(Bun.semver.satisfies(tested, declared), `Harness installs ${name}@${tested}, which the published ${name}@${declared} peer range excludes`); + } +}; + const baseSafeOverrides = { axios: '1.18.0', uuid: '11.1.1' @@ -146,12 +185,13 @@ const walletOnlyManifest = (tarball: string, overrides?: Record ...(overrides ? { overrides } : {}) }); -const fullConsumerManifest = (tarball: string, packageManager: 'bun' | 'npm') => ({ +const fullConsumerManifest = (tarball: string, packageManager: 'bun' | 'npm', dynamicClientVersion: string, testedPeerVersions: TestedPeerVersions) => ({ name: 'sei-global-wallet-full-consumer', private: true, type: 'module', dependencies: { ...testedPeerVersions, + '@dynamic-labs/global-wallet-client': dynamicClientVersion, '@sei-js/sei-global-wallet': `file:${tarball}`, 'ethjs-unit': '0.1.6', 'number-to-bn': '1.7.0' @@ -623,9 +663,20 @@ const assertAcceptedBunAudit = (result: ProcessResult) => { ); }; -const assertNpmDynamicGraph = (lock: PackageLock) => { - const resolved = assertSatisfiesDynamicRange(lock.packages['node_modules/@dynamic-labs/global-wallet-client']?.version, 'npm consumer'); - assert.equal(lock.packages['node_modules/@dynamic-labs/ethereum-aa']?.version, testedPeerVersions['@dynamic-labs/ethereum-aa']); +const assertNpmDynamicGraph = (lock: PackageLock, expectedClientVersion: string, expectedAaVersion: string) => { + const hoisted = lock.packages['node_modules/@dynamic-labs/global-wallet-client']?.version; + // An install that succeeds without a hoisted client means npm could not place + // the client's exact peers at the root and nested it instead, which duplicates + // the Dynamic runtime rather than failing. Name those locations, because + // "did not resolve" would point at a dependency that is in fact installed. + if (!hoisted) { + const nested = listDynamicPackageInstallations(lock.packages, '@dynamic-labs/global-wallet-client'); + assert.deepEqual(nested, [], `npm consumer nested @dynamic-labs/global-wallet-client instead of hoisting it: ${nested.join(', ')}`); + } + + const resolved = assertSatisfiesDynamicRange(hoisted, 'npm consumer'); + assert.equal(resolved, expectedClientVersion, `npm consumer resolved client ${resolved}, expected the preflight contract ${expectedClientVersion}`); + assert.equal(lock.packages['node_modules/@dynamic-labs/ethereum-aa']?.version, expectedAaVersion); const conflicts = findDynamicLineConflicts(lock.packages, resolved); assert.deepEqual(conflicts, [], `npm consumer kept a stale Dynamic ${resolved} subtree: ${formatDynamicLineConflicts(conflicts)}`); @@ -633,7 +684,7 @@ const assertNpmDynamicGraph = (lock: PackageLock) => { return resolved; }; -const assertBunDynamicGraph = (lockText: string) => { +const assertBunDynamicGraph = (lockText: string, expectedClientVersion: string, expectedAaVersion: string) => { // Bun records every resolution as a "@" specifier rather than // as install locations, so group the specifiers instead of lock paths. const versions = new Map>(); @@ -644,9 +695,10 @@ const assertBunDynamicGraph = (lockText: string) => { const walletClient = [...(versions.get('@dynamic-labs/global-wallet-client') ?? [])]; assert.equal(walletClient.length, 1, `Bun consumer resolved multiple Dynamic clients: ${walletClient.join(', ')}`); const resolved = assertSatisfiesDynamicRange(walletClient[0], 'Bun consumer'); + assert.equal(resolved, expectedClientVersion, `Bun consumer resolved client ${resolved}, expected the preflight contract ${expectedClientVersion}`); assert( - [...(versions.get('@dynamic-labs/ethereum-aa') ?? [])].includes(testedPeerVersions['@dynamic-labs/ethereum-aa']), - `Bun consumer did not resolve @dynamic-labs/ethereum-aa@${testedPeerVersions['@dynamic-labs/ethereum-aa']}` + [...(versions.get('@dynamic-labs/ethereum-aa') ?? [])].includes(expectedAaVersion), + `Bun consumer did not resolve @dynamic-labs/ethereum-aa@${expectedAaVersion}` ); // Same major-line invariant as the npm graph: versions outside the client's @@ -699,7 +751,15 @@ const assertBrowserMetafile = async (metafilePath: string, lock: PackageLock) => assert(!runtimeImports.some(({ path }) => path === 'node:worker_threads' || path === 'worker_threads')); }; +const temporaryRoot = await mkdtemp(join(tmpdir(), 'sei-global-wallet-release-')); try { + const dynamicContract = await resolveDynamicContract(); + console.log( + `Dynamic ${dynamicRange} resolves to global-wallet-client@${dynamicContract.client}, whose peer contract pins @dynamic-labs/ethereum-aa@${dynamicContract.ethereumAa}.` + ); + const testedPeerVersions = makeTestedPeerVersions(dynamicContract.ethereumAa); + assertTestedPeersFitPublishedRanges(testedPeerVersions); + await run(['bun', 'run', '--cwd', packageDir, 'build'], root); const pack = await run(['npm', 'pack', '--json', '--pack-destination', temporaryRoot], packageDir); const [packResult] = parseJsonOutput(pack.stdout); @@ -758,11 +818,11 @@ try { const npmConsumerDir = join(temporaryRoot, 'npm-full'); await mkdir(npmConsumerDir); - await writeJson(join(npmConsumerDir, 'package.json'), fullConsumerManifest(tarball, 'npm')); + await writeJson(join(npmConsumerDir, 'package.json'), fullConsumerManifest(tarball, 'npm', dynamicContract.client, testedPeerVersions)); await setupConsumerFiles(npmConsumerDir); await run(['npm', 'install', '--no-audit', '--no-fund'], npmConsumerDir); const npmLock = JSON.parse(await readFile(join(npmConsumerDir, 'package-lock.json'), 'utf8')) as PackageLock; - assertNpmDynamicGraph(npmLock); + assertNpmDynamicGraph(npmLock, dynamicContract.client, dynamicContract.ethereumAa); assert.equal(npmLock.packages['node_modules/ethjs-unit/node_modules/bn.js']?.version, '4.12.5'); assert.equal(npmLock.packages['node_modules/number-to-bn/node_modules/bn.js']?.version, '4.12.5'); assertMajor(npmLock.packages['node_modules/bn.js']?.version, '5', 'hoisted bn.js'); @@ -823,11 +883,11 @@ try { if (!fastCheck) { const bunConsumerDir = join(temporaryRoot, 'bun-full'); await mkdir(bunConsumerDir); - await writeJson(join(bunConsumerDir, 'package.json'), fullConsumerManifest(tarball, 'bun')); + await writeJson(join(bunConsumerDir, 'package.json'), fullConsumerManifest(tarball, 'bun', dynamicContract.client, testedPeerVersions)); await setupConsumerFiles(bunConsumerDir); await run(['bun', 'install'], bunConsumerDir); const bunLock = await readFile(join(bunConsumerDir, 'bun.lock'), 'utf8'); - assertBunDynamicGraph(bunLock); + assertBunDynamicGraph(bunLock, dynamicContract.client, dynamicContract.ethereumAa); assert.match(bunLock, /"bn\.js": \["bn\.js@5\./); assert.match(bunLock, /"ethjs-unit\/bn\.js": \["bn\.js@4\./); assert.match(bunLock, /"number-to-bn\/bn\.js": \["bn\.js@4\./); diff --git a/scripts/dynamic-package-contract.test.ts b/scripts/dynamic-package-contract.test.ts new file mode 100644 index 000000000..96d27c1ef --- /dev/null +++ b/scripts/dynamic-package-contract.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from 'bun:test'; +import { highestVersion, normalizeNpmViewVersions, parseNpmViewResult } from './dynamic-package-contract.js'; + +describe('Dynamic registry contract helpers', () => { + test('normalizes npm view version scalars, arrays, and absent values', () => { + expect(normalizeNpmViewVersions('4.96.4')).toEqual(['4.96.4']); + expect(normalizeNpmViewVersions(['4.96.3', '4.96.4'])).toEqual(['4.96.3', '4.96.4']); + expect(normalizeNpmViewVersions(undefined)).toEqual([]); + }); + + test('selects the highest stable or prerelease version semantically', () => { + expect(highestVersion(['4.96.4-beta.10', '4.96.3', '4.96.4-beta.2', '4.96.4'])).toBe('4.96.4'); + expect(highestVersion(['4.96.4-beta.2', '4.96.4-beta.10'])).toBe('4.96.4-beta.10'); + }); + + test('parses npm view JSON scalars and arrays', () => { + expect(parseNpmViewResult({ exitCode: 0, stderr: '', stdout: '"4.96.4"\n' }, 'npm view')).toBe('4.96.4'); + expect(parseNpmViewResult({ exitCode: 0, stderr: '', stdout: '["4.96.3","4.96.4"]\n' }, 'npm view')).toEqual(['4.96.3', '4.96.4']); + }); + + test('returns no value for absent fields and versions', () => { + expect(parseNpmViewResult({ exitCode: 0, stderr: '', stdout: '' }, 'npm view')).toBeUndefined(); + expect( + parseNpmViewResult( + { + exitCode: 1, + stderr: 'npm error code E404\n', + stdout: '{"error":{"code":"E404","summary":"No match found"}}' + }, + 'npm view' + ) + ).toBeUndefined(); + }); + + test('preserves non-404 registry failures', () => { + expect(() => parseNpmViewResult({ exitCode: 1, stderr: 'npm error code EAI_AGAIN\n', stdout: '' }, 'npm view package version --json')).toThrow( + 'npm view package version --json failed\nnpm error code EAI_AGAIN' + ); + }); +}); diff --git a/scripts/dynamic-package-contract.ts b/scripts/dynamic-package-contract.ts new file mode 100644 index 000000000..d159485a6 --- /dev/null +++ b/scripts/dynamic-package-contract.ts @@ -0,0 +1,33 @@ +interface ProcessResult { + exitCode: number; + stderr: string; + stdout: string; +} + +const npmErrorCode = (output: string) => { + try { + return (JSON.parse(output) as { error?: { code?: string } }).error?.code; + } catch { + return undefined; + } +}; + +/** + * Parses `npm view --json` without turning an absent field or version into an + * unrelated JSON syntax error. Other registry failures retain their diagnostics. + */ +export const parseNpmViewResult = (result: ProcessResult, command: string): T | undefined => { + const plain = result.stdout.trim(); + if (result.exitCode !== 0) { + if (npmErrorCode(plain) === 'E404') return undefined; + throw new Error(`${command} failed\n${result.stderr}${result.stdout}`); + } + + return plain ? (JSON.parse(plain) as T) : undefined; +}; + +/** `npm view version --json` returns a scalar or array by match count. */ +export const normalizeNpmViewVersions = (versions: string | string[] | undefined) => (versions === undefined ? [] : [versions].flat()); + +export const highestVersion = (versions: readonly string[]) => + versions.reduce((highest, version) => (Bun.semver.order(version, highest) > 0 ? version : highest)); diff --git a/scripts/dynamic-package-lock.test.ts b/scripts/dynamic-package-lock.test.ts index 72842b633..c53f13c67 100644 --- a/scripts/dynamic-package-lock.test.ts +++ b/scripts/dynamic-package-lock.test.ts @@ -1,5 +1,11 @@ import { describe, expect, test } from 'bun:test'; -import { collectDynamicLineVersions, dynamicLockPackageName, findDynamicLineConflicts, formatDynamicLineConflicts } from './dynamic-package-lock.js'; +import { + collectDynamicLineVersions, + dynamicLockPackageName, + findDynamicLineConflicts, + formatDynamicLineConflicts, + listDynamicPackageInstallations +} from './dynamic-package-lock.js'; describe('dynamic package lock locations', () => { test('names hoisted and nested Dynamic packages, and nothing else', () => { @@ -11,6 +17,19 @@ describe('dynamic package lock locations', () => { expect(dynamicLockPackageName('node_modules/axios')).toBeUndefined(); }); + test('lists a client installed only beneath the package that depends on it', () => { + expect( + listDynamicPackageInstallations( + { + 'node_modules/@dynamic-labs/ethereum-aa': { version: '4.96.3' }, + 'node_modules/@sei-js/sei-global-wallet/node_modules/@dynamic-labs/global-wallet-client': { version: '4.96.4' }, + 'node_modules/@sei-js/sei-global-wallet/node_modules/@dynamic-labs/types': { version: '4.96.4' } + }, + '@dynamic-labs/global-wallet-client' + ) + ).toEqual(['node_modules/@sei-js/sei-global-wallet/node_modules/@dynamic-labs/global-wallet-client@4.96.4']); + }); + test('collects only the resolutions inside the requested major line', () => { const versions = collectDynamicLineVersions( { diff --git a/scripts/dynamic-package-lock.ts b/scripts/dynamic-package-lock.ts index 4b9df8123..73876e760 100644 --- a/scripts/dynamic-package-lock.ts +++ b/scripts/dynamic-package-lock.ts @@ -3,6 +3,13 @@ const DYNAMIC_PACKAGE_LOCATION = /(?:^|\/)node_modules\/(@dynamic-labs\/[^/]+)$/ /** Returns the `@dynamic-labs/*` package a lock location installs, at any nesting depth. */ export const dynamicLockPackageName = (location: string) => location.match(DYNAMIC_PACKAGE_LOCATION)?.[1]; +/** Lists every npm lock location for one Dynamic package, including nested copies. */ +export const listDynamicPackageInstallations = (packages: Record, packageName: string) => + Object.entries(packages) + .filter(([location]) => dynamicLockPackageName(location) === packageName) + .map(([location, metadata]) => `${location}@${metadata.version}`) + .sort(); + const majorOf = (version: string) => version.split('.')[0]; /**