diff --git a/.eslintignore b/.eslintignore index 9f2b3b0db0..3c29481021 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1743,6 +1743,7 @@ packages/plugin-repo-cli/lib/gitCompareUrl.test.js packages/plugin-repo-cli/lib/gitCompareUrl.js packages/plugin-repo-cli/lib/overrideUtils.test.js packages/plugin-repo-cli/lib/overrideUtils.js +packages/plugin-repo-cli/lib/searchPlugins.js packages/plugin-repo-cli/lib/types.js packages/plugin-repo-cli/lib/updateReadme.test.js packages/plugin-repo-cli/lib/updateReadme.js diff --git a/.gitignore b/.gitignore index f37e568af1..7aaa3526b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1716,6 +1716,7 @@ packages/plugin-repo-cli/lib/gitCompareUrl.test.js packages/plugin-repo-cli/lib/gitCompareUrl.js packages/plugin-repo-cli/lib/overrideUtils.test.js packages/plugin-repo-cli/lib/overrideUtils.js +packages/plugin-repo-cli/lib/searchPlugins.js packages/plugin-repo-cli/lib/types.js packages/plugin-repo-cli/lib/updateReadme.test.js packages/plugin-repo-cli/lib/updateReadme.js diff --git a/packages/plugin-repo-cli/index.ts b/packages/plugin-repo-cli/index.ts index 825c5376ef..5da36f2c66 100644 --- a/packages/plugin-repo-cli/index.ts +++ b/packages/plugin-repo-cli/index.ts @@ -16,9 +16,9 @@ import { isJoplinPluginPackage, readJsonFile } from './lib/utils'; import { applyManifestOverrides, getObsoleteManifests, getSupersededPackages, readManifestOverrides } from './lib/overrideUtils'; import { execCommand } from '@joplin/utils'; import validateUntrustedManifest from './lib/validateUntrustedManifest'; +import searchPlugins, { PackageInfo } from './lib/searchPlugins'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied -function pluginInfoFromSearchResults(results: any[]): NpmPackage[] { +function pluginInfoFromSearchResults(results: PackageInfo[]): NpmPackage[] { const output: NpmPackage[] = []; for (const r of results) { @@ -245,8 +245,7 @@ async function commandBuild(args: CommandBuildArgs) { chdir(previousDir); - const searchResults = (await execCommand('npm search keywords:joplin-plugin --searchlimit 5000 --json', { showStdout: false, showStderr: false })).trim(); - const npmPackages = pluginInfoFromSearchResults(JSON.parse(searchResults)); + const npmPackages = pluginInfoFromSearchResults(await searchPlugins()); for (const npmPackage of npmPackages) { await processNpmPackage(npmPackage, repoDir, dryRun); diff --git a/packages/plugin-repo-cli/lib/searchPlugins.ts b/packages/plugin-repo-cli/lib/searchPlugins.ts new file mode 100644 index 0000000000..ed5a9f714a --- /dev/null +++ b/packages/plugin-repo-cli/lib/searchPlugins.ts @@ -0,0 +1,79 @@ +import { URLSearchParams } from 'node:url'; + +export interface PackageInfo { + name: string; + version: string; + date: string; + keywords: string[]; +} + +interface SearchResult { + package: PackageInfo; +} + +interface SearchResponse { + objects: SearchResult[]; + total: number; +} + +function validateResponse(response: unknown): asserts response is SearchResponse { + if (typeof response !== 'object') throw new Error('Invalid response (must be an object)'); + if (!('total' in response) || typeof response.total !== 'number') { + throw new Error('Invalid response.total'); + } + if (!('objects' in response) || !Array.isArray(response.objects)) { + throw new Error('response.objects must be an array'); + } + for (const object of response.objects) { + if (!('package' in object)) throw new Error('Missing "package" field'); + const packageField = object.package; + if (typeof packageField.name !== 'string') throw new Error('package.name: Invalid type'); + if (typeof packageField.version !== 'string') throw new Error('package.version: Invalid type'); + if (typeof packageField.date !== 'string') throw new Error('package.date: Invalid type'); + if (!Array.isArray(packageField.keywords)) throw new Error('package.keywords: Invalid type'); + } +} + +const searchPlugins = async () => { + const pageSize = 100; + const makeRequest = async (start: number): Promise => { + const urlParams = new URLSearchParams({ + text: 'keywords:joplin-plugin', + size: String(pageSize), + from: String(start), + }); + // API documentation: https://github.com/npm/registry/blob/main/docs/REGISTRY-API.md#get-v1search + const query = `https://registry.npmjs.org/-/v1/search?${urlParams.toString()}`; + const result = await fetch(query); + const json: unknown = await result.json(); + validateResponse(json); + return json; + }; + + const packageInfos: PackageInfo[] = []; + const addPackageInfos = (response: SearchResponse) => { + for (const object of response.objects) { + packageInfos.push(object.package); + } + }; + + const firstResponse = await makeRequest(0); + let total = firstResponse.total; + addPackageInfos(firstResponse); + + for (let page = 1; packageInfos.length < total; page++) { + // Cap the maximum number of requests: Fail early in the case where the search query breaks + // in the future. + if (page >= 100) { + throw new Error('More requests sent than expected. Does maximumRequests need to be increased?'); + } + + const response = await makeRequest(page * pageSize); + total = response.total; + addPackageInfos(response); + } + + return packageInfos; +}; + +export default searchPlugins;