UNPKG

typesync

Version:

Install missing TypeScript typings for your dependencies.

346 lines (339 loc) 10.7 kB
import * as path$1 from "node:path"; import * as path from "node:path"; import { defaultLoaders, lilconfig } from "lilconfig"; import * as yaml from "yaml"; import fetch from "npm-registry-fetch"; import { compare, parse } from "semver"; //#region src/types.ts let IDependencySection = /* @__PURE__ */ 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] !== void 0) 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 async function loadYaml(_filepath, content) { return await yaml.parse(content); } function searchPlaces(moduleName) { const rcs = [ `.${moduleName}rc`, `.${moduleName}rc.json`, `.${moduleName}rc.yaml`, `.${moduleName}rc.yml`, `.${moduleName}rc.js`, `.${moduleName}rc.mjs`, `.${moduleName}rc.cjs` ]; return [ "package.json", ...rcs, ...rcs.map((rc) => `.config/${rc}`), `${moduleName}.config.js`, `${moduleName}.config.mjs`, `${moduleName}.config.cjs` ]; } const explorer = lilconfig("typesync", { searchPlaces: searchPlaces("typesync"), loaders: { ...defaultLoaders, ".yml": loadYaml, ".yaml": loadYaml, noExt: loadYaml } }); 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) : void 0; }; 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-D40qMMFq.js.map