Skip to content

Commit

Permalink
feat: time-base resolution mode
Browse files Browse the repository at this point in the history
  • Loading branch information
zkochan committed Aug 30, 2022
1 parent 87d8226 commit 9e138b9
Show file tree
Hide file tree
Showing 36 changed files with 270 additions and 91 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -39,7 +39,7 @@
"@commitlint/prompt-cli": "^17.0.3",
"@pnpm/eslint-config": "workspace:*",
"@pnpm/meta-updater": "0.0.6",
"@pnpm/registry-mock": "3.0.0-2",
"@pnpm/registry-mock": "3.0.0-3",
"@pnpm/tsconfig": "workspace:*",
"@types/jest": "^28.1.8",
"@types/node": "^14.18.26",
Expand Down
2 changes: 2 additions & 0 deletions packages/config/src/Config.ts
Expand Up @@ -83,6 +83,8 @@ export interface Config {
useStderr?: boolean
nodeLinker?: 'hoisted' | 'isolated' | 'pnp'
preferSymlinkedExecutables?: boolean
resolutionMode?: 'highest' | 'time-based'
registrySupportsTimeField?: boolean

// proxy
httpProxy?: string
Expand Down
4 changes: 4 additions & 0 deletions packages/config/src/index.ts
Expand Up @@ -96,6 +96,7 @@ export const types = Object.assign({
'publish-branch': String,
'recursive-install': Boolean,
reporter: String,
'resolution-mode': ['highest', 'time-based'],
'aggregate-output': Boolean,
'save-peer': Boolean,
'save-workspace-protocol': Boolean,
Expand Down Expand Up @@ -127,6 +128,7 @@ export const types = Object.assign({
'changed-files-ignore-pattern': [String, Array],
'embed-readme': Boolean,
'update-notifier': Boolean,
'registry-supports-time-field': Boolean,
}, npmTypes.types)

export type CliOptions = Record<string, unknown> & { dir?: string }
Expand Down Expand Up @@ -215,6 +217,7 @@ export default async (
],
'recursive-install': true,
registry: npmDefaults.registry,
'resolution-mode': 'highest',
'save-peer': false,
'save-workspace-protocol': true,
'scripts-prepend-node-path': false,
Expand All @@ -234,6 +237,7 @@ export default async (
'workspace-concurrency': 4,
'workspace-prefix': opts.workspaceDir,
'embed-readme': false,
'registry-supports-time-field': false,
})

npmConfig.addFile(path.resolve(path.join(__dirname, 'pnpmrc')), 'pnpm-builtin')
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Expand Up @@ -77,7 +77,7 @@
"@pnpm/logger": "^4.0.0",
"@pnpm/package-store": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "3.0.0-2",
"@pnpm/registry-mock": "3.0.0-3",
"@pnpm/store-path": "workspace:*",
"@pnpm/test-fixtures": "workspace:*",
"@types/fs-extra": "^9.0.13",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/install/extendInstallOptions.ts
Expand Up @@ -99,6 +99,7 @@ export interface StrictInstallOptions {
peerDependencyRules: PeerDependencyRules
allowedDeprecatedVersions: AllowedDeprecatedVersions
preferSymlinkedExecutables: boolean
resolutionMode: 'highest' | 'time-based'

publicHoistPattern: string[] | undefined
hoistPattern: string[] | undefined
Expand Down Expand Up @@ -164,6 +165,7 @@ const defaults = async (opts: InstallOptions) => {
pruneStore: false,
rawConfig: {},
registries: DEFAULT_REGISTRIES,
resolutionMode: 'highest',
saveWorkspaceProtocol: true,
lockfileIncludeTarballUrl: false,
scriptsPrependNodePath: false,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/install/index.ts
Expand Up @@ -835,6 +835,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
preferredVersions,
preserveWorkspaceProtocol: opts.preserveWorkspaceProtocol,
registries: ctx.registries,
resolutionMode: opts.resolutionMode,
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
storeController: opts.storeController,
tag: opts.tag,
Expand Down
17 changes: 17 additions & 0 deletions packages/core/test/install/timeBasedResolutionMode.ts
@@ -0,0 +1,17 @@
import { prepareEmpty } from '@pnpm/prepare'
import { addDependenciesToPackage } from '@pnpm/core'
import { testDefaults } from '../utils'

test('time-based resolution mode', async () => {
const project = prepareEmpty()

await addDependenciesToPackage({}, ['@pnpm.e2e/bravo', '@pnpm.e2e/romeo'], await testDefaults({ resolutionMode: 'time-based' }))

const lockfile = await project.readLockfile()
expect(Object.keys(lockfile.packages)).toStrictEqual([
'/@pnpm.e2e/bravo-dep/1.0.1',
'/@pnpm.e2e/bravo/1.0.0',
'/@pnpm.e2e/romeo-dep/1.0.0',
'/@pnpm.e2e/romeo/1.0.0',
])
})
2 changes: 1 addition & 1 deletion packages/headless/package.json
Expand Up @@ -23,7 +23,7 @@
"@pnpm/package-store": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/read-projects-context": "workspace:*",
"@pnpm/registry-mock": "3.0.0-2",
"@pnpm/registry-mock": "3.0.0-3",
"@pnpm/store-path": "workspace:*",
"@pnpm/test-fixtures": "workspace:*",
"@types/fs-extra": "^9.0.13",
Expand Down
2 changes: 1 addition & 1 deletion packages/lockfile-file/src/sortLockfileKeys.ts
Expand Up @@ -79,7 +79,7 @@ export function sortLockfileKeys (lockfile: LockfileFile) {
})
}
}
for (const key of ['specifiers', 'dependencies', 'devDependencies', 'optionalDependencies']) {
for (const key of ['specifiers', 'dependencies', 'devDependencies', 'optionalDependencies', 'time']) {
if (!lockfile[key]) continue
lockfile[key] = sortKeys(lockfile[key])
}
Expand Down
1 change: 1 addition & 0 deletions packages/lockfile-types/src/index.ts
Expand Up @@ -5,6 +5,7 @@ export { PatchFile }
export interface Lockfile {
importers: Record<string, ProjectSnapshot>
lockfileVersion: number
time?: Record<string, string>
packages?: PackageSnapshots
neverBuiltDependencies?: string[]
onlyBuiltDependencies?: string[]
Expand Down
2 changes: 2 additions & 0 deletions packages/npm-resolver/package.json
Expand Up @@ -41,6 +41,7 @@
"@pnpm/resolve-workspace-range": "workspace:*",
"@pnpm/resolver-base": "workspace:*",
"@pnpm/types": "workspace:*",
"@types/ramda": "0.28.15",
"@zkochan/retry": "^0.2.0",
"encode-registry": "^3.0.0",
"load-json-file": "^6.2.0",
Expand All @@ -50,6 +51,7 @@
"p-memoize": "4.0.1",
"parse-npm-tarball-url": "^3.0.0",
"path-temp": "^2.0.0",
"ramda": "npm:@pnpm/ramda@0.28.1",
"rename-overwrite": "^4.0.2",
"semver": "^7.3.7",
"ssri": "^9.0.1",
Expand Down
8 changes: 7 additions & 1 deletion packages/npm-resolver/src/index.ts
Expand Up @@ -55,10 +55,12 @@ export {
// of one package/version
const META_DIR = 'metadata'
const FULL_META_DIR = 'metadata-full'
const FULL_FILTERED_META_DIR = 'metadata-full-filtered'

export interface ResolverFactoryOptions {
cacheDir: string
fullMetadata?: boolean
filterMetadata?: boolean
offline?: boolean
preferOffline?: boolean
retry?: RetryTimeoutOptions
Expand Down Expand Up @@ -90,8 +92,9 @@ export default function createResolver (
getAuthHeaderValueByURI,
pickPackage: pickPackage.bind(null, {
fetch,
filterMetadata: opts.filterMetadata,
metaCache,
metaDir: opts.fullMetadata ? FULL_META_DIR : META_DIR,
metaDir: opts.fullMetadata ? (opts.filterMetadata ? FULL_FILTERED_META_DIR : FULL_META_DIR) : META_DIR,
offline: opts.offline,
preferOffline: opts.preferOffline,
cacheDir: opts.cacheDir,
Expand All @@ -102,6 +105,7 @@ export default function createResolver (
export type ResolveFromNpmOptions = {
alwaysTryWorkspacePackages?: boolean
defaultTag?: string
publishedBy?: Date
dryRun?: boolean
lockfileDir?: string
registry: string
Expand Down Expand Up @@ -147,6 +151,7 @@ async function resolveNpm (
let pickResult!: {meta: PackageMeta, pickedPackage: PackageInRegistry | null}
try {
pickResult = await ctx.pickPackage(spec, {
publishedBy: opts.publishedBy,
authHeaderValue,
dryRun: opts.dryRun === true,
preferredVersionSelectors: opts.preferredVersions?.[spec.name],
Expand Down Expand Up @@ -214,6 +219,7 @@ async function resolveNpm (
normalizedPref: spec.normalizedPref,
resolution,
resolvedVia: 'npm-registry',
timeString: meta.time?.[pickedPackage.version],
}
}

Expand Down
61 changes: 55 additions & 6 deletions packages/npm-resolver/src/pickPackage.ts
Expand Up @@ -10,14 +10,17 @@ import getRegistryName from 'encode-registry'
import loadJsonFile from 'load-json-file'
import pLimit from 'p-limit'
import pathTemp from 'path-temp'
import pick from 'ramda/src/pick'
import renameOverwrite from 'rename-overwrite'
import toRaw from './toRaw'
import pickPackageFromMeta from './pickPackageFromMeta'
import { RegistryPackageSpec } from './parsePref'

export interface PackageMeta {
name: string
'dist-tags': Record<string, string>
versions: Record<string, PackageInRegistry>
time?: Record<string, string>
cachedAt?: number
}

Expand Down Expand Up @@ -45,6 +48,7 @@ const metafileOperationLimits = {} as {

export interface PickPackageOptions {
authHeaderValue?: string
publishedBy?: Date
preferredVersionSelectors: VersionSelectors | undefined
registry: string
dryRun: boolean
Expand All @@ -58,6 +62,7 @@ export default async (
cacheDir: string
offline?: boolean
preferOffline?: boolean
filterMetadata?: boolean
},
spec: RegistryPackageSpec,
opts: PickPackageOptions
Expand All @@ -70,7 +75,7 @@ export default async (
if (cachedMeta != null) {
return {
meta: cachedMeta,
pickedPackage: pickPackageFromMeta(spec, opts.preferredVersionSelectors, cachedMeta),
pickedPackage: pickPackageFromMeta(spec, opts.preferredVersionSelectors, cachedMeta, opts.publishedBy),
}
}

Expand All @@ -85,14 +90,14 @@ export default async (
if (ctx.offline) {
if (metaCachedInStore != null) return {
meta: metaCachedInStore,
pickedPackage: pickPackageFromMeta(spec, opts.preferredVersionSelectors, metaCachedInStore),
pickedPackage: pickPackageFromMeta(spec, opts.preferredVersionSelectors, metaCachedInStore, opts.publishedBy),
}

throw new PnpmError('NO_OFFLINE_META', `Failed to resolve ${toRaw(spec)} in package mirror ${pkgMirror}`)
}

if (metaCachedInStore != null) {
const pickedPackage = pickPackageFromMeta(spec, opts.preferredVersionSelectors, metaCachedInStore)
const pickedPackage = pickPackageFromMeta(spec, opts.preferredVersionSelectors, metaCachedInStore, opts.publishedBy)
if (pickedPackage) {
return {
meta: metaCachedInStore,
Expand All @@ -113,9 +118,21 @@ export default async (
}
}
}
if (opts.publishedBy) {
metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror))
if (metaCachedInStore?.cachedAt && new Date(metaCachedInStore.cachedAt) >= opts.publishedBy) {
return {
meta: metaCachedInStore,
pickedPackage: pickPackageFromMeta(spec, opts.preferredVersionSelectors, metaCachedInStore, opts.publishedBy),
}
}
}

try {
const meta = await ctx.fetch(spec.name, opts.registry, opts.authHeaderValue)
let meta = await ctx.fetch(spec.name, opts.registry, opts.authHeaderValue)
if (ctx.filterMetadata) {
meta = clearMeta(meta)
}
meta.cachedAt = Date.now()
// only save meta to cache, when it is fresh
ctx.metaCache.set(spec.name, meta)
Expand All @@ -131,7 +148,7 @@ export default async (
}
return {
meta,
pickedPackage: pickPackageFromMeta(spec, opts.preferredVersionSelectors, meta),
pickedPackage: pickPackageFromMeta(spec, opts.preferredVersionSelectors, meta, opts.publishedBy),
}
} catch (err: any) { // eslint-disable-line
err.spec = spec
Expand All @@ -141,11 +158,43 @@ export default async (
logger.debug({ message: `Using cached meta from ${pkgMirror}` })
return {
meta,
pickedPackage: pickPackageFromMeta(spec, opts.preferredVersionSelectors, meta),
pickedPackage: pickPackageFromMeta(spec, opts.preferredVersionSelectors, meta, opts.publishedBy),
}
}
}

function clearMeta (pkg: PackageMeta): PackageMeta {
const versions = {}
for (const [version, info] of Object.entries(pkg.versions)) {
versions[version] = pick([
'name',
'version',
'bin',
'directories',
'devDependencies',
'optionalDependencies',
'dependencies',
'peerDependencies',
'dist',
'engines',
'peerDependenciesMeta',
'cpu',
'os',
'deprecated',
'bundleDependencies',
'bundledDependencies',
], info)
}

return {
name: pkg.name,
'dist-tags': pkg['dist-tags'],
versions,
time: pkg.time,
cachedAt: pkg.cachedAt,
}
}

function encodePkgName (pkgName: string) {
if (pkgName !== pkgName.toLowerCase()) {
return `${pkgName}_${crypto.createHash('md5').update(pkgName).digest('hex')}`
Expand Down
26 changes: 17 additions & 9 deletions packages/npm-resolver/src/pickPackageFromMeta.ts
Expand Up @@ -7,7 +7,8 @@ import { PackageInRegistry, PackageMeta } from './pickPackage'
export default function (
spec: RegistryPackageSpec,
preferredVersionSelectors: VersionSelectors | undefined,
meta: PackageMeta
meta: PackageMeta,
publishedBy?: Date
): PackageInRegistry | null {
try {
let version!: string | null
Expand All @@ -19,7 +20,7 @@ export default function (
version = meta['dist-tags'][spec.fetchSpec]
break
case 'range':
version = pickVersionByVersionRange(meta, spec.fetchSpec, preferredVersionSelectors)
version = pickVersionByVersionRange(meta, spec.fetchSpec, preferredVersionSelectors, publishedBy)
break
}
if (!version) return null
Expand All @@ -44,10 +45,11 @@ export default function (
function pickVersionByVersionRange (
meta: PackageMeta,
versionRange: string,
preferredVerSels?: VersionSelectors
): string | null {
preferredVerSels?: VersionSelectors,
publishedBy?: Date
) {
let versions: string[] | undefined
const latest = meta['dist-tags'].latest
let latest: string | undefined = meta['dist-tags'].latest

const preferredVerSelsArr = Object.entries(preferredVerSels ?? {})
if (preferredVerSelsArr.length > 0) {
Expand Down Expand Up @@ -89,12 +91,18 @@ function pickVersionByVersionRange (
}
}

// Not using semver.satisfies in case of * because it does not select beta versions.
// E.g.: 1.0.0-beta.1. See issue: https://github.com/pnpm/pnpm/issues/865
if (versionRange === '*' || semver.satisfies(latest, versionRange, true)) {
versions = versions ?? Object.keys(meta.versions)
if (publishedBy) {
versions = versions.filter(version => new Date(meta.time![version]) <= publishedBy)
if (!versions.includes(latest)) {
latest = undefined
}
}
if (latest && (versionRange === '*' || semver.satisfies(latest, versionRange, true))) {
// Not using semver.satisfies in case of * because it does not select beta versions.
// E.g.: 1.0.0-beta.1. See issue: https://github.com/pnpm/pnpm/issues/865
return latest
}
versions = versions ?? Object.keys(meta.versions)

const maxVersion = semver.maxSatisfying(versions, versionRange, true)

Expand Down
2 changes: 1 addition & 1 deletion packages/package-requester/package.json
Expand Up @@ -68,7 +68,7 @@
"@pnpm/create-cafs-store": "workspace:*",
"@pnpm/logger": "^4.0.0",
"@pnpm/package-requester": "workspace:*",
"@pnpm/registry-mock": "3.0.0-2",
"@pnpm/registry-mock": "3.0.0-3",
"@pnpm/test-fixtures": "workspace:*",
"@types/normalize-path": "^3.0.0",
"@types/ramda": "0.28.15",
Expand Down

0 comments on commit 9e138b9

Please sign in to comment.