typesync
Version:
Install missing TypeScript typings for your dependencies.
315 lines (308 loc) • 10 kB
JavaScript
import * as path$1 from "node:path";
import * as path from "node:path";
import { cosmiconfig } from "cosmiconfig";
import fetch from "npm-registry-fetch";
import { compare, parse } from "semver";
//#region src/types.ts
let IDependencySection = function(IDependencySection$1) {
IDependencySection$1["dev"] = "dev";
IDependencySection$1["deps"] = "deps";
IDependencySection$1["optional"] = "optional";
IDependencySection$1["peer"] = "peer";
return IDependencySection$1;
}({});
//#endregion
//#region src/util.ts
function uniq(source) {
return [...new Set(source)];
}
function shrinkObject(source) {
const object = {};
for (const key in source) if (source[key] !== undefined) object[key] = source[key];
return object;
}
function mergeObjects(source) {
return source.reduce((accum, next) => ({
...accum,
...next
}), {});
}
function typed(name) {
if (/^@.*?\//i.test(name)) {
const splat = name.split("/");
return `@types/${splat[0].slice(1)}__${splat[1]}`;
}
return `@types/${name}`;
}
function orderObject(source, comparer) {
const keys = Object.keys(source).sort(comparer);
const result = {};
for (const key of keys) result[key] = source[key];
return result;
}
function memoizeAsync(fn) {
const cache = new Map();
async function run(...args) {
try {
return await fn(...args);
} catch (err) {
cache.delete(args[0]);
throw err;
}
}
return async function(...args) {
const key = args[0];
if (cache.has(key)) return await cache.get(key);
const p = run(...args);
cache.set(key, p);
return await p;
};
}
//#endregion
//#region src/config-service.ts
const explorer = cosmiconfig("typesync");
function createConfigService() {
return { readConfig: async (filePath, flags) => {
const fileConfig = await explorer.search(path$1.dirname(filePath)).then((result) => result?.config ?? {});
const cliConfig = readCliConfig(flags);
return {
...shrinkObject(fileConfig),
...shrinkObject(cliConfig)
};
} };
}
function readCliConfig(flags) {
const readValues = (key, validator) => {
const values = flags[key];
return typeof values === "string" ? values.split(",").filter((value) => validator ? validator(value) : true) : undefined;
};
return {
ignoreDeps: readValues("ignoredeps", isIgnoreDepConfigValue),
ignorePackages: readValues("ignorepackages"),
ignoreProjects: readValues("ignoreprojects")
};
}
function isIgnoreDepConfigValue(value) {
return Object.keys(IDependencySection).includes(value);
}
//#endregion
//#region src/package-source.ts
function createPackageSource() {
return { fetch: async (name) => {
const response = await fetch(encodeURI(name)).catch((err) => {
if (err.statusCode === 404) return null;
throw err;
});
const data = await response?.json();
if (!data?.versions) return null;
const versionIdentifiers = Object.keys(data.versions).sort(compare).reverse();
const versions = versionIdentifiers.map((v) => {
const item = data.versions[v];
return {
version: item.version,
containsInternalTypings: !!item.types || !!item.typings
};
});
return {
name: data.name,
deprecated: Boolean(data.versions[versionIdentifiers[0]].deprecated),
latestVersion: data["dist-tags"].latest,
versions
};
} };
}
//#endregion
//#region src/versioning.ts
function getClosestMatchingVersion(availableVersions, version) {
const parsedVersion = parseVersion(version);
if (!parsedVersion) return availableVersions[0];
const bestMatch = availableVersions.find((v) => {
const parsedAvailableVersion = parseVersion(v.version);
if (!parsedAvailableVersion) return false;
if (parsedVersion.major !== parsedAvailableVersion.major) return false;
if (parsedVersion.minor !== parsedAvailableVersion.minor) return false;
return true;
});
return bestMatch ?? availableVersions[0];
}
/**
* Parses the version if possible.
*
* @param version
* @returns
*/
function parseVersion(version) {
return parse(cleanVersion(version));
}
/**
* Cleans the version of any semver range specifiers.
* @param version
* @returns
*/
function cleanVersion(version) {
return version.replace(/^[\^~=\s]/, "");
}
//#endregion
//#region src/type-syncer.ts
function createTypeSyncer(packageJSONService, workspaceResolverService, packageSource, configService, globber) {
const fetchPackageInfo = memoizeAsync(packageSource.fetch);
return { sync };
/**
* Syncs typings in the specified package.json.
*/
async function sync(filePath, flags) {
const dryRun = !!flags.dry;
const syncOpts = await configService.readConfig(filePath, flags);
const { file, subManifests } = await getManifests(filePath, globber, syncOpts.ignoreProjects ?? []);
const syncedFiles = await Promise.all([syncFile(filePath, file, syncOpts, dryRun), ...subManifests.map(async (p) => await syncFile(p, await packageJSONService.readPackageFile(p), syncOpts, dryRun))]);
return { syncedFiles };
}
/**
* Get the `package.json` files and sub-packages.
*
* @param filePath
* @param globber
*/
async function getManifests(filePath, globber$1, ignoredWorkspaces) {
const root = path.dirname(filePath);
const file = await packageJSONService.readPackageFile(filePath);
const subPackages = await workspaceResolverService.getWorkspaces(file, root, globber$1, ignoredWorkspaces);
const subManifests = subPackages.map((p) => path.join(root, p, "package.json"));
return {
file,
subManifests
};
}
/**
* Syncs a single file.
*
* @param filePath
* @param file
* @param allTypings
* @param opts
*/
async function syncFile(filePath, file, opts, dryRun) {
const { ignoreDeps, ignorePackages } = opts;
const allLocalPackages = Object.values(IDependencySection).map((dep) => {
const section = getDependenciesBySection(file, dep);
if (!section) return [];
const ignoredSection = ignoreDeps?.includes(dep);
return getPackagesFromSection(section, ignoredSection, ignorePackages);
}).flat();
const allPackageNames = uniq(allLocalPackages.map((p) => p.name));
const potentiallyUntypedPackages = getPotentiallyUntypedPackages(allPackageNames);
const used = [];
const devDepsToAdd = await Promise.all(potentiallyUntypedPackages.map(async (t) => {
const typePackageInfoPromise = fetchPackageInfo(t.typesPackageName);
const codePackageInfo = await fetchPackageInfo(t.codePackageName);
if (!codePackageInfo) return {};
const localCodePackage = allLocalPackages.find((p) => p.name === t.codePackageName);
const closestMatchingCodeVersion = getClosestMatchingVersion(codePackageInfo.versions, localCodePackage.version);
if (closestMatchingCodeVersion.containsInternalTypings) return {};
const typePackageInfo = await typePackageInfoPromise;
if (!typePackageInfo || typePackageInfo.deprecated) return {};
const closestMatchingTypingsVersion = getClosestMatchingVersion(typePackageInfo.versions, localCodePackage.version);
const version = closestMatchingTypingsVersion.version;
const semverRangeSpecifier = "~";
used.push(t);
return { [t.typesPackageName]: semverRangeSpecifier + version };
})).then(mergeObjects);
const devDeps = file.devDependencies;
if (!dryRun) {
const newPackageFile = { ...file };
if (Object.keys(devDepsToAdd).length > 0) {
newPackageFile.devDependencies = orderObject({
...devDepsToAdd,
...devDeps
});
await packageJSONService.writePackageFile(filePath, newPackageFile);
}
}
return {
filePath,
newTypings: used,
package: file
};
}
}
/**
* Returns an array of packages that do not have a `@types/` package.
*
* @param allPackageNames Used to filter the typings that are new.
* @param allTypings All typings available
*/
function getPotentiallyUntypedPackages(allPackageNames) {
const existingTypings = allPackageNames.filter((x) => x.startsWith("@types/"));
return allPackageNames.flatMap((p) => {
if (p.startsWith("@types/")) return [];
const typingsName = getTypingsName(p);
const fullTypingsPackage = typed(p);
const alreadyHasTyping = existingTypings.includes(fullTypingsPackage);
if (alreadyHasTyping) return [];
return [{
typingsName,
typesPackageName: fullTypingsPackage,
codePackageName: p
}];
});
}
/**
* Gets the typings name for the specified package name.
* For example, `koa` would be `koa`, but `@koa/router` would be `koa__router`.
*
* @param packageName the package name to generate the typings name for
*/
function getTypingsName(packageName) {
const scopeInfo = getPackageScope(packageName);
return scopeInfo && scopeInfo[0] !== "types" ? `${scopeInfo[0]}__${scopeInfo[1]}` : packageName;
}
/**
* If a package is scoped, returns the scope + package as a tuple, otherwise null.
*
* @param packageName Package name to check scope for.
*/
function getPackageScope(packageName) {
const EXPR = /^@([^/]+)\/(.*)$/i;
const matches = EXPR.exec(packageName);
if (!matches) return null;
return [matches[1], matches[2]];
}
/**
* Get packages from a dependency section
*
* @param section
* @param ignoredSection
* @param ignorePackages
*/
function getPackagesFromSection(section, ignoredSection, ignorePackages) {
return Object.keys(section).flatMap((name) => {
const isTyping = name.startsWith("@types/");
if (!isTyping) {
if (ignoredSection || ignorePackages?.includes(name)) return [];
}
return [{
name,
version: section[name]
}];
});
}
/**
* Get dependencies from a package section
*
* @param file Package file
* @param section Package section, eg: dev, peer
*/
function getDependenciesBySection(file, section) {
const dependenciesSection = (() => {
switch (section) {
case IDependencySection.deps: return file.dependencies;
case IDependencySection.dev: return file.devDependencies;
case IDependencySection.optional: return file.optionalDependencies;
case IDependencySection.peer: return file.peerDependencies;
}
})();
return dependenciesSection;
}
//#endregion
export { IDependencySection, createConfigService, createPackageSource, createTypeSyncer, uniq };
//# sourceMappingURL=type-syncer-D93oGCvC.js.map