Skip to content

Commit

Permalink
feat: add support for URL in "packageManager"
Browse files Browse the repository at this point in the history
  • Loading branch information
aduh95 committed Jan 22, 2024
1 parent a3f271c commit 731a970
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 48 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ along with the SHA-224 hash of this version for validation.
recommended as a security practice. Permitted values for the package manager are
`yarn`, `npm`, and `pnpm`.

You can also provide a URL to a `.js` file (which will be interpreted as a
CommonJS module) or a `.tgz` file (which will be interpreted as a package, and
the `"bin"` field of the `package.json` will be used to determine which file to
use in the archive).

```json
{
"packageManager": "yarn@https://registry.npmjs.org/@yarnpkg/cli-dist/-/cli-dist-3.2.3.tgz#sha224.16a0797d1710d1fb7ec40ab5c3801b68370a612a9b66ba117ad9924b"
}
```

## Known Good Releases

When running Corepack within projects that don't list a supported package
Expand Down
41 changes: 30 additions & 11 deletions sources/Engine.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import {UsageError} from 'clipanion';
import fs from 'fs';
import path from 'path';
import process from 'process';
import semver from 'semver';
import {UsageError} from 'clipanion';
import fs from 'fs';
import path from 'path';
import process from 'process';
import semver from 'semver';

import defaultConfig from '../config.json';
import defaultConfig from '../config.json';

import * as corepackUtils from './corepackUtils';
import * as folderUtils from './folderUtils';
import * as semverUtils from './semverUtils';
import {Config, Descriptor, Locator} from './types';
import {SupportedPackageManagers, SupportedPackageManagerSet} from './types';
import * as corepackUtils from './corepackUtils';
import * as folderUtils from './folderUtils';
import * as semverUtils from './semverUtils';
import {Config, Descriptor, Locator, SupportedPackageManagerDescriptor, SupportedPackageManagerLocator, URLDescriptor} from './types';

Check failure on line 12 in sources/Engine.ts

View workflow job for this annotation

GitHub Actions / Testing chores

'"./types"' has no exported member named 'URLDescriptor'. Did you mean 'Descriptor'?
import {SupportedPackageManagers, SupportedPackageManagerSet} from './types';

export type PreparedPackageManagerInfo = Awaited<ReturnType<Engine[`ensurePackageManager`]>>;

Expand All @@ -35,6 +35,18 @@ export class Engine {
}

