mirror of
https://github.com/zhigang1992/yarn.git
synced 2026-06-14 10:19:15 +08:00
* Adds the pkg-tests testsuite to the repository * Fixes flow * Adds babel to strip flowtypes * Moves pkg-tests into the packages directory * Fixes server timeouts * Feedback
338 lines
9.3 KiB
JavaScript
338 lines
9.3 KiB
JavaScript
/* @flow */
|
|
|
|
import type {ServerResponse} from 'http';
|
|
import type {Gzip} from 'zlib';
|
|
|
|
const crypto = require('crypto');
|
|
const deepResolve = require('super-resolve');
|
|
const http = require('http');
|
|
const invariant = require('invariant');
|
|
const semver = require('semver');
|
|
|
|
const fsUtils = require('./fs');
|
|
|
|
export type PackageEntry = Map<string, {|path: string, packageJson: Object|}>;
|
|
export type PackageRegistry = Map<string, PackageEntry>;
|
|
|
|
export type PackageRunDriver = (
|
|
string,
|
|
Array<string>,
|
|
{registryUrl: string},
|
|
) => Promise<{|stdout: Buffer, stderr: Buffer|}>;
|
|
|
|
export type PackageDriver = any;
|
|
|
|
exports.getPackageRegistry = function getPackageRegistry(): Promise<PackageRegistry> {
|
|
if (getPackageRegistry.promise) {
|
|
return getPackageRegistry.promise;
|
|
}
|
|
|
|
return (getPackageRegistry.promise = (async () => {
|
|
const packageRegistry = new Map();
|
|
for (const packageFile of await fsUtils.walk(`${require('pkg-tests-fixtures')}/packages`, {
|
|
filter: ['package.json'],
|
|
})) {
|
|
const packageJson = await fsUtils.readJson(packageFile);
|
|
const {name, version} = packageJson;
|
|
|
|
if (name.startsWith('git-')) {
|
|
continue;
|
|
}
|
|
|
|
let packageEntry = packageRegistry.get(name);
|
|
|
|
if (!packageEntry) {
|
|
packageRegistry.set(name, (packageEntry = new Map()));
|
|
}
|
|
|
|
packageEntry.set(version, {
|
|
path: require('path').dirname(packageFile),
|
|
packageJson,
|
|
});
|
|
}
|
|
|
|
return packageRegistry;
|
|
})());
|
|
};
|
|
|
|
exports.getPackageEntry = async function getPackageEntry(name: string): Promise<?PackageEntry> {
|
|
const packageRegistry = await exports.getPackageRegistry();
|
|
|
|
return packageRegistry.get(name);
|
|
};
|
|
|
|
exports.getPackageArchiveStream = async function getPackageArchiveStream(name: string, version: string): Promise<Gzip> {
|
|
const packageEntry = await exports.getPackageEntry(name);
|
|
|
|
if (!packageEntry) {
|
|
throw new Error(`Unknown package "${name}"`);
|
|
}
|
|
|
|
const packageVersionEntry = packageEntry.get(version);
|
|
|
|
if (!packageVersionEntry) {
|
|
throw new Error(`Unknown version "${version}" for package "${name}"`);
|
|
}
|
|
|
|
return fsUtils.packToStream(packageVersionEntry.path, {
|
|
virtualPath: '/package',
|
|
});
|
|
};
|
|
|
|
exports.getPackageArchivePath = async function getPackageArchivePath(name: string, version: string): Promise<string> {
|
|
const packageEntry = await exports.getPackageEntry(name);
|
|
|
|
if (!packageEntry) {
|
|
throw new Error(`Unknown package "${name}"`);
|
|
}
|
|
|
|
const packageVersionEntry = packageEntry.get(version);
|
|
|
|
if (!packageVersionEntry) {
|
|
throw new Error(`Unknown version "${version}" for package "${name}"`);
|
|
}
|
|
|
|
const archivePath = await fsUtils.createTemporaryFile(`${name}-${version}.tar.gz`);
|
|
|
|
await fsUtils.packToFile(archivePath, packageVersionEntry.path, {
|
|
virtualPath: '/package',
|
|
});
|
|
|
|
return archivePath;
|
|
};
|
|
|
|
exports.getPackageArchiveHash = async function getPackageArchiveHash(
|
|
name: string,
|
|
version: string,
|
|
): Promise<string | Buffer> {
|
|
const stream = await exports.getPackageArchiveStream(name, version);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const hash = crypto.createHash('sha1');
|
|
hash.setEncoding('hex');
|
|
|
|
// Send the archive to the hash function
|
|
stream.pipe(hash);
|
|
|
|
stream.on('end', () => {
|
|
const finalHash = hash.read();
|
|
invariant(finalHash, 'The hash should have been computated');
|
|
resolve(finalHash);
|
|
});
|
|
});
|
|
};
|
|
|
|
exports.getPackageHttpArchivePath = async function getPackageHttpArchivePath(
|
|
name: string,
|
|
version: string,
|
|
): Promise<string> {
|
|
const packageEntry = await exports.getPackageEntry(name);
|
|
|
|
if (!packageEntry) {
|
|
throw new Error(`Unknown package "${name}"`);
|
|
}
|
|
|
|
const packageVersionEntry = packageEntry.get(version);
|
|
|
|
if (!packageVersionEntry) {
|
|
throw new Error(`Unknown version "${version}" for package "${name}"`);
|
|
}
|
|
|
|
const serverUrl = await exports.startPackageServer();
|
|
const archiveUrl = `${serverUrl}/${name}/-/${name}-${version}.tgz`;
|
|
|
|
return archiveUrl;
|
|
};
|
|
|
|
exports.getPackageDirectoryPath = async function getPackageDirectoryPath(
|
|
name: string,
|
|
version: string,
|
|
): Promise<string> {
|
|
const packageEntry = await exports.getPackageEntry(name);
|
|
|
|
if (!packageEntry) {
|
|
throw new Error(`Unknown package "${name}"`);
|
|
}
|
|
|
|
const packageVersionEntry = packageEntry.get(version);
|
|
|
|
if (!packageVersionEntry) {
|
|
throw new Error(`Unknown version "${version}" for package "${name}"`);
|
|
}
|
|
|
|
return packageVersionEntry.path;
|
|
};
|
|
|
|
exports.startPackageServer = function startPackageServer(): Promise<string> {
|
|
if (startPackageServer.url) {
|
|
return startPackageServer.url;
|
|
}
|
|
|
|
async function processPackageInfo(params: ?Array<string>, res: ServerResponse): Promise<boolean> {
|
|
if (!params) {
|
|
return false;
|
|
}
|
|
|
|
const [, scope, localName] = params;
|
|
const name = scope ? `${scope}/${localName}` : localName;
|
|
|
|
const packageEntry = await exports.getPackageEntry(name);
|
|
|
|
if (!packageEntry) {
|
|
return processError(res, 404, `Package not found: ${name}`);
|
|
}
|
|
|
|
const versions = Array.from(packageEntry.keys());
|
|
|
|
const data = JSON.stringify({
|
|
name,
|
|
versions: Object.assign(
|
|
{},
|
|
...(await Promise.all(
|
|
versions.map(async version => {
|
|
const packageVersionEntry = packageEntry.get(version);
|
|
invariant(packageVersionEntry, 'This can only exist');
|
|
|
|
return {
|
|
[version]: Object.assign({}, packageVersionEntry.packageJson, {
|
|
dist: {
|
|
shasum: await exports.getPackageArchiveHash(name, version),
|
|
tarball: await exports.getPackageHttpArchivePath(name, version),
|
|
},
|
|
}),
|
|
};
|
|
}),
|
|
)),
|
|
),
|
|
['dist-tags']: {latest: semver.maxSatisfying(versions, '*')},
|
|
});
|
|
|
|
res.writeHead(200, {['Content-Type']: 'application/json'});
|
|
res.end(data);
|
|
|
|
return true;
|
|
}
|
|
|
|
async function processPackageTarball(params: ?Array<string>, res: ServerResponse): Promise<boolean> {
|
|
if (!params) {
|
|
return false;
|
|
}
|
|
|
|
const [, scope, localName, version] = params;
|
|
const name = scope ? `${scope}/${localName}` : localName;
|
|
|
|
const packageEntry = await exports.getPackageEntry(name);
|
|
|
|
if (!packageEntry) {
|
|
return processError(res, 404, `Package not found: ${name}`);
|
|
}
|
|
|
|
const packageVersionEntry = packageEntry.get(version);
|
|
|
|
if (!packageVersionEntry) {
|
|
return processError(res, 404, `Package not found: ${name}@${version}`);
|
|
}
|
|
|
|
res.writeHead(200, {
|
|
['Content-Type']: 'application/octet-stream',
|
|
['Transfer-Encoding']: 'chunked',
|
|
});
|
|
|
|
const packStream = fsUtils.packToStream(packageVersionEntry.path, {virtualPath: '/package'});
|
|
packStream.pipe(res);
|
|
|
|
return true;
|
|
}
|
|
|
|
function processError(res: ServerResponse, statusCode: number, errorMessage: string): boolean {
|
|
console.error(errorMessage);
|
|
|
|
res.writeHead(statusCode);
|
|
res.end(errorMessage);
|
|
|
|
return true;
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const server = http.createServer(
|
|
(req, res) =>
|
|
void (async () => {
|
|
try {
|
|
if (await processPackageInfo(req.url.match(/^\/(?:(@[^\/]+)\/)?([^@\/][^\/]*)$/), res)) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
await processPackageTarball(req.url.match(/^\/(?:(@[^\/]+)\/)?([^@\/][^\/]*)\/-\/\2-(.*)\.tgz$/), res)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
processError(res, 404, `Invalid route: ${req.url}`);
|
|
} catch (error) {
|
|
processError(res, 500, error.stack);
|
|
}
|
|
})(),
|
|
);
|
|
|
|
// We don't want the server to prevent the process from exiting
|
|
server.unref();
|
|
server.listen(() => {
|
|
const {port} = server.address();
|
|
resolve((startPackageServer.url = `http://localhost:${port}`));
|
|
});
|
|
});
|
|
};
|
|
|
|
exports.generatePkgDriver = function generatePkgDriver({runDriver}: {|runDriver: PackageRunDriver|}): PackageDriver {
|
|
function withConfig(definition): PackageDriver {
|
|
const makeTemporaryEnv = (packageJson, subDefinition, fn) => {
|
|
if (typeof subDefinition === 'function') {
|
|
fn = subDefinition;
|
|
subDefinition = {};
|
|
}
|
|
|
|
if (typeof fn !== 'function') {
|
|
throw new Error(
|
|
// eslint-disable-next-line
|
|
`Invalid test function (got ${typeof fn}) - you probably put the closing parenthesis of the "makeTemporaryEnv" utility at the wrong place`,
|
|
);
|
|
}
|
|
|
|
return async function(): Promise<void> {
|
|
const path = await fsUtils.createTemporaryFolder();
|
|
|
|
const registryUrl = await exports.startPackageServer();
|
|
|
|
// Writes a new package.json file into our temporary directory
|
|
await fsUtils.writeJson(`${path}/package.json`, await deepResolve(packageJson));
|
|
|
|
const run = (...args) => {
|
|
return runDriver(path, args, {
|
|
registryUrl,
|
|
...subDefinition,
|
|
});
|
|
};
|
|
|
|
const source = async script => {
|
|
return JSON.parse((await run('node', '-p', `JSON.stringify(${script})`)).stdout.toString());
|
|
};
|
|
|
|
await fn({
|
|
path,
|
|
run,
|
|
source,
|
|
});
|
|
};
|
|
};
|
|
|
|
makeTemporaryEnv.withConfig = subDefinition => {
|
|
return withConfig({...definition, ...subDefinition});
|
|
};
|
|
|
|
return makeTemporaryEnv;
|
|
}
|
|
|
|
return withConfig({});
|
|
};
|