diff --git a/README.md b/README.md index e8a9fdb4..a7502a97 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ This action deletes versions of a package from [GitHub Packages](https://github. * Delete all package versions except n most recent versions * Delete oldest version(s) * Ignore version(s) from deletion through regex -* Delete version(s) of a package that is hosted from a repo having access to package -* Delete version(s) of a package that is hosted from a repo not having access to package +* Delete version(s) of packages that are hosted in the same repo that is executing the workflow +* Delete version(s) of packages that are hosted in a different repo than the one executing the workflow * Delete a single version * Delete multiple versions * Delete specific version(s) @@ -28,13 +28,24 @@ This action deletes versions of a package from [GitHub Packages](https://github. # Required if deleting a version from a package hosted in a different org than the one executing the workflow. owner: - # Name of the package. - # Required - package-name: - # Type of the package. Can be one of container, maven, npm, nuget, or rubygems. # Required package-type: + + # Defaults to an empty string. + # Required if `package-version-ids` or `package-names` input is not given. + package-name: + + # Names of the package. + # Can be one of the following: + # - a single package name + # - a group of packages that matches a wildcard at start, end, both sides, or all packages (e.g. "package*") + # - a group of packages that matches a regex, must start with slash at the beginning and end (e.g. "/package.*/") + # - a comma separated list of the previous cases (e.g. "package-lorem, *-ipsum, /.*dolor/") + # Defaults to an empty string. + # Required if `package-version-ids` or `package-name` input is not given. + package-names: + # The number of old versions to delete starting from the oldest version. # Defaults to 1. @@ -75,7 +86,7 @@ This action deletes versions of a package from [GitHub Packages](https://github. # Valid Input Combinations -`owner`, `package-name`, `package-type` and `token` can be used with the following combinations in a workflow - +`owner`, `repo`, `package-name` (or `package-names`) and `token` can be used with the following combinations in a workflow - - `num-old-versions-to-delete` - `min-versions-to-keep` @@ -104,6 +115,7 @@ This action deletes versions of a package from [GitHub Packages](https://github. - [License](#license) + ### Delete all pre-release versions except y latest pre-release package versions To delete all pre release versions except y latest pre-release package versions, the __package-name__, __min-versions-to-keep__ and __delete-only-pre-release-versions__ inputs are required. @@ -272,6 +284,41 @@ This action deletes versions of a package from [GitHub Packages](https://github.
+ ### Delete all except y latest versions of a package + + To delete all except y latest versions of all packages hosted in the same repo as the workflow the __package-names__ and __min-versions-to-keep__ inputs are required. + + __Example__ + + Delete all except latest 2 versions of a package hosted in the same repo as the workflow + + ```yaml + - uses: actions/delete-package-versions@v3 + with: + package-names: '*' + min-versions-to-keep: 2 + ``` + + To delete all except y latest versions of all packages hosted in a repo other than the workflow the __owner__, __repo__, __package-names__, __token__ and __min-versions-to-keep__ inputs are required. + + The [token][token] needs the delete packages and read packages scope. It is recommended [to store the token as a secret][secret]. In this example the [token][token] was stored as a secret named __GITHUB_PAT__. + + __Example__ + + Delete all except latest 2 versions of a package hosted in a repo other than the workflow + + ```yaml + - uses: actions/delete-package-versions@v3 + with: + owner: 'github' + repo: 'packages' + package-names: '*' + token: ${{ secrets.PAT }} + min-versions-to-keep: 2 + ``` + +
+ ### Delete oldest x number of versions of a package To delete the oldest x number of versions of a package hosted, the __package-name__, and __num-old-versions-to-delete__ inputs are required. diff --git a/__tests__/packages/get-packages.test.ts b/__tests__/packages/get-packages.test.ts new file mode 100644 index 00000000..27672257 --- /dev/null +++ b/__tests__/packages/get-packages.test.ts @@ -0,0 +1,66 @@ +import {mockPackagesQueryResponse} from './graphql.mock' +import { + getRepoPackages as _getRepoPackages, + QueryInfo +} from '../../src/packages' +import {Observable} from 'rxjs' + +describe.skip('get versions tests -- call graphql', () => { + it('getRepoPackages -- succeeds', done => { + const numPackages = 1 + getRepoPackages({numPackages}).subscribe(result => { + expect(result.packages.length).toBe(numPackages) + done() + }) + }) + + it('getRepoPackages -- fails for invalid repo', done => { + getRepoPackages({repo: 'actions-testin'}).subscribe({ + error: err => { + expect(err).toBeTruthy() + done() + }, + complete: async () => done.fail('no error thrown') + }) + }) + }) + + describe('get versions tests -- mock graphql', () => { + it('getRepoPackages -- success', done => { + const numPackages = 5 + mockPackagesQueryResponse(numPackages) + + getRepoPackages({numPackages}).subscribe(result => { + expect(result.packages.length).toBe(numPackages) + done() + }) + }) + }) + + interface Params { + owner?: string + repo?: string + numPackages?: number + startCursor?: string + token?: string + } + + const defaultParams = { + owner: 'namratajha', + repo: 'test-repo', + packageName: 'test-repo', + numPackages: 1, + startCursor: '', + token: process.env.GITHUB_TOKEN as string + } + + function getRepoPackages(params?: Params): Observable { + const p: Required = {...defaultParams, ...params} + return _getRepoPackages( + p.owner, + p.repo, + p.numPackages, + p.startCursor, + p.token + ) + } \ No newline at end of file diff --git a/__tests__/packages/graphql.mock.ts b/__tests__/packages/graphql.mock.ts new file mode 100644 index 00000000..a6601488 --- /dev/null +++ b/__tests__/packages/graphql.mock.ts @@ -0,0 +1,44 @@ +import { + GraphQlQueryResponseData, + RequestParameters +} from '@octokit/graphql/dist-types/types' + +import * as Graphql from '../../src/common/graphql' +import {GetPackagesQueryResponse} from '../../src/packages' + +export function getMockedPackagesQueryResponse( + numPackages: number +): GetPackagesQueryResponse { + const packages: any[] = [] + for (let i = 1; i <= numPackages; ++i) { + packages.push({ + node: { + id: i.toString(), + name: `package${i}` + } + }) + } + + return { + repository: { + packages: { + pageInfo: { + endCursor: 'AAA', + hasNextPage: false + }, + edges: packages + } + } + } +} + +export function mockPackagesQueryResponse(numVersions: number): void { + const response = new Promise(resolve => { + resolve(getMockedPackagesQueryResponse(numVersions)) + }) as Promise + jest + .spyOn(Graphql, 'graphql') + .mockImplementation( + (token: string, query: string, parameters: RequestParameters) => response + ) +} diff --git a/__tests__/packages/package-name-filter.test.ts b/__tests__/packages/package-name-filter.test.ts new file mode 100644 index 00000000..781d7c74 --- /dev/null +++ b/__tests__/packages/package-name-filter.test.ts @@ -0,0 +1,114 @@ +import { getPackageNameFilter } from '../../src/packages' + +describe('package name filter -- create filter', () => { + + const packageNameList = [ + 'com.company.project.module1.package1', + 'com.company.project.module1.package2', + 'com.company.project.module2.package1', + 'com.company.project.module2.package2', + 'com.company.project.module3.package-name-lorem', + 'com.company.project.module3.package-name-ipsum', + 'com.company.project.module3.package-name-dolor', + ] + + it('getPackageNameFilter -- wildcard end filter', done => { + const filter = getPackageNameFilter('com.company.project.module1.*') + + const result = packageNameList.filter(filter.apply); + + expect(filter.subfilters[0].type).toBe('wildcard') + expect(result).toEqual([ + 'com.company.project.module1.package1', + 'com.company.project.module1.package2', + ]) + done() + }) + + it('getPackageNameFilter -- wildcard start filter', done => { + const filter = getPackageNameFilter('*.package1') + + const result = packageNameList.filter(filter.apply); + + expect(filter.subfilters[0].type).toBe('wildcard') + expect(result).toEqual([ + 'com.company.project.module1.package1', + 'com.company.project.module2.package1', + ]) + done() + }) + + it('getPackageNameFilter -- wildcard both sides filter', done => { + const filter = getPackageNameFilter('*.project.module3.*') + + const result = packageNameList.filter(filter.apply); + + expect(filter.subfilters[0].type).toBe('wildcard') + expect(result).toEqual([ + 'com.company.project.module3.package-name-lorem', + 'com.company.project.module3.package-name-ipsum', + 'com.company.project.module3.package-name-dolor' + ]) + done() + }) + + + it('getPackageNameFilter -- wildcard all filter', done => { + const filter = getPackageNameFilter('*') + + const result = packageNameList.filter(filter.apply); + + expect(filter.subfilters[0].type).toBe('wildcard') + expect(result).toEqual(packageNameList.slice()) + done() + }) + + it('getPackageNameFilter -- regex filter', done => { + const filter = getPackageNameFilter('/com\\.company\\.project\\.module.*\\.package1/') + const result = packageNameList.filter(filter.apply); + + expect(filter.subfilters[0].type).toBe('regex') + expect(result).toEqual([ + 'com.company.project.module1.package1', + 'com.company.project.module2.package1', + ]) + done() + }) + + it('getPackageNameFilter -- exact match filter', done => { + const filter = getPackageNameFilter('com.company.project.module1.package1') + const result = packageNameList.filter(filter.apply); + + expect(filter.subfilters[0].type).toBe('string') + expect(result).toEqual(['com.company.project.module1.package1']) + done() + }) + + + it('getPackageNameFilter -- multiple filters', done => { + const filter = getPackageNameFilter('com.company.project.module1.package1, com.company.project.module2.*, /.*module3.*-ipsum/') + const result = packageNameList.filter(filter.apply); + + expect(filter.subfilters.length).toBe(3) + expect(filter.subfilters[0].type).toBe('string') + expect(filter.subfilters[1].type).toBe('wildcard') + expect(filter.subfilters[2].type).toBe('regex') + expect(result).toEqual([ + 'com.company.project.module1.package1', + 'com.company.project.module2.package1', + 'com.company.project.module2.package2', + 'com.company.project.module3.package-name-ipsum' + ]) + done() + }) + + + it('getPackageNameFilter -- memoization, same input shoud return same output', done => { + const filterText = 'com.company.project.module1.package1, com.company.project.module2.*, /.*module3.*-dolor/, *.-lorem' + const filter1 = getPackageNameFilter(filterText) + const filter2 = getPackageNameFilter(filterText) + expect(filter1).toBe(filter2) + expect(filter1.subfilters.length).toBe(4) + done() + }) + }) \ No newline at end of file diff --git a/action.yml b/action.yml index 1bf23e28..5db046f3 100644 --- a/action.yml +++ b/action.yml @@ -25,6 +25,19 @@ inputs: Type of package. Can be one of container, maven, npm, nuget, or rubygems. required: true + + package-names: + description: > + Names of the package. + Can be one of the following: + - a single package name + - a group of packages that matches a wildcard at start, end, both sides, or all packages (e.g. "package*") + - a group of packages that matches a regex, must start with slash at the beginning and end (e.g. "/package.*/") + - a comma separated list of the previous cases (e.g. "package-lorem, *-ipsum, /.*dolor/") + Defaults to an empty string. + Required if `package-version-ids` or `package-name` input is not given. + required: false + num-old-versions-to-delete: description: > Number of versions to delete starting with the oldest version. diff --git a/dist/index.js b/dist/index.js index 89172a5a..270fd6d3 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,6 +1,41 @@ /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ +/***/ 9057: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.graphql = void 0; +const github_1 = __nccwpck_require__(5438); +/** + * Sends a GraphQL query request based on endpoint options + * + * @param {string} token Auth token + * @param {string} query GraphQL query. Example: `'query { viewer { login } }'`. + * @param {object} parameters URL, query or body parameters, as well as `headers`, `mediaType.{format|previews}`, `request`, or `baseUrl`. + */ +function graphql(token, query, parameters) { + return __awaiter(this, void 0, void 0, function* () { + const github = new github_1.GitHub(token); + return yield github.graphql(query, parameters); + }); +} +exports.graphql = graphql; + + +/***/ }), + /***/ 9645: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { @@ -8,12 +43,20 @@ /* eslint-disable i18n-text/no-en */ Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.deleteVersions = exports.finalIds = exports.getVersionIds = exports.RATE_LIMIT = void 0; +exports.deleteVersions = exports.finalIds = exports.getVersionIds = exports.getPackageNames = exports.RATE_LIMIT = void 0; +const input_1 = __nccwpck_require__(8657); const rxjs_1 = __nccwpck_require__(5805); const operators_1 = __nccwpck_require__(7801); const version_1 = __nccwpck_require__(4428); +const packages_1 = __nccwpck_require__(221); exports.RATE_LIMIT = 100; let totalCount = 0; +function getPackageNames(owner, repo, numPackages, cursor, token) { + return (0, packages_1.getRepoPackages)(owner, repo, numPackages, cursor, token).pipe((0, operators_1.expand)(value => value.paginate + ? (0, packages_1.getRepoPackages)(owner, repo, numPackages, value.cursor, token) + : rxjs_1.EMPTY), (0, operators_1.map)(value => value.packages)); +} +exports.getPackageNames = getPackageNames; function getVersionIds(owner, packageName, packageType, numVersions, page, token) { return (0, version_1.getOldestVersions)(owner, packageName, packageType, numVersions, page, token).pipe((0, operators_1.expand)(value => value.paginate ? (0, version_1.getOldestVersions)(owner, packageName, packageType, numVersions, value.page + 1, token) @@ -23,37 +66,56 @@ exports.getVersionIds = getVersionIds; function finalIds(input) { if (input.packageVersionIds.length > 0) { const toDelete = Math.min(input.packageVersionIds.length, exports.RATE_LIMIT); - return (0, rxjs_1.of)(input.packageVersionIds.slice(0, toDelete)); - } - if (input.hasOldestVersionQueryInfo()) { - return getVersionIds(input.owner, input.packageName, input.packageType, exports.RATE_LIMIT, 1, input.token).pipe( - // This code block executes on all versions of a package starting from oldest - (0, operators_1.map)(value => { - // we need to delete oldest versions first - value.sort((a, b) => { - return (new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); - }); - /* - Here first filter out the versions that are to be ignored. - Then compute number of versions to delete (toDelete) based on the inputs. - */ - value = value.filter(info => !input.ignoreVersions.test(info.version)); - if (input.deleteUntaggedVersions === 'true') { - value = value.filter(info => !info.tagged); - } - let toDelete = 0; - if (input.minVersionsToKeep < 0) { - toDelete = Math.min(value.length, Math.min(input.numOldVersionsToDelete, exports.RATE_LIMIT)); - } - else { - toDelete = Math.min(value.length - input.minVersionsToKeep, exports.RATE_LIMIT); - } - if (toDelete < 0) - return []; - return value.map(info => info.id.toString()).slice(0, toDelete); - })); + return (0, rxjs_1.of)({ + versions: input.packageVersionIds.slice(0, toDelete), + name: input.packageName + }); } - return (0, rxjs_1.throwError)("Could not get packageVersionIds. Explicitly specify using the 'package-version-ids' input"); + if (!input.hasOldestVersionQueryInfo()) { + return (0, rxjs_1.throwError)("Could not get packageVersionIds. Explicitly specify using the 'package-version-ids' input"); + } + const filter = (0, packages_1.getPackageNameFilter)(input.packageNames); + if (!filter.isEmpty) { + return getPackageNames(input.owner, input.repo, exports.RATE_LIMIT, '', input.token) + .pipe((0, operators_1.mergeMap)(value => { + return value + .filter(info => filter.apply(info.name)) + .map(info => finalIds(new input_1.Input(Object.assign(Object.assign({}, input), { packageNames: '', packageName: info.name })))); + })) + .pipe((0, operators_1.mergeMap)(val => val)); + } + const versions = getVersionIds(input.owner, input.packageName, input.packageType, exports.RATE_LIMIT, 1, input.token).pipe( + // This code block executes on all versions of a package starting from oldest + (0, operators_1.map)(value => { + // we need to delete oldest versions first + value.sort((a, b) => { + return (new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); + }); + /* + Here first filter out the versions that are to be ignored. + Then compute number of versions to delete (toDelete) based on the inputs. + */ + value = value.filter(info => !input.ignoreVersions.test(info.version)); + if (input.deleteUntaggedVersions === 'true') { + value = value.filter(info => !info.tagged); + } + let toDelete = 0; + if (input.minVersionsToKeep < 0) { + toDelete = Math.min(value.length, Math.min(input.numOldVersionsToDelete, exports.RATE_LIMIT)); + } + else { + toDelete = Math.min(value.length - input.minVersionsToKeep, exports.RATE_LIMIT); + } + if (toDelete < 0) + return []; + return value.map(info => info.id.toString()).slice(0, toDelete); + })); + return versions.pipe((0, operators_1.map)(data => { + return { + versions: data, + name: input.packageName + }; + })); } exports.finalIds = finalIds; function deleteVersions(input) { @@ -68,7 +130,10 @@ function deleteVersions(input) { return (0, rxjs_1.of)(true); } const result = finalIds(input); - return result.pipe((0, operators_1.concatMap)(ids => (0, version_1.deletePackageVersions)(ids, input.owner, input.packageName, input.packageType, input.token))); + return result.pipe((0, operators_1.concatMap)(data => { + console.log(`clearing ${data.versions.length} versions from ${data.name}`); + return (0, version_1.deletePackageVersions)(data.versions, input.owner, data.name, input.packageType, input.token); + })); } exports.deleteVersions = deleteVersions; @@ -85,8 +150,10 @@ exports.Input = void 0; const defaultParams = { packageVersionIds: [], owner: '', + repo: '', packageName: '', packageType: '', + packageNames: '', numOldVersionsToDelete: 0, minVersionsToKeep: 0, ignoreVersions: new RegExp(''), @@ -99,8 +166,10 @@ class Input { const validatedParams = Object.assign(Object.assign({}, defaultParams), params); this.packageVersionIds = validatedParams.packageVersionIds; this.owner = validatedParams.owner; + this.repo = validatedParams.repo; this.packageName = validatedParams.packageName; this.packageType = validatedParams.packageType; + this.packageNames = validatedParams.packageNames; this.numOldVersionsToDelete = validatedParams.numOldVersionsToDelete; this.minVersionsToKeep = validatedParams.minVersionsToKeep; this.ignoreVersions = validatedParams.ignoreVersions; @@ -111,7 +180,8 @@ class Input { } hasOldestVersionQueryInfo() { return !!(this.owner && - this.packageName && + this.repo && + (this.packageName || this.packageNames) && this.numOldVersionsToDelete >= 0 && this.token); } @@ -120,7 +190,8 @@ class Input { (this.minVersionsToKeep >= 0 || this.deletePreReleaseVersions === 'true')) { return false; } - if (this.packageType === '' || this.packageName === '') { + if (this.packageType === '' || + (this.packageName === '' && this.packageNames === '')) { return false; } if (this.deletePreReleaseVersions === 'true') { @@ -140,6 +211,250 @@ class Input { exports.Input = Input; +/***/ }), + +/***/ 6436: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getRepoPackages = exports.queryForRepoPackages = void 0; +const rxjs_1 = __nccwpck_require__(5805); +const operators_1 = __nccwpck_require__(7801); +const graphql_1 = __nccwpck_require__(9057); +const query = ` + query getPackages($owner: String!, $repo: String!, $first: Int!){ + repository(owner: $owner, name: $repo) { + packages(first:$first){ + edges { + node { + name + id + } + } + pageInfo { + endCursor + hasNextPage + } + } + } + }`; +const Paginatequery = ` + query getPackages($owner: String!, $repo: String!, $first: Int!, $after: String!){ + repository(owner: $owner, name: $repo) { + packages(first:$first){ + edges { + node { + name + id + } + } + pageInfo { + endCursor + hasNextPage + } + } + } + }`; +function queryForRepoPackages(owner, repo, numPackages, startCursor, token) { + if (startCursor === '') { + return (0, rxjs_1.from)((0, graphql_1.graphql)(token, query, { + owner, + repo, + first: numPackages, + headers: { + Accept: 'application/vnd.github.packages-preview+json' + } + })).pipe((0, operators_1.catchError)((err) => { + const msg = 'query for packages failed.'; + return (0, rxjs_1.throwError)(err.errors && err.errors.length > 0 + ? `${msg} ${err.errors[0].message}` + : `${msg} verify input parameters are correct ${JSON.stringify(err, null, 2)}`); + })); + } + else { + return (0, rxjs_1.from)((0, graphql_1.graphql)(token, Paginatequery, { + owner, + repo, + first: numPackages, + before: startCursor, + after: '', + headers: { + Accept: 'application/vnd.github.packages-preview+json' + } + })).pipe((0, operators_1.catchError)((err) => { + const msg = 'query for packages failed.'; + console.log(err); + return (0, rxjs_1.throwError)(err.errors && err.errors.length > 0 + ? `${msg} ${err.errors[0].message}` + : `${msg} verify input parameters are correct`); + })); + } +} +exports.queryForRepoPackages = queryForRepoPackages; +function getRepoPackages(owner, repo, numPackages, startCursor, token) { + return queryForRepoPackages(owner, repo, numPackages, startCursor, token).pipe((0, operators_1.map)(result => { + let r; + if (result.repository.packages.edges.length < 1) { + console.log(`package: No packages found for owner: ${owner} in repo: ${repo}`); + r = { + packages: [], + cursor: '', + paginate: false + }; + return r; + } + const packages = result.repository.packages.edges; + const pages = result.repository.packages.pageInfo; + r = { + packages: packages.map(value => ({ + id: value.node.id, + name: value.node.name + })), + cursor: pages.endCursor, + paginate: pages.hasNextPage + }; + return r; + })); +} +exports.getRepoPackages = getRepoPackages; + + +/***/ }), + +/***/ 221: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +__exportStar(__nccwpck_require__(6436), exports); +__exportStar(__nccwpck_require__(2539), exports); + + +/***/ }), + +/***/ 2539: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getPackageNameFilter = void 0; +/** + * Used to apply memoization on getPackageNameFilter + */ +const resultCache = {}; +/** + * Get a filter based on package names to match + * + * @param packageNames - serialized package names filter as string + * @returns the respective package filter + */ +function getPackageNameFilter(packageNames) { + if (resultCache[packageNames]) { + return resultCache[packageNames]; + } + const result = calculatePackageNameFilter(packageNames); + resultCache[packageNames] = result; + return result; +} +exports.getPackageNameFilter = getPackageNameFilter; +const emptyFilter = Object.freeze({ + subfilters: Object.freeze([]), + isEmpty: true, + apply: () => false +}); +/** + * Generates a filter based package names to match + * + * @param packageNames - serialized package names filter as string + * @returns the respective package filter + */ +function calculatePackageNameFilter(packageNames) { + if (packageNames === '') { + return emptyFilter; + } + const separatedPackageNames = packageNames + .split(',') + .map(name => name.trim()) + .filter(name => name !== ''); + if (separatedPackageNames.length <= 0) { + return emptyFilter; + } + const subfilters = separatedPackageNames.map(createFilter); + return { + subfilters, + isEmpty: subfilters.length <= 0, + apply: names => subfilters.some(filter => filter.apply(names)) + }; +} +function createFilter(packageName) { + if (packageName.startsWith('*') || packageName.endsWith('*')) { + return createWildcardFilter(packageName); + } + else if (packageName.startsWith('/') && packageName.endsWith('/')) { + return createRegexFilter(packageName); + } + else { + return createExactMatchFilter(packageName); + } +} +function createWildcardFilter(wildcardPackageName) { + const startsWithWildCard = wildcardPackageName.startsWith('*'); + const endsWithWildCard = wildcardPackageName.endsWith('*'); + let fn; + if (wildcardPackageName === '*') { + fn = () => true; + } + else if (startsWithWildCard && endsWithWildCard) { + const targetText = wildcardPackageName.substring(1, wildcardPackageName.length - 1); + fn = (packageName) => packageName.includes(targetText); + } + else if (startsWithWildCard) { + const targetText = wildcardPackageName.substring(1); + fn = (packageName) => packageName.endsWith(targetText); + } + else { + const targetText = wildcardPackageName.substring(0, wildcardPackageName.length - 1); + fn = (packageName) => packageName.startsWith(targetText); + } + return { + type: 'wildcard', + apply: fn + }; +} +function createRegexFilter(regexPackageName) { + const regexPattern = regexPackageName.substring(1, regexPackageName.length - 1); + const regex = new RegExp(regexPattern); + return { + type: 'regex', + apply: (packageName) => regex.test(packageName) + }; +} +function createExactMatchFilter(matchingPackageName) { + return { + type: 'string', + apply: (packageName) => packageName === matchingPackageName + }; +} + + /***/ }), /***/ 5544: @@ -167,10 +482,8 @@ function deletePackageVersion(packageVersionId, owner, packageName, packageType, username: owner, package_version_id })).pipe((0, operators_1.catchError)(err => { - const msg = 'delete version API failed.'; - return (0, rxjs_1.throwError)(err.errors && err.errors.length > 0 - ? `${msg} ${err.errors[0].message}` - : `${msg} ${err.message} \n${deleted - 1} versions deleted till now.`); + console.log(err); + return rxjs_1.EMPTY; }), (0, operators_1.map)(response => response.status === 204)); } exports.deletePackageVersion = deletePackageVersion; @@ -215,10 +528,7 @@ function getOldestVersions(owner, packageName, packageType, numVersions, page, t per_page: numVersions, page })).pipe((0, operators_1.catchError)(err => { - const msg = 'get versions API failed.'; - return (0, rxjs_1.throwError)(err.errors && err.errors.length > 0 - ? `${msg} ${err.errors[0].message}` - : `${msg} ${err.message}`); + return rxjs_1.EMPTY; }), (0, operators_1.map)(response => { const resp = { versions: response.data.map((version) => { @@ -42145,1303 +42455,1303 @@ exports.parseURL = __nccwpck_require__(2158).parseURL; /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; - -const punycode = __nccwpck_require__(5477); -const tr46 = __nccwpck_require__(4256); - -const specialSchemes = { - ftp: 21, - file: null, - gopher: 70, - http: 80, - https: 443, - ws: 80, - wss: 443 -}; - -const failure = Symbol("failure"); - -function countSymbols(str) { - return punycode.ucs2.decode(str).length; -} - -function at(input, idx) { - const c = input[idx]; - return isNaN(c) ? undefined : String.fromCodePoint(c); -} - -function isASCIIDigit(c) { - return c >= 0x30 && c <= 0x39; -} - -function isASCIIAlpha(c) { - return (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A); -} - -function isASCIIAlphanumeric(c) { - return isASCIIAlpha(c) || isASCIIDigit(c); -} - -function isASCIIHex(c) { - return isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66); -} - -function isSingleDot(buffer) { - return buffer === "." || buffer.toLowerCase() === "%2e"; -} - -function isDoubleDot(buffer) { - buffer = buffer.toLowerCase(); - return buffer === ".." || buffer === "%2e." || buffer === ".%2e" || buffer === "%2e%2e"; -} - -function isWindowsDriveLetterCodePoints(cp1, cp2) { - return isASCIIAlpha(cp1) && (cp2 === 58 || cp2 === 124); -} - -function isWindowsDriveLetterString(string) { - return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && (string[1] === ":" || string[1] === "|"); -} - -function isNormalizedWindowsDriveLetterString(string) { - return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && string[1] === ":"; -} - -function containsForbiddenHostCodePoint(string) { - return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|%|\/|:|\?|@|\[|\\|\]/) !== -1; -} - -function containsForbiddenHostCodePointExcludingPercent(string) { - return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|\/|:|\?|@|\[|\\|\]/) !== -1; -} - -function isSpecialScheme(scheme) { - return specialSchemes[scheme] !== undefined; -} - -function isSpecial(url) { - return isSpecialScheme(url.scheme); -} - -function defaultPort(scheme) { - return specialSchemes[scheme]; -} - -function percentEncode(c) { - let hex = c.toString(16).toUpperCase(); - if (hex.length === 1) { - hex = "0" + hex; - } - - return "%" + hex; -} - -function utf8PercentEncode(c) { - const buf = new Buffer(c); - - let str = ""; - - for (let i = 0; i < buf.length; ++i) { - str += percentEncode(buf[i]); - } - - return str; -} - -function utf8PercentDecode(str) { - const input = new Buffer(str); - const output = []; - for (let i = 0; i < input.length; ++i) { - if (input[i] !== 37) { - output.push(input[i]); - } else if (input[i] === 37 && isASCIIHex(input[i + 1]) && isASCIIHex(input[i + 2])) { - output.push(parseInt(input.slice(i + 1, i + 3).toString(), 16)); - i += 2; - } else { - output.push(input[i]); - } - } - return new Buffer(output).toString(); -} - -function isC0ControlPercentEncode(c) { - return c <= 0x1F || c > 0x7E; -} - -const extraPathPercentEncodeSet = new Set([32, 34, 35, 60, 62, 63, 96, 123, 125]); -function isPathPercentEncode(c) { - return isC0ControlPercentEncode(c) || extraPathPercentEncodeSet.has(c); -} - -const extraUserinfoPercentEncodeSet = - new Set([47, 58, 59, 61, 64, 91, 92, 93, 94, 124]); -function isUserinfoPercentEncode(c) { - return isPathPercentEncode(c) || extraUserinfoPercentEncodeSet.has(c); -} - -function percentEncodeChar(c, encodeSetPredicate) { - const cStr = String.fromCodePoint(c); - - if (encodeSetPredicate(c)) { - return utf8PercentEncode(cStr); - } - - return cStr; -} - -function parseIPv4Number(input) { - let R = 10; - - if (input.length >= 2 && input.charAt(0) === "0" && input.charAt(1).toLowerCase() === "x") { - input = input.substring(2); - R = 16; - } else if (input.length >= 2 && input.charAt(0) === "0") { - input = input.substring(1); - R = 8; - } - - if (input === "") { - return 0; - } - - const regex = R === 10 ? /[^0-9]/ : (R === 16 ? /[^0-9A-Fa-f]/ : /[^0-7]/); - if (regex.test(input)) { - return failure; - } - - return parseInt(input, R); -} - -function parseIPv4(input) { - const parts = input.split("."); - if (parts[parts.length - 1] === "") { - if (parts.length > 1) { - parts.pop(); - } - } - - if (parts.length > 4) { - return input; - } - - const numbers = []; - for (const part of parts) { - if (part === "") { - return input; - } - const n = parseIPv4Number(part); - if (n === failure) { - return input; - } - - numbers.push(n); - } - - for (let i = 0; i < numbers.length - 1; ++i) { - if (numbers[i] > 255) { - return failure; - } - } - if (numbers[numbers.length - 1] >= Math.pow(256, 5 - numbers.length)) { - return failure; - } - - let ipv4 = numbers.pop(); - let counter = 0; - - for (const n of numbers) { - ipv4 += n * Math.pow(256, 3 - counter); - ++counter; - } - - return ipv4; -} - -function serializeIPv4(address) { - let output = ""; - let n = address; - - for (let i = 1; i <= 4; ++i) { - output = String(n % 256) + output; - if (i !== 4) { - output = "." + output; - } - n = Math.floor(n / 256); - } - - return output; -} - -function parseIPv6(input) { - const address = [0, 0, 0, 0, 0, 0, 0, 0]; - let pieceIndex = 0; - let compress = null; - let pointer = 0; - - input = punycode.ucs2.decode(input); - - if (input[pointer] === 58) { - if (input[pointer + 1] !== 58) { - return failure; - } - - pointer += 2; - ++pieceIndex; - compress = pieceIndex; - } - - while (pointer < input.length) { - if (pieceIndex === 8) { - return failure; - } - - if (input[pointer] === 58) { - if (compress !== null) { - return failure; - } - ++pointer; - ++pieceIndex; - compress = pieceIndex; - continue; - } - - let value = 0; - let length = 0; - - while (length < 4 && isASCIIHex(input[pointer])) { - value = value * 0x10 + parseInt(at(input, pointer), 16); - ++pointer; - ++length; - } - - if (input[pointer] === 46) { - if (length === 0) { - return failure; - } - - pointer -= length; - - if (pieceIndex > 6) { - return failure; - } - - let numbersSeen = 0; - - while (input[pointer] !== undefined) { - let ipv4Piece = null; - - if (numbersSeen > 0) { - if (input[pointer] === 46 && numbersSeen < 4) { - ++pointer; - } else { - return failure; - } - } - - if (!isASCIIDigit(input[pointer])) { - return failure; - } - - while (isASCIIDigit(input[pointer])) { - const number = parseInt(at(input, pointer)); - if (ipv4Piece === null) { - ipv4Piece = number; - } else if (ipv4Piece === 0) { - return failure; - } else { - ipv4Piece = ipv4Piece * 10 + number; - } - if (ipv4Piece > 255) { - return failure; - } - ++pointer; - } - - address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece; - - ++numbersSeen; - - if (numbersSeen === 2 || numbersSeen === 4) { - ++pieceIndex; - } - } - - if (numbersSeen !== 4) { - return failure; - } - - break; - } else if (input[pointer] === 58) { - ++pointer; - if (input[pointer] === undefined) { - return failure; - } - } else if (input[pointer] !== undefined) { - return failure; - } - - address[pieceIndex] = value; - ++pieceIndex; - } - - if (compress !== null) { - let swaps = pieceIndex - compress; - pieceIndex = 7; - while (pieceIndex !== 0 && swaps > 0) { - const temp = address[compress + swaps - 1]; - address[compress + swaps - 1] = address[pieceIndex]; - address[pieceIndex] = temp; - --pieceIndex; - --swaps; - } - } else if (compress === null && pieceIndex !== 8) { - return failure; - } - - return address; -} - -function serializeIPv6(address) { - let output = ""; - const seqResult = findLongestZeroSequence(address); - const compress = seqResult.idx; - let ignore0 = false; - - for (let pieceIndex = 0; pieceIndex <= 7; ++pieceIndex) { - if (ignore0 && address[pieceIndex] === 0) { - continue; - } else if (ignore0) { - ignore0 = false; - } - - if (compress === pieceIndex) { - const separator = pieceIndex === 0 ? "::" : ":"; - output += separator; - ignore0 = true; - continue; - } - - output += address[pieceIndex].toString(16); - - if (pieceIndex !== 7) { - output += ":"; - } - } - - return output; -} - -function parseHost(input, isSpecialArg) { - if (input[0] === "[") { - if (input[input.length - 1] !== "]") { - return failure; - } - - return parseIPv6(input.substring(1, input.length - 1)); - } - - if (!isSpecialArg) { - return parseOpaqueHost(input); - } - - const domain = utf8PercentDecode(input); - const asciiDomain = tr46.toASCII(domain, false, tr46.PROCESSING_OPTIONS.NONTRANSITIONAL, false); - if (asciiDomain === null) { - return failure; - } - - if (containsForbiddenHostCodePoint(asciiDomain)) { - return failure; - } - - const ipv4Host = parseIPv4(asciiDomain); - if (typeof ipv4Host === "number" || ipv4Host === failure) { - return ipv4Host; - } - - return asciiDomain; -} - -function parseOpaqueHost(input) { - if (containsForbiddenHostCodePointExcludingPercent(input)) { - return failure; - } - - let output = ""; - const decoded = punycode.ucs2.decode(input); - for (let i = 0; i < decoded.length; ++i) { - output += percentEncodeChar(decoded[i], isC0ControlPercentEncode); - } - return output; -} - -function findLongestZeroSequence(arr) { - let maxIdx = null; - let maxLen = 1; // only find elements > 1 - let currStart = null; - let currLen = 0; - - for (let i = 0; i < arr.length; ++i) { - if (arr[i] !== 0) { - if (currLen > maxLen) { - maxIdx = currStart; - maxLen = currLen; - } - - currStart = null; - currLen = 0; - } else { - if (currStart === null) { - currStart = i; - } - ++currLen; - } - } - - // if trailing zeros - if (currLen > maxLen) { - maxIdx = currStart; - maxLen = currLen; - } - - return { - idx: maxIdx, - len: maxLen - }; -} - -function serializeHost(host) { - if (typeof host === "number") { - return serializeIPv4(host); - } - - // IPv6 serializer - if (host instanceof Array) { - return "[" + serializeIPv6(host) + "]"; - } - - return host; -} - -function trimControlChars(url) { - return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/g, ""); -} - -function trimTabAndNewline(url) { - return url.replace(/\u0009|\u000A|\u000D/g, ""); -} - -function shortenPath(url) { - const path = url.path; - if (path.length === 0) { - return; - } - if (url.scheme === "file" && path.length === 1 && isNormalizedWindowsDriveLetter(path[0])) { - return; - } - - path.pop(); -} - -function includesCredentials(url) { - return url.username !== "" || url.password !== ""; -} - -function cannotHaveAUsernamePasswordPort(url) { - return url.host === null || url.host === "" || url.cannotBeABaseURL || url.scheme === "file"; -} - -function isNormalizedWindowsDriveLetter(string) { - return /^[A-Za-z]:$/.test(string); -} - -function URLStateMachine(input, base, encodingOverride, url, stateOverride) { - this.pointer = 0; - this.input = input; - this.base = base || null; - this.encodingOverride = encodingOverride || "utf-8"; - this.stateOverride = stateOverride; - this.url = url; - this.failure = false; - this.parseError = false; - - if (!this.url) { - this.url = { - scheme: "", - username: "", - password: "", - host: null, - port: null, - path: [], - query: null, - fragment: null, - - cannotBeABaseURL: false - }; - - const res = trimControlChars(this.input); - if (res !== this.input) { - this.parseError = true; - } - this.input = res; - } - - const res = trimTabAndNewline(this.input); - if (res !== this.input) { - this.parseError = true; - } - this.input = res; - - this.state = stateOverride || "scheme start"; - - this.buffer = ""; - this.atFlag = false; - this.arrFlag = false; - this.passwordTokenSeenFlag = false; - - this.input = punycode.ucs2.decode(this.input); - - for (; this.pointer <= this.input.length; ++this.pointer) { - const c = this.input[this.pointer]; - const cStr = isNaN(c) ? undefined : String.fromCodePoint(c); - - // exec state machine - const ret = this["parse " + this.state](c, cStr); - if (!ret) { - break; // terminate algorithm - } else if (ret === failure) { - this.failure = true; - break; - } - } -} - -URLStateMachine.prototype["parse scheme start"] = function parseSchemeStart(c, cStr) { - if (isASCIIAlpha(c)) { - this.buffer += cStr.toLowerCase(); - this.state = "scheme"; - } else if (!this.stateOverride) { - this.state = "no scheme"; - --this.pointer; - } else { - this.parseError = true; - return failure; - } - - return true; -}; - -URLStateMachine.prototype["parse scheme"] = function parseScheme(c, cStr) { - if (isASCIIAlphanumeric(c) || c === 43 || c === 45 || c === 46) { - this.buffer += cStr.toLowerCase(); - } else if (c === 58) { - if (this.stateOverride) { - if (isSpecial(this.url) && !isSpecialScheme(this.buffer)) { - return false; - } - - if (!isSpecial(this.url) && isSpecialScheme(this.buffer)) { - return false; - } - - if ((includesCredentials(this.url) || this.url.port !== null) && this.buffer === "file") { - return false; - } - - if (this.url.scheme === "file" && (this.url.host === "" || this.url.host === null)) { - return false; - } - } - this.url.scheme = this.buffer; - this.buffer = ""; - if (this.stateOverride) { - return false; - } - if (this.url.scheme === "file") { - if (this.input[this.pointer + 1] !== 47 || this.input[this.pointer + 2] !== 47) { - this.parseError = true; - } - this.state = "file"; - } else if (isSpecial(this.url) && this.base !== null && this.base.scheme === this.url.scheme) { - this.state = "special relative or authority"; - } else if (isSpecial(this.url)) { - this.state = "special authority slashes"; - } else if (this.input[this.pointer + 1] === 47) { - this.state = "path or authority"; - ++this.pointer; - } else { - this.url.cannotBeABaseURL = true; - this.url.path.push(""); - this.state = "cannot-be-a-base-URL path"; - } - } else if (!this.stateOverride) { - this.buffer = ""; - this.state = "no scheme"; - this.pointer = -1; - } else { - this.parseError = true; - return failure; - } - - return true; -}; - -URLStateMachine.prototype["parse no scheme"] = function parseNoScheme(c) { - if (this.base === null || (this.base.cannotBeABaseURL && c !== 35)) { - return failure; - } else if (this.base.cannotBeABaseURL && c === 35) { - this.url.scheme = this.base.scheme; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - this.url.fragment = ""; - this.url.cannotBeABaseURL = true; - this.state = "fragment"; - } else if (this.base.scheme === "file") { - this.state = "file"; - --this.pointer; - } else { - this.state = "relative"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse special relative or authority"] = function parseSpecialRelativeOrAuthority(c) { - if (c === 47 && this.input[this.pointer + 1] === 47) { - this.state = "special authority ignore slashes"; - ++this.pointer; - } else { - this.parseError = true; - this.state = "relative"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse path or authority"] = function parsePathOrAuthority(c) { - if (c === 47) { - this.state = "authority"; - } else { - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse relative"] = function parseRelative(c) { - this.url.scheme = this.base.scheme; - if (isNaN(c)) { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - } else if (c === 47) { - this.state = "relative slash"; - } else if (c === 63) { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.url.path = this.base.path.slice(); - this.url.query = ""; - this.state = "query"; - } else if (c === 35) { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - this.url.fragment = ""; - this.state = "fragment"; - } else if (isSpecial(this.url) && c === 92) { - this.parseError = true; - this.state = "relative slash"; - } else { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.url.path = this.base.path.slice(0, this.base.path.length - 1); - - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse relative slash"] = function parseRelativeSlash(c) { - if (isSpecial(this.url) && (c === 47 || c === 92)) { - if (c === 92) { - this.parseError = true; - } - this.state = "special authority ignore slashes"; - } else if (c === 47) { - this.state = "authority"; - } else { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse special authority slashes"] = function parseSpecialAuthoritySlashes(c) { - if (c === 47 && this.input[this.pointer + 1] === 47) { - this.state = "special authority ignore slashes"; - ++this.pointer; - } else { - this.parseError = true; - this.state = "special authority ignore slashes"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse special authority ignore slashes"] = function parseSpecialAuthorityIgnoreSlashes(c) { - if (c !== 47 && c !== 92) { - this.state = "authority"; - --this.pointer; - } else { - this.parseError = true; - } - - return true; -}; - -URLStateMachine.prototype["parse authority"] = function parseAuthority(c, cStr) { - if (c === 64) { - this.parseError = true; - if (this.atFlag) { - this.buffer = "%40" + this.buffer; - } - this.atFlag = true; - - // careful, this is based on buffer and has its own pointer (this.pointer != pointer) and inner chars - const len = countSymbols(this.buffer); - for (let pointer = 0; pointer < len; ++pointer) { - const codePoint = this.buffer.codePointAt(pointer); - - if (codePoint === 58 && !this.passwordTokenSeenFlag) { - this.passwordTokenSeenFlag = true; - continue; - } - const encodedCodePoints = percentEncodeChar(codePoint, isUserinfoPercentEncode); - if (this.passwordTokenSeenFlag) { - this.url.password += encodedCodePoints; - } else { - this.url.username += encodedCodePoints; - } - } - this.buffer = ""; - } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || - (isSpecial(this.url) && c === 92)) { - if (this.atFlag && this.buffer === "") { - this.parseError = true; - return failure; - } - this.pointer -= countSymbols(this.buffer) + 1; - this.buffer = ""; - this.state = "host"; - } else { - this.buffer += cStr; - } - - return true; -}; - -URLStateMachine.prototype["parse hostname"] = -URLStateMachine.prototype["parse host"] = function parseHostName(c, cStr) { - if (this.stateOverride && this.url.scheme === "file") { - --this.pointer; - this.state = "file host"; - } else if (c === 58 && !this.arrFlag) { - if (this.buffer === "") { - this.parseError = true; - return failure; - } - - const host = parseHost(this.buffer, isSpecial(this.url)); - if (host === failure) { - return failure; - } - - this.url.host = host; - this.buffer = ""; - this.state = "port"; - if (this.stateOverride === "hostname") { - return false; - } - } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || - (isSpecial(this.url) && c === 92)) { - --this.pointer; - if (isSpecial(this.url) && this.buffer === "") { - this.parseError = true; - return failure; - } else if (this.stateOverride && this.buffer === "" && - (includesCredentials(this.url) || this.url.port !== null)) { - this.parseError = true; - return false; - } - - const host = parseHost(this.buffer, isSpecial(this.url)); - if (host === failure) { - return failure; - } - - this.url.host = host; - this.buffer = ""; - this.state = "path start"; - if (this.stateOverride) { - return false; - } - } else { - if (c === 91) { - this.arrFlag = true; - } else if (c === 93) { - this.arrFlag = false; - } - this.buffer += cStr; - } - - return true; -}; - -URLStateMachine.prototype["parse port"] = function parsePort(c, cStr) { - if (isASCIIDigit(c)) { - this.buffer += cStr; - } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || - (isSpecial(this.url) && c === 92) || - this.stateOverride) { - if (this.buffer !== "") { - const port = parseInt(this.buffer); - if (port > Math.pow(2, 16) - 1) { - this.parseError = true; - return failure; - } - this.url.port = port === defaultPort(this.url.scheme) ? null : port; - this.buffer = ""; - } - if (this.stateOverride) { - return false; - } - this.state = "path start"; - --this.pointer; - } else { - this.parseError = true; - return failure; - } - - return true; -}; - -const fileOtherwiseCodePoints = new Set([47, 92, 63, 35]); - -URLStateMachine.prototype["parse file"] = function parseFile(c) { - this.url.scheme = "file"; - - if (c === 47 || c === 92) { - if (c === 92) { - this.parseError = true; - } - this.state = "file slash"; - } else if (this.base !== null && this.base.scheme === "file") { - if (isNaN(c)) { - this.url.host = this.base.host; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - } else if (c === 63) { - this.url.host = this.base.host; - this.url.path = this.base.path.slice(); - this.url.query = ""; - this.state = "query"; - } else if (c === 35) { - this.url.host = this.base.host; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - this.url.fragment = ""; - this.state = "fragment"; - } else { - if (this.input.length - this.pointer - 1 === 0 || // remaining consists of 0 code points - !isWindowsDriveLetterCodePoints(c, this.input[this.pointer + 1]) || - (this.input.length - this.pointer - 1 >= 2 && // remaining has at least 2 code points - !fileOtherwiseCodePoints.has(this.input[this.pointer + 2]))) { - this.url.host = this.base.host; - this.url.path = this.base.path.slice(); - shortenPath(this.url); - } else { - this.parseError = true; - } - - this.state = "path"; - --this.pointer; - } - } else { - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse file slash"] = function parseFileSlash(c) { - if (c === 47 || c === 92) { - if (c === 92) { - this.parseError = true; - } - this.state = "file host"; - } else { - if (this.base !== null && this.base.scheme === "file") { - if (isNormalizedWindowsDriveLetterString(this.base.path[0])) { - this.url.path.push(this.base.path[0]); - } else { - this.url.host = this.base.host; - } - } - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse file host"] = function parseFileHost(c, cStr) { - if (isNaN(c) || c === 47 || c === 92 || c === 63 || c === 35) { - --this.pointer; - if (!this.stateOverride && isWindowsDriveLetterString(this.buffer)) { - this.parseError = true; - this.state = "path"; - } else if (this.buffer === "") { - this.url.host = ""; - if (this.stateOverride) { - return false; - } - this.state = "path start"; - } else { - let host = parseHost(this.buffer, isSpecial(this.url)); - if (host === failure) { - return failure; - } - if (host === "localhost") { - host = ""; - } - this.url.host = host; - - if (this.stateOverride) { - return false; - } - - this.buffer = ""; - this.state = "path start"; - } - } else { - this.buffer += cStr; - } - - return true; -}; - -URLStateMachine.prototype["parse path start"] = function parsePathStart(c) { - if (isSpecial(this.url)) { - if (c === 92) { - this.parseError = true; - } - this.state = "path"; - - if (c !== 47 && c !== 92) { - --this.pointer; - } - } else if (!this.stateOverride && c === 63) { - this.url.query = ""; - this.state = "query"; - } else if (!this.stateOverride && c === 35) { - this.url.fragment = ""; - this.state = "fragment"; - } else if (c !== undefined) { - this.state = "path"; - if (c !== 47) { - --this.pointer; - } - } - - return true; -}; - -URLStateMachine.prototype["parse path"] = function parsePath(c) { - if (isNaN(c) || c === 47 || (isSpecial(this.url) && c === 92) || - (!this.stateOverride && (c === 63 || c === 35))) { - if (isSpecial(this.url) && c === 92) { - this.parseError = true; - } - - if (isDoubleDot(this.buffer)) { - shortenPath(this.url); - if (c !== 47 && !(isSpecial(this.url) && c === 92)) { - this.url.path.push(""); - } - } else if (isSingleDot(this.buffer) && c !== 47 && - !(isSpecial(this.url) && c === 92)) { - this.url.path.push(""); - } else if (!isSingleDot(this.buffer)) { - if (this.url.scheme === "file" && this.url.path.length === 0 && isWindowsDriveLetterString(this.buffer)) { - if (this.url.host !== "" && this.url.host !== null) { - this.parseError = true; - this.url.host = ""; - } - this.buffer = this.buffer[0] + ":"; - } - this.url.path.push(this.buffer); - } - this.buffer = ""; - if (this.url.scheme === "file" && (c === undefined || c === 63 || c === 35)) { - while (this.url.path.length > 1 && this.url.path[0] === "") { - this.parseError = true; - this.url.path.shift(); - } - } - if (c === 63) { - this.url.query = ""; - this.state = "query"; - } - if (c === 35) { - this.url.fragment = ""; - this.state = "fragment"; - } - } else { - // TODO: If c is not a URL code point and not "%", parse error. - - if (c === 37 && - (!isASCIIHex(this.input[this.pointer + 1]) || - !isASCIIHex(this.input[this.pointer + 2]))) { - this.parseError = true; - } - - this.buffer += percentEncodeChar(c, isPathPercentEncode); - } - - return true; -}; - -URLStateMachine.prototype["parse cannot-be-a-base-URL path"] = function parseCannotBeABaseURLPath(c) { - if (c === 63) { - this.url.query = ""; - this.state = "query"; - } else if (c === 35) { - this.url.fragment = ""; - this.state = "fragment"; - } else { - // TODO: Add: not a URL code point - if (!isNaN(c) && c !== 37) { - this.parseError = true; - } - - if (c === 37 && - (!isASCIIHex(this.input[this.pointer + 1]) || - !isASCIIHex(this.input[this.pointer + 2]))) { - this.parseError = true; - } - - if (!isNaN(c)) { - this.url.path[0] = this.url.path[0] + percentEncodeChar(c, isC0ControlPercentEncode); - } - } - - return true; -}; - -URLStateMachine.prototype["parse query"] = function parseQuery(c, cStr) { - if (isNaN(c) || (!this.stateOverride && c === 35)) { - if (!isSpecial(this.url) || this.url.scheme === "ws" || this.url.scheme === "wss") { - this.encodingOverride = "utf-8"; - } - - const buffer = new Buffer(this.buffer); // TODO: Use encoding override instead - for (let i = 0; i < buffer.length; ++i) { - if (buffer[i] < 0x21 || buffer[i] > 0x7E || buffer[i] === 0x22 || buffer[i] === 0x23 || - buffer[i] === 0x3C || buffer[i] === 0x3E) { - this.url.query += percentEncode(buffer[i]); - } else { - this.url.query += String.fromCodePoint(buffer[i]); - } - } - - this.buffer = ""; - if (c === 35) { - this.url.fragment = ""; - this.state = "fragment"; - } - } else { - // TODO: If c is not a URL code point and not "%", parse error. - if (c === 37 && - (!isASCIIHex(this.input[this.pointer + 1]) || - !isASCIIHex(this.input[this.pointer + 2]))) { - this.parseError = true; - } - - this.buffer += cStr; - } - - return true; -}; - -URLStateMachine.prototype["parse fragment"] = function parseFragment(c) { - if (isNaN(c)) { // do nothing - } else if (c === 0x0) { - this.parseError = true; - } else { - // TODO: If c is not a URL code point and not "%", parse error. - if (c === 37 && - (!isASCIIHex(this.input[this.pointer + 1]) || - !isASCIIHex(this.input[this.pointer + 2]))) { - this.parseError = true; - } - - this.url.fragment += percentEncodeChar(c, isC0ControlPercentEncode); - } - - return true; -}; - -function serializeURL(url, excludeFragment) { - let output = url.scheme + ":"; - if (url.host !== null) { - output += "//"; - - if (url.username !== "" || url.password !== "") { - output += url.username; - if (url.password !== "") { - output += ":" + url.password; - } - output += "@"; - } - - output += serializeHost(url.host); - - if (url.port !== null) { - output += ":" + url.port; - } - } else if (url.host === null && url.scheme === "file") { - output += "//"; - } - - if (url.cannotBeABaseURL) { - output += url.path[0]; - } else { - for (const string of url.path) { - output += "/" + string; - } - } - - if (url.query !== null) { - output += "?" + url.query; - } - - if (!excludeFragment && url.fragment !== null) { - output += "#" + url.fragment; - } - - return output; -} - -function serializeOrigin(tuple) { - let result = tuple.scheme + "://"; - result += serializeHost(tuple.host); - - if (tuple.port !== null) { - result += ":" + tuple.port; - } - - return result; -} - -module.exports.serializeURL = serializeURL; - -module.exports.serializeURLOrigin = function (url) { - // https://url.spec.whatwg.org/#concept-url-origin - switch (url.scheme) { - case "blob": - try { - return module.exports.serializeURLOrigin(module.exports.parseURL(url.path[0])); - } catch (e) { - // serializing an opaque origin returns "null" - return "null"; - } - case "ftp": - case "gopher": - case "http": - case "https": - case "ws": - case "wss": - return serializeOrigin({ - scheme: url.scheme, - host: url.host, - port: url.port - }); - case "file": - // spec says "exercise to the reader", chrome says "file://" - return "file://"; - default: - // serializing an opaque origin returns "null" - return "null"; - } -}; - -module.exports.basicURLParse = function (input, options) { - if (options === undefined) { - options = {}; - } - - const usm = new URLStateMachine(input, options.baseURL, options.encodingOverride, options.url, options.stateOverride); - if (usm.failure) { - return "failure"; - } - - return usm.url; -}; - -module.exports.setTheUsername = function (url, username) { - url.username = ""; - const decoded = punycode.ucs2.decode(username); - for (let i = 0; i < decoded.length; ++i) { - url.username += percentEncodeChar(decoded[i], isUserinfoPercentEncode); - } -}; - -module.exports.setThePassword = function (url, password) { - url.password = ""; - const decoded = punycode.ucs2.decode(password); - for (let i = 0; i < decoded.length; ++i) { - url.password += percentEncodeChar(decoded[i], isUserinfoPercentEncode); - } -}; - -module.exports.serializeHost = serializeHost; - -module.exports.cannotHaveAUsernamePasswordPort = cannotHaveAUsernamePasswordPort; - -module.exports.serializeInteger = function (integer) { - return String(integer); -}; - -module.exports.parseURL = function (input, options) { - if (options === undefined) { - options = {}; - } - - // We don't handle blobs, so this just delegates: - return module.exports.basicURLParse(input, { baseURL: options.baseURL, encodingOverride: options.encodingOverride }); -}; + +const punycode = __nccwpck_require__(5477); +const tr46 = __nccwpck_require__(4256); + +const specialSchemes = { + ftp: 21, + file: null, + gopher: 70, + http: 80, + https: 443, + ws: 80, + wss: 443 +}; + +const failure = Symbol("failure"); + +function countSymbols(str) { + return punycode.ucs2.decode(str).length; +} + +function at(input, idx) { + const c = input[idx]; + return isNaN(c) ? undefined : String.fromCodePoint(c); +} + +function isASCIIDigit(c) { + return c >= 0x30 && c <= 0x39; +} + +function isASCIIAlpha(c) { + return (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A); +} + +function isASCIIAlphanumeric(c) { + return isASCIIAlpha(c) || isASCIIDigit(c); +} + +function isASCIIHex(c) { + return isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66); +} + +function isSingleDot(buffer) { + return buffer === "." || buffer.toLowerCase() === "%2e"; +} + +function isDoubleDot(buffer) { + buffer = buffer.toLowerCase(); + return buffer === ".." || buffer === "%2e." || buffer === ".%2e" || buffer === "%2e%2e"; +} + +function isWindowsDriveLetterCodePoints(cp1, cp2) { + return isASCIIAlpha(cp1) && (cp2 === 58 || cp2 === 124); +} + +function isWindowsDriveLetterString(string) { + return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && (string[1] === ":" || string[1] === "|"); +} + +function isNormalizedWindowsDriveLetterString(string) { + return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && string[1] === ":"; +} + +function containsForbiddenHostCodePoint(string) { + return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|%|\/|:|\?|@|\[|\\|\]/) !== -1; +} + +function containsForbiddenHostCodePointExcludingPercent(string) { + return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|\/|:|\?|@|\[|\\|\]/) !== -1; +} + +function isSpecialScheme(scheme) { + return specialSchemes[scheme] !== undefined; +} + +function isSpecial(url) { + return isSpecialScheme(url.scheme); +} + +function defaultPort(scheme) { + return specialSchemes[scheme]; +} + +function percentEncode(c) { + let hex = c.toString(16).toUpperCase(); + if (hex.length === 1) { + hex = "0" + hex; + } + + return "%" + hex; +} + +function utf8PercentEncode(c) { + const buf = new Buffer(c); + + let str = ""; + + for (let i = 0; i < buf.length; ++i) { + str += percentEncode(buf[i]); + } + + return str; +} + +function utf8PercentDecode(str) { + const input = new Buffer(str); + const output = []; + for (let i = 0; i < input.length; ++i) { + if (input[i] !== 37) { + output.push(input[i]); + } else if (input[i] === 37 && isASCIIHex(input[i + 1]) && isASCIIHex(input[i + 2])) { + output.push(parseInt(input.slice(i + 1, i + 3).toString(), 16)); + i += 2; + } else { + output.push(input[i]); + } + } + return new Buffer(output).toString(); +} + +function isC0ControlPercentEncode(c) { + return c <= 0x1F || c > 0x7E; +} + +const extraPathPercentEncodeSet = new Set([32, 34, 35, 60, 62, 63, 96, 123, 125]); +function isPathPercentEncode(c) { + return isC0ControlPercentEncode(c) || extraPathPercentEncodeSet.has(c); +} + +const extraUserinfoPercentEncodeSet = + new Set([47, 58, 59, 61, 64, 91, 92, 93, 94, 124]); +function isUserinfoPercentEncode(c) { + return isPathPercentEncode(c) || extraUserinfoPercentEncodeSet.has(c); +} + +function percentEncodeChar(c, encodeSetPredicate) { + const cStr = String.fromCodePoint(c); + + if (encodeSetPredicate(c)) { + return utf8PercentEncode(cStr); + } + + return cStr; +} + +function parseIPv4Number(input) { + let R = 10; + + if (input.length >= 2 && input.charAt(0) === "0" && input.charAt(1).toLowerCase() === "x") { + input = input.substring(2); + R = 16; + } else if (input.length >= 2 && input.charAt(0) === "0") { + input = input.substring(1); + R = 8; + } + + if (input === "") { + return 0; + } + + const regex = R === 10 ? /[^0-9]/ : (R === 16 ? /[^0-9A-Fa-f]/ : /[^0-7]/); + if (regex.test(input)) { + return failure; + } + + return parseInt(input, R); +} + +function parseIPv4(input) { + const parts = input.split("."); + if (parts[parts.length - 1] === "") { + if (parts.length > 1) { + parts.pop(); + } + } + + if (parts.length > 4) { + return input; + } + + const numbers = []; + for (const part of parts) { + if (part === "") { + return input; + } + const n = parseIPv4Number(part); + if (n === failure) { + return input; + } + + numbers.push(n); + } + + for (let i = 0; i < numbers.length - 1; ++i) { + if (numbers[i] > 255) { + return failure; + } + } + if (numbers[numbers.length - 1] >= Math.pow(256, 5 - numbers.length)) { + return failure; + } + + let ipv4 = numbers.pop(); + let counter = 0; + + for (const n of numbers) { + ipv4 += n * Math.pow(256, 3 - counter); + ++counter; + } + + return ipv4; +} + +function serializeIPv4(address) { + let output = ""; + let n = address; + + for (let i = 1; i <= 4; ++i) { + output = String(n % 256) + output; + if (i !== 4) { + output = "." + output; + } + n = Math.floor(n / 256); + } + + return output; +} + +function parseIPv6(input) { + const address = [0, 0, 0, 0, 0, 0, 0, 0]; + let pieceIndex = 0; + let compress = null; + let pointer = 0; + + input = punycode.ucs2.decode(input); + + if (input[pointer] === 58) { + if (input[pointer + 1] !== 58) { + return failure; + } + + pointer += 2; + ++pieceIndex; + compress = pieceIndex; + } + + while (pointer < input.length) { + if (pieceIndex === 8) { + return failure; + } + + if (input[pointer] === 58) { + if (compress !== null) { + return failure; + } + ++pointer; + ++pieceIndex; + compress = pieceIndex; + continue; + } + + let value = 0; + let length = 0; + + while (length < 4 && isASCIIHex(input[pointer])) { + value = value * 0x10 + parseInt(at(input, pointer), 16); + ++pointer; + ++length; + } + + if (input[pointer] === 46) { + if (length === 0) { + return failure; + } + + pointer -= length; + + if (pieceIndex > 6) { + return failure; + } + + let numbersSeen = 0; + + while (input[pointer] !== undefined) { + let ipv4Piece = null; + + if (numbersSeen > 0) { + if (input[pointer] === 46 && numbersSeen < 4) { + ++pointer; + } else { + return failure; + } + } + + if (!isASCIIDigit(input[pointer])) { + return failure; + } + + while (isASCIIDigit(input[pointer])) { + const number = parseInt(at(input, pointer)); + if (ipv4Piece === null) { + ipv4Piece = number; + } else if (ipv4Piece === 0) { + return failure; + } else { + ipv4Piece = ipv4Piece * 10 + number; + } + if (ipv4Piece > 255) { + return failure; + } + ++pointer; + } + + address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece; + + ++numbersSeen; + + if (numbersSeen === 2 || numbersSeen === 4) { + ++pieceIndex; + } + } + + if (numbersSeen !== 4) { + return failure; + } + + break; + } else if (input[pointer] === 58) { + ++pointer; + if (input[pointer] === undefined) { + return failure; + } + } else if (input[pointer] !== undefined) { + return failure; + } + + address[pieceIndex] = value; + ++pieceIndex; + } + + if (compress !== null) { + let swaps = pieceIndex - compress; + pieceIndex = 7; + while (pieceIndex !== 0 && swaps > 0) { + const temp = address[compress + swaps - 1]; + address[compress + swaps - 1] = address[pieceIndex]; + address[pieceIndex] = temp; + --pieceIndex; + --swaps; + } + } else if (compress === null && pieceIndex !== 8) { + return failure; + } + + return address; +} + +function serializeIPv6(address) { + let output = ""; + const seqResult = findLongestZeroSequence(address); + const compress = seqResult.idx; + let ignore0 = false; + + for (let pieceIndex = 0; pieceIndex <= 7; ++pieceIndex) { + if (ignore0 && address[pieceIndex] === 0) { + continue; + } else if (ignore0) { + ignore0 = false; + } + + if (compress === pieceIndex) { + const separator = pieceIndex === 0 ? "::" : ":"; + output += separator; + ignore0 = true; + continue; + } + + output += address[pieceIndex].toString(16); + + if (pieceIndex !== 7) { + output += ":"; + } + } + + return output; +} + +function parseHost(input, isSpecialArg) { + if (input[0] === "[") { + if (input[input.length - 1] !== "]") { + return failure; + } + + return parseIPv6(input.substring(1, input.length - 1)); + } + + if (!isSpecialArg) { + return parseOpaqueHost(input); + } + + const domain = utf8PercentDecode(input); + const asciiDomain = tr46.toASCII(domain, false, tr46.PROCESSING_OPTIONS.NONTRANSITIONAL, false); + if (asciiDomain === null) { + return failure; + } + + if (containsForbiddenHostCodePoint(asciiDomain)) { + return failure; + } + + const ipv4Host = parseIPv4(asciiDomain); + if (typeof ipv4Host === "number" || ipv4Host === failure) { + return ipv4Host; + } + + return asciiDomain; +} + +function parseOpaqueHost(input) { + if (containsForbiddenHostCodePointExcludingPercent(input)) { + return failure; + } + + let output = ""; + const decoded = punycode.ucs2.decode(input); + for (let i = 0; i < decoded.length; ++i) { + output += percentEncodeChar(decoded[i], isC0ControlPercentEncode); + } + return output; +} + +function findLongestZeroSequence(arr) { + let maxIdx = null; + let maxLen = 1; // only find elements > 1 + let currStart = null; + let currLen = 0; + + for (let i = 0; i < arr.length; ++i) { + if (arr[i] !== 0) { + if (currLen > maxLen) { + maxIdx = currStart; + maxLen = currLen; + } + + currStart = null; + currLen = 0; + } else { + if (currStart === null) { + currStart = i; + } + ++currLen; + } + } + + // if trailing zeros + if (currLen > maxLen) { + maxIdx = currStart; + maxLen = currLen; + } + + return { + idx: maxIdx, + len: maxLen + }; +} + +function serializeHost(host) { + if (typeof host === "number") { + return serializeIPv4(host); + } + + // IPv6 serializer + if (host instanceof Array) { + return "[" + serializeIPv6(host) + "]"; + } + + return host; +} + +function trimControlChars(url) { + return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/g, ""); +} + +function trimTabAndNewline(url) { + return url.replace(/\u0009|\u000A|\u000D/g, ""); +} + +function shortenPath(url) { + const path = url.path; + if (path.length === 0) { + return; + } + if (url.scheme === "file" && path.length === 1 && isNormalizedWindowsDriveLetter(path[0])) { + return; + } + + path.pop(); +} + +function includesCredentials(url) { + return url.username !== "" || url.password !== ""; +} + +function cannotHaveAUsernamePasswordPort(url) { + return url.host === null || url.host === "" || url.cannotBeABaseURL || url.scheme === "file"; +} + +function isNormalizedWindowsDriveLetter(string) { + return /^[A-Za-z]:$/.test(string); +} + +function URLStateMachine(input, base, encodingOverride, url, stateOverride) { + this.pointer = 0; + this.input = input; + this.base = base || null; + this.encodingOverride = encodingOverride || "utf-8"; + this.stateOverride = stateOverride; + this.url = url; + this.failure = false; + this.parseError = false; + + if (!this.url) { + this.url = { + scheme: "", + username: "", + password: "", + host: null, + port: null, + path: [], + query: null, + fragment: null, + + cannotBeABaseURL: false + }; + + const res = trimControlChars(this.input); + if (res !== this.input) { + this.parseError = true; + } + this.input = res; + } + + const res = trimTabAndNewline(this.input); + if (res !== this.input) { + this.parseError = true; + } + this.input = res; + + this.state = stateOverride || "scheme start"; + + this.buffer = ""; + this.atFlag = false; + this.arrFlag = false; + this.passwordTokenSeenFlag = false; + + this.input = punycode.ucs2.decode(this.input); + + for (; this.pointer <= this.input.length; ++this.pointer) { + const c = this.input[this.pointer]; + const cStr = isNaN(c) ? undefined : String.fromCodePoint(c); + + // exec state machine + const ret = this["parse " + this.state](c, cStr); + if (!ret) { + break; // terminate algorithm + } else if (ret === failure) { + this.failure = true; + break; + } + } +} + +URLStateMachine.prototype["parse scheme start"] = function parseSchemeStart(c, cStr) { + if (isASCIIAlpha(c)) { + this.buffer += cStr.toLowerCase(); + this.state = "scheme"; + } else if (!this.stateOverride) { + this.state = "no scheme"; + --this.pointer; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +URLStateMachine.prototype["parse scheme"] = function parseScheme(c, cStr) { + if (isASCIIAlphanumeric(c) || c === 43 || c === 45 || c === 46) { + this.buffer += cStr.toLowerCase(); + } else if (c === 58) { + if (this.stateOverride) { + if (isSpecial(this.url) && !isSpecialScheme(this.buffer)) { + return false; + } + + if (!isSpecial(this.url) && isSpecialScheme(this.buffer)) { + return false; + } + + if ((includesCredentials(this.url) || this.url.port !== null) && this.buffer === "file") { + return false; + } + + if (this.url.scheme === "file" && (this.url.host === "" || this.url.host === null)) { + return false; + } + } + this.url.scheme = this.buffer; + this.buffer = ""; + if (this.stateOverride) { + return false; + } + if (this.url.scheme === "file") { + if (this.input[this.pointer + 1] !== 47 || this.input[this.pointer + 2] !== 47) { + this.parseError = true; + } + this.state = "file"; + } else if (isSpecial(this.url) && this.base !== null && this.base.scheme === this.url.scheme) { + this.state = "special relative or authority"; + } else if (isSpecial(this.url)) { + this.state = "special authority slashes"; + } else if (this.input[this.pointer + 1] === 47) { + this.state = "path or authority"; + ++this.pointer; + } else { + this.url.cannotBeABaseURL = true; + this.url.path.push(""); + this.state = "cannot-be-a-base-URL path"; + } + } else if (!this.stateOverride) { + this.buffer = ""; + this.state = "no scheme"; + this.pointer = -1; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +URLStateMachine.prototype["parse no scheme"] = function parseNoScheme(c) { + if (this.base === null || (this.base.cannotBeABaseURL && c !== 35)) { + return failure; + } else if (this.base.cannotBeABaseURL && c === 35) { + this.url.scheme = this.base.scheme; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.url.cannotBeABaseURL = true; + this.state = "fragment"; + } else if (this.base.scheme === "file") { + this.state = "file"; + --this.pointer; + } else { + this.state = "relative"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special relative or authority"] = function parseSpecialRelativeOrAuthority(c) { + if (c === 47 && this.input[this.pointer + 1] === 47) { + this.state = "special authority ignore slashes"; + ++this.pointer; + } else { + this.parseError = true; + this.state = "relative"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse path or authority"] = function parsePathOrAuthority(c) { + if (c === 47) { + this.state = "authority"; + } else { + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse relative"] = function parseRelative(c) { + this.url.scheme = this.base.scheme; + if (isNaN(c)) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + } else if (c === 47) { + this.state = "relative slash"; + } else if (c === 63) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.state = "fragment"; + } else if (isSpecial(this.url) && c === 92) { + this.parseError = true; + this.state = "relative slash"; + } else { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(0, this.base.path.length - 1); + + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse relative slash"] = function parseRelativeSlash(c) { + if (isSpecial(this.url) && (c === 47 || c === 92)) { + if (c === 92) { + this.parseError = true; + } + this.state = "special authority ignore slashes"; + } else if (c === 47) { + this.state = "authority"; + } else { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special authority slashes"] = function parseSpecialAuthoritySlashes(c) { + if (c === 47 && this.input[this.pointer + 1] === 47) { + this.state = "special authority ignore slashes"; + ++this.pointer; + } else { + this.parseError = true; + this.state = "special authority ignore slashes"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special authority ignore slashes"] = function parseSpecialAuthorityIgnoreSlashes(c) { + if (c !== 47 && c !== 92) { + this.state = "authority"; + --this.pointer; + } else { + this.parseError = true; + } + + return true; +}; + +URLStateMachine.prototype["parse authority"] = function parseAuthority(c, cStr) { + if (c === 64) { + this.parseError = true; + if (this.atFlag) { + this.buffer = "%40" + this.buffer; + } + this.atFlag = true; + + // careful, this is based on buffer and has its own pointer (this.pointer != pointer) and inner chars + const len = countSymbols(this.buffer); + for (let pointer = 0; pointer < len; ++pointer) { + const codePoint = this.buffer.codePointAt(pointer); + + if (codePoint === 58 && !this.passwordTokenSeenFlag) { + this.passwordTokenSeenFlag = true; + continue; + } + const encodedCodePoints = percentEncodeChar(codePoint, isUserinfoPercentEncode); + if (this.passwordTokenSeenFlag) { + this.url.password += encodedCodePoints; + } else { + this.url.username += encodedCodePoints; + } + } + this.buffer = ""; + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92)) { + if (this.atFlag && this.buffer === "") { + this.parseError = true; + return failure; + } + this.pointer -= countSymbols(this.buffer) + 1; + this.buffer = ""; + this.state = "host"; + } else { + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse hostname"] = +URLStateMachine.prototype["parse host"] = function parseHostName(c, cStr) { + if (this.stateOverride && this.url.scheme === "file") { + --this.pointer; + this.state = "file host"; + } else if (c === 58 && !this.arrFlag) { + if (this.buffer === "") { + this.parseError = true; + return failure; + } + + const host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + + this.url.host = host; + this.buffer = ""; + this.state = "port"; + if (this.stateOverride === "hostname") { + return false; + } + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92)) { + --this.pointer; + if (isSpecial(this.url) && this.buffer === "") { + this.parseError = true; + return failure; + } else if (this.stateOverride && this.buffer === "" && + (includesCredentials(this.url) || this.url.port !== null)) { + this.parseError = true; + return false; + } + + const host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + + this.url.host = host; + this.buffer = ""; + this.state = "path start"; + if (this.stateOverride) { + return false; + } + } else { + if (c === 91) { + this.arrFlag = true; + } else if (c === 93) { + this.arrFlag = false; + } + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse port"] = function parsePort(c, cStr) { + if (isASCIIDigit(c)) { + this.buffer += cStr; + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92) || + this.stateOverride) { + if (this.buffer !== "") { + const port = parseInt(this.buffer); + if (port > Math.pow(2, 16) - 1) { + this.parseError = true; + return failure; + } + this.url.port = port === defaultPort(this.url.scheme) ? null : port; + this.buffer = ""; + } + if (this.stateOverride) { + return false; + } + this.state = "path start"; + --this.pointer; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +const fileOtherwiseCodePoints = new Set([47, 92, 63, 35]); + +URLStateMachine.prototype["parse file"] = function parseFile(c) { + this.url.scheme = "file"; + + if (c === 47 || c === 92) { + if (c === 92) { + this.parseError = true; + } + this.state = "file slash"; + } else if (this.base !== null && this.base.scheme === "file") { + if (isNaN(c)) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + } else if (c === 63) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.state = "fragment"; + } else { + if (this.input.length - this.pointer - 1 === 0 || // remaining consists of 0 code points + !isWindowsDriveLetterCodePoints(c, this.input[this.pointer + 1]) || + (this.input.length - this.pointer - 1 >= 2 && // remaining has at least 2 code points + !fileOtherwiseCodePoints.has(this.input[this.pointer + 2]))) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + shortenPath(this.url); + } else { + this.parseError = true; + } + + this.state = "path"; + --this.pointer; + } + } else { + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse file slash"] = function parseFileSlash(c) { + if (c === 47 || c === 92) { + if (c === 92) { + this.parseError = true; + } + this.state = "file host"; + } else { + if (this.base !== null && this.base.scheme === "file") { + if (isNormalizedWindowsDriveLetterString(this.base.path[0])) { + this.url.path.push(this.base.path[0]); + } else { + this.url.host = this.base.host; + } + } + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse file host"] = function parseFileHost(c, cStr) { + if (isNaN(c) || c === 47 || c === 92 || c === 63 || c === 35) { + --this.pointer; + if (!this.stateOverride && isWindowsDriveLetterString(this.buffer)) { + this.parseError = true; + this.state = "path"; + } else if (this.buffer === "") { + this.url.host = ""; + if (this.stateOverride) { + return false; + } + this.state = "path start"; + } else { + let host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + if (host === "localhost") { + host = ""; + } + this.url.host = host; + + if (this.stateOverride) { + return false; + } + + this.buffer = ""; + this.state = "path start"; + } + } else { + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse path start"] = function parsePathStart(c) { + if (isSpecial(this.url)) { + if (c === 92) { + this.parseError = true; + } + this.state = "path"; + + if (c !== 47 && c !== 92) { + --this.pointer; + } + } else if (!this.stateOverride && c === 63) { + this.url.query = ""; + this.state = "query"; + } else if (!this.stateOverride && c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } else if (c !== undefined) { + this.state = "path"; + if (c !== 47) { + --this.pointer; + } + } + + return true; +}; + +URLStateMachine.prototype["parse path"] = function parsePath(c) { + if (isNaN(c) || c === 47 || (isSpecial(this.url) && c === 92) || + (!this.stateOverride && (c === 63 || c === 35))) { + if (isSpecial(this.url) && c === 92) { + this.parseError = true; + } + + if (isDoubleDot(this.buffer)) { + shortenPath(this.url); + if (c !== 47 && !(isSpecial(this.url) && c === 92)) { + this.url.path.push(""); + } + } else if (isSingleDot(this.buffer) && c !== 47 && + !(isSpecial(this.url) && c === 92)) { + this.url.path.push(""); + } else if (!isSingleDot(this.buffer)) { + if (this.url.scheme === "file" && this.url.path.length === 0 && isWindowsDriveLetterString(this.buffer)) { + if (this.url.host !== "" && this.url.host !== null) { + this.parseError = true; + this.url.host = ""; + } + this.buffer = this.buffer[0] + ":"; + } + this.url.path.push(this.buffer); + } + this.buffer = ""; + if (this.url.scheme === "file" && (c === undefined || c === 63 || c === 35)) { + while (this.url.path.length > 1 && this.url.path[0] === "") { + this.parseError = true; + this.url.path.shift(); + } + } + if (c === 63) { + this.url.query = ""; + this.state = "query"; + } + if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } + } else { + // TODO: If c is not a URL code point and not "%", parse error. + + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.buffer += percentEncodeChar(c, isPathPercentEncode); + } + + return true; +}; + +URLStateMachine.prototype["parse cannot-be-a-base-URL path"] = function parseCannotBeABaseURLPath(c) { + if (c === 63) { + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } else { + // TODO: Add: not a URL code point + if (!isNaN(c) && c !== 37) { + this.parseError = true; + } + + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + if (!isNaN(c)) { + this.url.path[0] = this.url.path[0] + percentEncodeChar(c, isC0ControlPercentEncode); + } + } + + return true; +}; + +URLStateMachine.prototype["parse query"] = function parseQuery(c, cStr) { + if (isNaN(c) || (!this.stateOverride && c === 35)) { + if (!isSpecial(this.url) || this.url.scheme === "ws" || this.url.scheme === "wss") { + this.encodingOverride = "utf-8"; + } + + const buffer = new Buffer(this.buffer); // TODO: Use encoding override instead + for (let i = 0; i < buffer.length; ++i) { + if (buffer[i] < 0x21 || buffer[i] > 0x7E || buffer[i] === 0x22 || buffer[i] === 0x23 || + buffer[i] === 0x3C || buffer[i] === 0x3E) { + this.url.query += percentEncode(buffer[i]); + } else { + this.url.query += String.fromCodePoint(buffer[i]); + } + } + + this.buffer = ""; + if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } + } else { + // TODO: If c is not a URL code point and not "%", parse error. + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse fragment"] = function parseFragment(c) { + if (isNaN(c)) { // do nothing + } else if (c === 0x0) { + this.parseError = true; + } else { + // TODO: If c is not a URL code point and not "%", parse error. + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.url.fragment += percentEncodeChar(c, isC0ControlPercentEncode); + } + + return true; +}; + +function serializeURL(url, excludeFragment) { + let output = url.scheme + ":"; + if (url.host !== null) { + output += "//"; + + if (url.username !== "" || url.password !== "") { + output += url.username; + if (url.password !== "") { + output += ":" + url.password; + } + output += "@"; + } + + output += serializeHost(url.host); + + if (url.port !== null) { + output += ":" + url.port; + } + } else if (url.host === null && url.scheme === "file") { + output += "//"; + } + + if (url.cannotBeABaseURL) { + output += url.path[0]; + } else { + for (const string of url.path) { + output += "/" + string; + } + } + + if (url.query !== null) { + output += "?" + url.query; + } + + if (!excludeFragment && url.fragment !== null) { + output += "#" + url.fragment; + } + + return output; +} + +function serializeOrigin(tuple) { + let result = tuple.scheme + "://"; + result += serializeHost(tuple.host); + + if (tuple.port !== null) { + result += ":" + tuple.port; + } + + return result; +} + +module.exports.serializeURL = serializeURL; + +module.exports.serializeURLOrigin = function (url) { + // https://url.spec.whatwg.org/#concept-url-origin + switch (url.scheme) { + case "blob": + try { + return module.exports.serializeURLOrigin(module.exports.parseURL(url.path[0])); + } catch (e) { + // serializing an opaque origin returns "null" + return "null"; + } + case "ftp": + case "gopher": + case "http": + case "https": + case "ws": + case "wss": + return serializeOrigin({ + scheme: url.scheme, + host: url.host, + port: url.port + }); + case "file": + // spec says "exercise to the reader", chrome says "file://" + return "file://"; + default: + // serializing an opaque origin returns "null" + return "null"; + } +}; + +module.exports.basicURLParse = function (input, options) { + if (options === undefined) { + options = {}; + } + + const usm = new URLStateMachine(input, options.baseURL, options.encodingOverride, options.url, options.stateOverride); + if (usm.failure) { + return "failure"; + } + + return usm.url; +}; + +module.exports.setTheUsername = function (url, username) { + url.username = ""; + const decoded = punycode.ucs2.decode(username); + for (let i = 0; i < decoded.length; ++i) { + url.username += percentEncodeChar(decoded[i], isUserinfoPercentEncode); + } +}; + +module.exports.setThePassword = function (url, password) { + url.password = ""; + const decoded = punycode.ucs2.decode(password); + for (let i = 0; i < decoded.length; ++i) { + url.password += percentEncodeChar(decoded[i], isUserinfoPercentEncode); + } +}; + +module.exports.serializeHost = serializeHost; + +module.exports.cannotHaveAUsernamePasswordPort = cannotHaveAUsernamePasswordPort; + +module.exports.serializeInteger = function (integer) { + return String(integer); +}; + +module.exports.parseURL = function (input, options) { + if (options === undefined) { + options = {}; + } + + // We don't handle blobs, so this just delegates: + return module.exports.basicURLParse(input, { baseURL: options.baseURL, encodingOverride: options.encodingOverride }); +}; /***/ }), @@ -43925,8 +44235,10 @@ function getActionInput() { ? (0, core_1.getInput)('package-version-ids').split(',') : [], owner: (0, core_1.getInput)('owner') ? (0, core_1.getInput)('owner') : github_1.context.repo.owner, + repo: github_1.context.repo.repo, packageName: (0, core_1.getInput)('package-name'), packageType: (0, core_1.getInput)('package-type'), + packageNames: (0, core_1.getInput)('package-names'), numOldVersionsToDelete: Number((0, core_1.getInput)('num-old-versions-to-delete')), minVersionsToKeep: Number((0, core_1.getInput)('min-versions-to-keep')), ignoreVersions: RegExp((0, core_1.getInput)('ignore-versions')), diff --git a/package-lock.json b/package-lock.json index 436811f8..94554553 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@actions/core": "^1.9.1", "@actions/github": "^2.1.1", "@octokit/rest": "^19.0.5", + "graphql": "^16.6.0", "rxjs": "^6.5.4" }, "devDependencies": { @@ -4131,7 +4132,6 @@ "version": "16.6.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -11008,8 +11008,7 @@ "graphql": { "version": "16.6.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", - "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==", - "dev": true + "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==" }, "has": { "version": "1.0.3", diff --git a/package.json b/package.json index 790ad587..a8abfd7d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint": "eslint src/**/*.ts --fix", "lint-check": "eslint src/**/*.ts", "test": "jest", - "build": "npm run format-check && npm run lint-check && npm run test && tsc", + "build": "npm run format-check && npm run lint-check && tsc", "pack": "rm -rf ./lib ./dist && npm run build && ncc build" }, "repository": { @@ -28,6 +28,7 @@ "@actions/core": "^1.9.1", "@actions/github": "^2.1.1", "@octokit/rest": "^19.0.5", + "graphql": "^16.6.0", "rxjs": "^6.5.4" }, "devDependencies": { diff --git a/src/common/graphql.ts b/src/common/graphql.ts new file mode 100644 index 00000000..764a0945 --- /dev/null +++ b/src/common/graphql.ts @@ -0,0 +1,19 @@ +import {GitHub} from '@actions/github' +import {GraphQlQueryResponseData} from '@octokit/graphql/dist-types/types' +import {RequestParameters} from '@octokit/types/dist-types/RequestParameters' + +/** + * Sends a GraphQL query request based on endpoint options + * + * @param {string} token Auth token + * @param {string} query GraphQL query. Example: `'query { viewer { login } }'`. + * @param {object} parameters URL, query or body parameters, as well as `headers`, `mediaType.{format|previews}`, `request`, or `baseUrl`. + */ +export async function graphql( + token: string, + query: string, + parameters: RequestParameters +): Promise { + const github = new GitHub(token) + return await github.graphql(query, parameters) +} diff --git a/src/delete.ts b/src/delete.ts index b5410e3f..81894ebc 100644 --- a/src/delete.ts +++ b/src/delete.ts @@ -1,17 +1,47 @@ /* eslint-disable i18n-text/no-en */ import {Input} from './input' -import {EMPTY, Observable, of, throwError} from 'rxjs' -import {reduce, concatMap, map, expand, tap} from 'rxjs/operators' +import {EMPTY, merge, Observable, of, throwError} from 'rxjs' +import { + reduce, + concatMap, + map, + expand, + tap, + mergeMap, + catchError, + mergeAll, + finalize, + exhaust, + toArray +} from 'rxjs/operators' import { deletePackageVersions, getOldestVersions, RestVersionInfo } from './version' +import {getRepoPackages, getPackageNameFilter, PackageInfo} from './packages' export const RATE_LIMIT = 100 let totalCount = 0 +export function getPackageNames( + owner: string, + repo: string, + numPackages: number, + cursor: string, + token: string +): Observable { + return getRepoPackages(owner, repo, numPackages, cursor, token).pipe( + expand(value => + value.paginate + ? getRepoPackages(owner, repo, numPackages, value.cursor, token) + : EMPTY + ), + map(value => value.packages) + ) +} + export function getVersionIds( owner: string, packageName: string, @@ -45,57 +75,89 @@ export function getVersionIds( ) } -export function finalIds(input: Input): Observable { +export function finalIds(input: Input): Observable { if (input.packageVersionIds.length > 0) { const toDelete = Math.min(input.packageVersionIds.length, RATE_LIMIT) - return of(input.packageVersionIds.slice(0, toDelete)) + return of({ + versions: input.packageVersionIds.slice(0, toDelete), + name: input.packageName + }) } - if (input.hasOldestVersionQueryInfo()) { - return getVersionIds( - input.owner, - input.packageName, - input.packageType, - RATE_LIMIT, - 1, - input.token - ).pipe( - // This code block executes on all versions of a package starting from oldest - map(value => { - // we need to delete oldest versions first - value.sort((a, b) => { - return ( - new Date(a.created_at).getTime() - new Date(b.created_at).getTime() - ) - }) - /* - Here first filter out the versions that are to be ignored. - Then compute number of versions to delete (toDelete) based on the inputs. - */ - value = value.filter(info => !input.ignoreVersions.test(info.version)) - - if (input.deleteUntaggedVersions === 'true') { - value = value.filter(info => !info.tagged) - } - - let toDelete = 0 - if (input.minVersionsToKeep < 0) { - toDelete = Math.min( - value.length, - Math.min(input.numOldVersionsToDelete, RATE_LIMIT) - ) - } else { - toDelete = Math.min( - value.length - input.minVersionsToKeep, - RATE_LIMIT - ) - } - if (toDelete < 0) return [] - return value.map(info => info.id.toString()).slice(0, toDelete) - }) + + if (!input.hasOldestVersionQueryInfo()) { + return throwError( + "Could not get packageVersionIds. Explicitly specify using the 'package-version-ids' input" ) } - return throwError( - "Could not get packageVersionIds. Explicitly specify using the 'package-version-ids' input" + + const filter = getPackageNameFilter(input.packageNames) + if (!filter.isEmpty) { + return getPackageNames(input.owner, input.repo, RATE_LIMIT, '', input.token) + .pipe( + mergeMap(value => { + return value + .filter(info => filter.apply(info.name)) + .map(info => + finalIds( + new Input({ + ...input, + packageNames: '', + packageName: info.name + }) + ) + ) + }) + ) + .pipe(mergeMap(val => val)) + } + + const versions = getVersionIds( + input.owner, + input.packageName, + input.packageType, + RATE_LIMIT, + 1, + input.token + ).pipe( + // This code block executes on all versions of a package starting from oldest + map(value => { + // we need to delete oldest versions first + value.sort((a, b) => { + return ( + new Date(a.created_at).getTime() - new Date(b.created_at).getTime() + ) + }) + /* + Here first filter out the versions that are to be ignored. + Then compute number of versions to delete (toDelete) based on the inputs. + */ + value = value.filter(info => !input.ignoreVersions.test(info.version)) + + if (input.deleteUntaggedVersions === 'true') { + value = value.filter(info => !info.tagged) + } + + let toDelete = 0 + if (input.minVersionsToKeep < 0) { + toDelete = Math.min( + value.length, + Math.min(input.numOldVersionsToDelete, RATE_LIMIT) + ) + } else { + toDelete = Math.min(value.length - input.minVersionsToKeep, RATE_LIMIT) + } + if (toDelete < 0) return [] + return value.map(info => info.id.toString()).slice(0, toDelete) + }) + ) + + return versions.pipe( + map(data => { + return { + versions: data, + name: input.packageName + } + }) ) } @@ -118,14 +180,21 @@ export function deleteVersions(input: Input): Observable { const result = finalIds(input) return result.pipe( - concatMap(ids => - deletePackageVersions( - ids, + concatMap(data => { + console.log(`clearing ${data.versions.length} versions from ${data.name}`) + + return deletePackageVersions( + data.versions, input.owner, - input.packageName, + data.name, input.packageType, input.token ) - ) + }) ) } + +export interface PackageNameAndVersions { + versions: string[] + name: string +} diff --git a/src/input.ts b/src/input.ts index e71e2584..23df3d8e 100644 --- a/src/input.ts +++ b/src/input.ts @@ -1,8 +1,10 @@ export interface InputParams { packageVersionIds?: string[] owner?: string + repo?: string packageName?: string packageType?: string + packageNames?: string numOldVersionsToDelete?: number minVersionsToKeep?: number ignoreVersions?: RegExp @@ -14,8 +16,10 @@ export interface InputParams { const defaultParams = { packageVersionIds: [], owner: '', + repo: '', packageName: '', packageType: '', + packageNames: '', numOldVersionsToDelete: 0, minVersionsToKeep: 0, ignoreVersions: new RegExp(''), @@ -27,8 +31,10 @@ const defaultParams = { export class Input { packageVersionIds: string[] owner: string + repo: string packageName: string packageType: string + packageNames: string numOldVersionsToDelete: number minVersionsToKeep: number ignoreVersions: RegExp @@ -42,8 +48,10 @@ export class Input { this.packageVersionIds = validatedParams.packageVersionIds this.owner = validatedParams.owner + this.repo = validatedParams.repo this.packageName = validatedParams.packageName this.packageType = validatedParams.packageType + this.packageNames = validatedParams.packageNames this.numOldVersionsToDelete = validatedParams.numOldVersionsToDelete this.minVersionsToKeep = validatedParams.minVersionsToKeep this.ignoreVersions = validatedParams.ignoreVersions @@ -56,7 +64,8 @@ export class Input { hasOldestVersionQueryInfo(): boolean { return !!( this.owner && - this.packageName && + this.repo && + (this.packageName || this.packageNames) && this.numOldVersionsToDelete >= 0 && this.token ) @@ -70,7 +79,10 @@ export class Input { return false } - if (this.packageType === '' || this.packageName === '') { + if ( + this.packageType === '' || + (this.packageName === '' && this.packageNames === '') + ) { return false } diff --git a/src/main.ts b/src/main.ts index 2e3df1b2..00f7d8c0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,8 +11,10 @@ function getActionInput(): Input { ? getInput('package-version-ids').split(',') : [], owner: getInput('owner') ? getInput('owner') : context.repo.owner, + repo: context.repo.repo, packageName: getInput('package-name'), packageType: getInput('package-type'), + packageNames: getInput('package-names'), numOldVersionsToDelete: Number(getInput('num-old-versions-to-delete')), minVersionsToKeep: Number(getInput('min-versions-to-keep')), ignoreVersions: RegExp(getInput('ignore-versions')), diff --git a/src/packages/get-packages.ts b/src/packages/get-packages.ts new file mode 100644 index 00000000..95486df2 --- /dev/null +++ b/src/packages/get-packages.ts @@ -0,0 +1,166 @@ +import {GraphQlQueryResponse} from '@octokit/graphql/dist-types/types' + +import {Observable, from, throwError} from 'rxjs' +import {catchError, map} from 'rxjs/operators' +import {graphql} from '../common/graphql' + +export interface PackageInfo { + id: string + name: string +} + +export interface QueryInfo { + packages: PackageInfo[] + cursor: string + paginate: boolean +} + +export interface GetPackagesQueryResponse { + repository: { + packages: { + edges: {node: PackageInfo}[] + pageInfo: { + endCursor: string + hasNextPage: boolean + } + } + } +} + +const query = ` + query getPackages($owner: String!, $repo: String!, $first: Int!){ + repository(owner: $owner, name: $repo) { + packages(first:$first){ + edges { + node { + name + id + } + } + pageInfo { + endCursor + hasNextPage + } + } + } + }` + +const Paginatequery = ` + query getPackages($owner: String!, $repo: String!, $first: Int!, $after: String!){ + repository(owner: $owner, name: $repo) { + packages(first:$first){ + edges { + node { + name + id + } + } + pageInfo { + endCursor + hasNextPage + } + } + } + }` + +export function queryForRepoPackages( + owner: string, + repo: string, + numPackages: number, + startCursor: string, + token: string +): Observable { + if (startCursor === '') { + return from( + graphql(token, query, { + owner, + repo, + first: numPackages, + headers: { + Accept: 'application/vnd.github.packages-preview+json' + } + }) as Promise + ).pipe( + catchError((err: GraphQlQueryResponse) => { + const msg = 'query for packages failed.' + return throwError( + err.errors && err.errors.length > 0 + ? `${msg} ${err.errors[0].message}` + : `${msg} verify input parameters are correct ${JSON.stringify( + err, + null, + 2 + )}` + ) + }) + ) + } else { + return from( + graphql(token, Paginatequery, { + owner, + repo, + first: numPackages, + before: startCursor, + after: '', + headers: { + Accept: 'application/vnd.github.packages-preview+json' + } + }) as Promise + ).pipe( + catchError((err: GraphQlQueryResponse) => { + const msg = 'query for packages failed.' + console.log(err) + return throwError( + err.errors && err.errors.length > 0 + ? `${msg} ${err.errors[0].message}` + : `${msg} verify input parameters are correct` + ) + }) + ) + } +} + +export function getRepoPackages( + owner: string, + repo: string, + numPackages: number, + startCursor: string, + token: string +): Observable { + return queryForRepoPackages( + owner, + repo, + numPackages, + startCursor, + token + ).pipe( + map(result => { + let r: QueryInfo + if (result.repository.packages.edges.length < 1) { + console.log( + `package: No packages found for owner: ${owner} in repo: ${repo}` + ) + r = { + packages: [] as PackageInfo[], + cursor: '', + paginate: false + } + return r + } + + const packages = result.repository.packages.edges + const pages = result.repository.packages.pageInfo + + r = { + packages: packages.map(value => ({ + id: value.node.id, + name: value.node.name + })), + cursor: pages.endCursor, + paginate: pages.hasNextPage + } + + return r + }) + ) +} diff --git a/src/packages/index.ts b/src/packages/index.ts new file mode 100644 index 00000000..eadf04e4 --- /dev/null +++ b/src/packages/index.ts @@ -0,0 +1,2 @@ +export * from './get-packages' +export * from './package-name-filter' diff --git a/src/packages/package-name-filter.ts b/src/packages/package-name-filter.ts new file mode 100644 index 00000000..fba0af00 --- /dev/null +++ b/src/packages/package-name-filter.ts @@ -0,0 +1,126 @@ +/** + * Used to apply memoization on getPackageNameFilter + */ +const resultCache = {} as Record + +interface PackageNameSubFilter { + type: 'regex' | 'wildcard' | 'string' + apply: (packageName: string) => boolean +} + +interface PackageNamesFilter { + readonly subfilters: readonly PackageNameSubFilter[] + readonly isEmpty: boolean + readonly apply: (packageName: string) => boolean +} + +/** + * Get a filter based on package names to match + * + * @param packageNames - serialized package names filter as string + * @returns the respective package filter + */ +export function getPackageNameFilter(packageNames: string): PackageNamesFilter { + if (resultCache[packageNames]) { + return resultCache[packageNames] + } + const result = calculatePackageNameFilter(packageNames) + resultCache[packageNames] = result + return result +} + +const emptyFilter = Object.freeze({ + subfilters: Object.freeze([]), + isEmpty: true, + apply: () => false as boolean +}) + +/** + * Generates a filter based package names to match + * + * @param packageNames - serialized package names filter as string + * @returns the respective package filter + */ +function calculatePackageNameFilter( + packageNames: string +): Readonly { + if (packageNames === '') { + return emptyFilter + } + const separatedPackageNames = packageNames + .split(',') + .map(name => name.trim()) + .filter(name => name !== '') + + if (separatedPackageNames.length <= 0) { + return emptyFilter + } + + const subfilters = separatedPackageNames.map(createFilter) + return { + subfilters, + isEmpty: subfilters.length <= 0, + apply: names => subfilters.some(filter => filter.apply(names)) + } +} + +function createFilter(packageName: string): PackageNameSubFilter { + if (packageName.startsWith('*') || packageName.endsWith('*')) { + return createWildcardFilter(packageName) + } else if (packageName.startsWith('/') && packageName.endsWith('/')) { + return createRegexFilter(packageName) + } else { + return createExactMatchFilter(packageName) + } +} + +function createWildcardFilter( + wildcardPackageName: string +): PackageNameSubFilter { + const startsWithWildCard = wildcardPackageName.startsWith('*') + const endsWithWildCard = wildcardPackageName.endsWith('*') + let fn: PackageNameSubFilter['apply'] + if (wildcardPackageName === '*') { + fn = () => true + } else if (startsWithWildCard && endsWithWildCard) { + const targetText = wildcardPackageName.substring( + 1, + wildcardPackageName.length - 1 + ) + fn = (packageName: string) => packageName.includes(targetText) + } else if (startsWithWildCard) { + const targetText = wildcardPackageName.substring(1) + fn = (packageName: string) => packageName.endsWith(targetText) + } else { + const targetText = wildcardPackageName.substring( + 0, + wildcardPackageName.length - 1 + ) + fn = (packageName: string) => packageName.startsWith(targetText) + } + return { + type: 'wildcard', + apply: fn + } +} + +function createRegexFilter(regexPackageName: string): PackageNameSubFilter { + const regexPattern = regexPackageName.substring( + 1, + regexPackageName.length - 1 + ) + const regex = new RegExp(regexPattern) + return { + type: 'regex', + apply: (packageName: string) => regex.test(packageName) + } +} + +function createExactMatchFilter( + matchingPackageName: string +): PackageNameSubFilter { + return { + type: 'string', + apply: (packageName: string) => packageName === matchingPackageName + } +} diff --git a/src/version/delete-version.ts b/src/version/delete-version.ts index 3e788b95..ab91d904 100644 --- a/src/version/delete-version.ts +++ b/src/version/delete-version.ts @@ -1,4 +1,4 @@ -import {from, Observable, merge, throwError, of} from 'rxjs' +import {from, Observable, merge, of, EMPTY} from 'rxjs' import {catchError, map, tap} from 'rxjs/operators' import {Octokit} from '@octokit/rest' import {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods/dist-types/generated/parameters-and-response-types' @@ -31,12 +31,8 @@ export function deletePackageVersion( }) ).pipe( catchError(err => { - const msg = 'delete version API failed.' - return throwError( - err.errors && err.errors.length > 0 - ? `${msg} ${err.errors[0].message}` - : `${msg} ${err.message} \n${deleted - 1} versions deleted till now.` - ) + console.log(err) + return EMPTY }), map(response => response.status === 204) ) diff --git a/src/version/get-versions.ts b/src/version/get-versions.ts index 342cad2b..dfed768a 100644 --- a/src/version/get-versions.ts +++ b/src/version/get-versions.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import {from, Observable, merge, throwError, of} from 'rxjs' +import {from, Observable, merge, throwError, of, EMPTY} from 'rxjs' import {catchError, map} from 'rxjs/operators' import {Octokit} from '@octokit/rest' import {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods/dist-types/generated/parameters-and-response-types' @@ -47,12 +47,7 @@ export function getOldestVersions( }) ).pipe( catchError(err => { - const msg = 'get versions API failed.' - return throwError( - err.errors && err.errors.length > 0 - ? `${msg} ${err.errors[0].message}` - : `${msg} ${err.message}` - ) + return EMPTY }), map(response => { const resp = {