getPackageManagerSpecFor(locator: Locator) {
if (!corepackUtils.isSupportedPackageManagerLocator(locator)) {
return {
url: locator.reference,
bin: {},
registry: {
type: `url`,
url: locator.reference,
fields: {},
},
};
}

const definition = this.config.definitions[locator.name];
if (typeof definition === `undefined`)
throw new UsageError(`This package manager (${locator.name}) isn't supported by this corepack build`);
Expand Down Expand Up @@ -143,6 +155,13 @@ export class Engine {
}

async resolveDescriptor(descriptor: Descriptor, {allowTags = false, useCache = true}: {allowTags?: boolean, useCache?: boolean} = {}) {
if (!corepackUtils.isSupportedPackageManagerDescriptor(descriptor)) {
return {
name: descriptor.name,
reference: new URL(descriptor.range),
};
}

const definition = this.config.definitions[descriptor.name];
if (typeof definition === `undefined`)
throw new UsageError(`This package manager (${descriptor.name}) isn't supported by this corepack build`);
Expand Down
92 changes: 67 additions & 25 deletions sources/corepackUtils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import {createHash} from 'crypto';
import {once} from 'events';
import fs from 'fs';
import type {Dir} from 'fs';
import Module from 'module';
import path from 'path';
import semver from 'semver';

import * as debugUtils from './debugUtils';
import * as folderUtils from './folderUtils';
import * as fsUtils from './fsUtils';
import * as httpUtils from './httpUtils';
import * as nodeUtils from './nodeUtils';
import * as npmRegistryUtils from './npmRegistryUtils';
import {RegistrySpec, Descriptor, Locator, PackageManagerSpec} from './types';
import {createHash} from 'crypto';
import {once} from 'events';
import fs from 'fs';
import type {Dir} from 'fs';
import Module from 'module';
import path from 'path';
import semver from 'semver';

import * as debugUtils from './debugUtils';
import * as folderUtils from './folderUtils';
import * as fsUtils from './fsUtils';
import * as httpUtils from './httpUtils';
import * as nodeUtils from './nodeUtils';
import * as npmRegistryUtils from './npmRegistryUtils';
import {RegistrySpec, Descriptor, Locator, PackageManagerSpec, SupportedPackageManagerDescriptor, SupportedPackageManagerLocator, URLLocator} from './types';

export function getRegistryFromPackageManagerSpec(spec: PackageManagerSpec) {
return process.env.COREPACK_NPM_REGISTRY
Expand Down Expand Up @@ -102,17 +102,46 @@ export async function findInstalledVersion(installTarget: string, descriptor: De
return bestMatch;
}

export function isSupportedPackageManagerDescriptor(descriptor: Descriptor): descriptor is SupportedPackageManagerDescriptor {
return !URL.canParse(descriptor.range);
}

export function isSupportedPackageManagerLocator(locator: Locator): locator is SupportedPackageManagerLocator {
return typeof locator.reference === `string`;
}

function parseURLReference(locator: URLLocator) {
const {hash, href} = locator.reference;
if (hash) {
return {
version: encodeURIComponent(href.slice(0, -hash.length)),
build: hash.slice(1).split(`.`),
};
}
return {version: href, build: []};
}

export async function installVersion(installTarget: string, locator: Locator, {spec}: {spec: PackageManagerSpec}) {
const {default: tar} = await import(`tar`);
const {version, build} = semver.parse(locator.reference)!;
const locatorIsASupportedPackageManager = isSupportedPackageManagerLocator(locator);
const {version, build} = locatorIsASupportedPackageManager ? semver.parse(locator.reference)! : parseURLReference(locator);

const installFolder = path.join(installTarget, locator.name, version);
const corepackFile = path.join(installFolder, `.corepack`);

let corepackContent;
try {
if (locatorIsASupportedPackageManager) {
corepackContent = await fs.promises.readFile(corepackFile, `utf8`);
}
} catch (err) {
if ((err as nodeUtils.NodeError)?.code !== `ENOENT`) {
throw err;
}
}
// Older versions of Corepack didn't generate the `.corepack` file; in
// that case we just download the package manager anew.
if (fs.existsSync(corepackFile)) {
const corepackContent = await fs.promises.readFile(corepackFile, `utf8`);
if (corepackContent) {
const corepackData = JSON.parse(corepackContent);

debugUtils.log(`Reusing ${locator.name}@${locator.reference}`);
Expand All @@ -123,13 +152,18 @@ export async function installVersion(installTarget: string, locator: Locator, {s
};
}

const defaultNpmRegistryURL = spec.url.replace(`{}`, version);
const url = process.env.COREPACK_NPM_REGISTRY ?
defaultNpmRegistryURL.replace(
npmRegistryUtils.DEFAULT_NPM_REGISTRY_URL,
() => process.env.COREPACK_NPM_REGISTRY!,
) :
defaultNpmRegistryURL;
let url: string;
if (locatorIsASupportedPackageManager) {
const defaultNpmRegistryURL = spec.url.replace(`{}`, version);
url = process.env.COREPACK_NPM_REGISTRY ?
defaultNpmRegistryURL.replace(
npmRegistryUtils.DEFAULT_NPM_REGISTRY_URL,
() => process.env.COREPACK_NPM_REGISTRY!,
) :
defaultNpmRegistryURL;
} else {
url = decodeURIComponent(version);
}

// Creating a temporary folder inside the install folder means that we
// are sure it'll be in the same drive as the destination, so we can
Expand Down Expand Up @@ -158,6 +192,14 @@ export async function installVersion(installTarget: string, locator: Locator, {s
const hash = stream.pipe(createHash(algo));
await once(sendTo, `finish`);

if (!locatorIsASupportedPackageManager) {
if (ext === `.tgz`) {
spec.bin = require(path.join(tmpFolder, `package.json`)).bin;
} else if (ext === `.js`) {
spec.bin = [locator.name];
}
}

const actualHash = hash.digest(`hex`);
if (build[1] && actualHash !== build[1])
throw new Error(`Mismatch hashes. Expected ${build[1]}, got ${actualHash}`);
Expand Down
36 changes: 29 additions & 7 deletions sources/specUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,38 @@ export function parseSpec(raw: unknown, source: string, {enforceExactVersion = t
if (typeof raw !== `string`)
throw new UsageError(`Invalid package manager specification in ${source}; expected a string`);

const match = raw.match(/^(?!_)([^@]+)(?:@(.+))?$/);
if (match === null || (enforceExactVersion && (!match[2] || !semver.valid(match[2]))))
throw new UsageError(`Invalid package manager specification in ${source} (${raw}); expected a semver version${enforceExactVersion ? `` : `, range, or tag`}`);
const atIndex = raw.indexOf(`@`);

if (atIndex === -1 || atIndex === raw.length - 1) {
if (enforceExactVersion)
throw new UsageError(`No version specified for ${raw} in "packageManager" of ${source}`);

const name = atIndex === -1 ? raw : raw.slice(0, -1);
if (!isSupportedPackageManager(name))
throw new UsageError(`Unsupported package manager specification (${name})`);

return {
name, range: `*`,
};
}

const name = raw.slice(0, atIndex);
const range = raw.slice(atIndex + 1);

const isURL = URL.canParse(range);
if (!isURL) {
if (enforceExactVersion && !semver.valid(range))
throw new UsageError(`Invalid package manager specification in ${source} (${raw}); expected a semver version${enforceExactVersion ? `` : `, range, or tag`}`);

if (!isSupportedPackageManager(name)) {
throw new UsageError(`Unsupported package manager specification (${raw})`);
}
}

if (!isSupportedPackageManager(match[1]))
throw new UsageError(`Unsupported package manager specification (${match})`);

return {
name: match[1],
range: match[2] ?? `*`,
name,
range,
};
}

Expand Down
22 changes: 20 additions & 2 deletions sources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ export interface Config {
* A structure containing the information needed to locate the package
* manager to use for the active project.
*/
export interface Descriptor {
export type Descriptor = SupportedPackageManagerDescriptor | URLDescriptor;
export interface SupportedPackageManagerDescriptor {
/**
* The name of the package manager required.
*/
Expand All @@ -107,11 +108,24 @@ export interface Descriptor {
*/
range: string;
}
interface URLDescriptor {
/**
* The name of the package manager required.
*/
name: string;

/**
* The range of versions allowed.
*/
range: string;
}

/**
*
*/
export interface Locator {
export type Locator = SupportedPackageManagerLocator | URLLocator;

export interface SupportedPackageManagerLocator {
/**
* The name of the package manager required.
*/
Expand All @@ -122,3 +136,7 @@ export interface Locator {
*/
reference: string;
}
export interface URLLocator {
name: string;
reference: URL;
}
14 changes: 11 additions & 3 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@ it(`should require a version to be specified`, async () => {
});
});

const testedPackageManagers: Array<[string, string]> = [
const testedPackageManagers: Array<[string, string] | [string, string, string]> = [
[`yarn`, `1.22.4`],
[`yarn`, `1.22.4+sha1.01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e`],
[`yarn`, `1.22.4+sha224.0d6eecaf4d82ec12566fdd97143794d0f0c317e0d652bd4d1b305430`],
[`yarn`, `https://registry.npmjs.com/yarn/-/yarn-1.22.21.tgz`, `1.22.21`],
[`yarn`, `https://registry.npmjs.com/yarn/-/yarn-1.22.21.tgz#sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72`, `1.22.21`],
[`yarn`, `2.0.0-rc.30`],
[`yarn`, `2.0.0-rc.30+sha1.4f0423b01bcb57f8e390b4e0f1990831f92dd1da`],
[`yarn`, `2.0.0-rc.30+sha224.0e7a64468c358596db21c401ffeb11b6534fce7367afd3ae640eadf1`],
Expand All @@ -84,17 +86,23 @@ const testedPackageManagers: Array<[string, string]> = [
[`npm`, `6.14.2+sha224.50512c1eb404900ee78586faa6d756b8d867ff46a328e6fb4cdf3a87`],
];

for (const [name, version] of testedPackageManagers) {
for (const [name, version, expectedVersion = version.split(`+`, 1)[0]] of testedPackageManagers) {
it(`should use the right package manager version for a given project (${name}@${version})`, async () => {
await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, [`${name}@${version}`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
stdout: `${expectedVersion}\n`,
});

await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
packageManager: `${name}@${version}`,
});

await expect(runCli(cwd, [name, `--version`])).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
stdout: `${version.split(`+`, 1)[0]}\n`,
stdout: `${expectedVersion}\n`,
});
});
});
Expand Down

0 comments on commit 731a970

Please sign in to comment.