diff --git a/__tests__/fetch.test.ts b/__tests__/fetch.test.ts new file mode 100644 index 00000000..0e1881f4 --- /dev/null +++ b/__tests__/fetch.test.ts @@ -0,0 +1,39 @@ +import * as fetch from '../src/fetch'; +import nock = require('nock'); + +it('checking fetch', async () => { + const host_url = 'https://example.com'; + const manifest_url = host_url + '/manifest'; + const ping_url = host_url + '/ping'; + + nock(host_url) + .get('/manifest') + .reply(200, {latest: 'latest'}) + .get('/manifest', '', { + reqheaders: {authorization: 'Bearer invalid_token'} + }) + .reply(401, {error: '401: Unauthorized'}) + .get('/ping') + .twice() + .reply(301, undefined, { + Location: host_url + '/pong' + }) + .get('/pong') + .reply(200, 'pong'); + + let response: Record = await fetch.fetch(manifest_url); + expect(response.error).toBe(undefined); + expect(response.data).toContain('latest'); + + response = await fetch.fetch(ping_url, '', 1); + expect(response.error).toBe(undefined); + expect(response.data).toContain('pong'); + + response = await fetch.fetch(ping_url, '', 0); + expect(response.error).toBe('301: Redirect error'); + expect(response.data).toBe(undefined); + + response = await fetch.fetch(manifest_url, 'invalid_token'); + expect(response.error).not.toBe(undefined); + expect(response.data).toBe(undefined); +}); diff --git a/__tests__/install.test.ts b/__tests__/install.test.ts index eef91409..f7051dd7 100644 --- a/__tests__/install.test.ts +++ b/__tests__/install.test.ts @@ -33,6 +33,15 @@ jest.mock('../src/install', () => ({ }) })); +/** + * Mock fetch.ts + */ +jest.mock('../src/fetch', () => ({ + fetch: jest.fn().mockImplementation(() => { + return {data: '{ "latest": "8.1", "5.x": "5.6" }'}; + }) +})); + describe('Install', () => { it.each` version | os | extension_csv | ini_file | ini_values_csv | coverage_driver | tools | output diff --git a/__tests__/tools.test.ts b/__tests__/tools.test.ts index 005af27c..e582bd89 100644 --- a/__tests__/tools.test.ts +++ b/__tests__/tools.test.ts @@ -1,5 +1,4 @@ import * as tools from '../src/tools'; -import * as utils from '../src/utils'; interface IData { tool: string; @@ -38,29 +37,34 @@ function getData(data: IData): Record { }; } -jest - .spyOn(utils, 'fetch') - .mockImplementation( - async (url: string, token?: string): Promise> => { - if (url.includes('atom') && !url.includes('no-')) { - return { - data: '"releases/tag/1.2.3", "releases/tag/3.2.1", "releases/tag/2.3.1"' - }; - } else if (url.includes('no-data')) { - return {}; - } else if (url.includes('no-release')) { - return {data: 'no-release'}; - } else if (!token || token === 'valid_token') { - return {data: `[{"ref": "refs/tags/1.2.3", "url": "${url}"}]`}; - } else if (token === 'beta_token') { - return {data: `[{"ref": "refs/tags/1.2.3-beta1", "url": "${url}"}]`}; - } else if (token === 'no_data') { - return {data: '[]'}; - } else { - return {error: 'Invalid token'}; +/** + * Mock fetch.ts + */ +jest.mock('../src/fetch', () => ({ + fetch: jest + .fn() + .mockImplementation( + async (url: string, token?: string): Promise> => { + if (url.includes('atom') && !url.includes('no-')) { + return { + data: '"releases/tag/1.2.3", "releases/tag/3.2.1", "releases/tag/2.3.1"' + }; + } else if (url.includes('no-data')) { + return {}; + } else if (url.includes('no-release')) { + return {data: 'no-release'}; + } else if (!token || token === 'valid_token') { + return {data: `[{"ref": "refs/tags/1.2.3", "url": "${url}"}]`}; + } else if (token === 'beta_token') { + return {data: `[{"ref": "refs/tags/1.2.3-beta1", "url": "${url}"}]`}; + } else if (token === 'no_data') { + return {data: '[]'}; + } else { + return {error: 'Invalid token'}; + } } - } - ); + ) +})); describe('Tools tests', () => { it.each` diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts index f2405012..be24803d 100644 --- a/__tests__/utils.test.ts +++ b/__tests__/utils.test.ts @@ -1,12 +1,24 @@ import * as path from 'path'; import * as utils from '../src/utils'; +/** + * Mock @actions/core + */ jest.mock('@actions/core', () => ({ getInput: jest.fn().mockImplementation(key => { return ['setup-php'].indexOf(key) !== -1 ? key : ''; }) })); +/** + * Mock fetch.ts + */ +jest.mock('../src/fetch', () => ({ + fetch: jest.fn().mockImplementation(() => { + return {data: '{ "latest": "8.1", "5.x": "5.6" }'}; + }) +})); + describe('Utils tests', () => { it('checking readEnv', async () => { process.env['test'] = 'setup-php'; @@ -28,33 +40,11 @@ describe('Utils tests', () => { }).rejects.toThrow('Input required and not supplied: DoesNotExist'); }); - it('checking fetch', async () => { - const manifest = await utils.getManifestURL(); - let response: Record = await utils.fetch(manifest); - expect(response.error).toBe(undefined); - expect(response.data).toContain('latest'); - - response = await utils.fetch(manifest, 'invalid_token'); - expect(response.error).not.toBe(undefined); - expect(response.data).toBe(undefined); - }); - it('checking getManifestURL', async () => { expect(await utils.getManifestURL()).toContain('php-versions.json'); }); it('checking parseVersion', async () => { - jest - .spyOn(utils, 'fetch') - .mockImplementation( - async (url, token?): Promise> => { - if (!token || token === 'valid_token') { - return {data: `{ "latest": "8.0", "5.x": "5.6", "url": "${url}" }`}; - } else { - return {error: 'Invalid token'}; - } - } - ); expect(await utils.parseVersion('latest')).toBe('8.1'); expect(await utils.parseVersion('7')).toBe('7.0'); expect(await utils.parseVersion('7.4')).toBe('7.4'); diff --git a/dist/index.js b/dist/index.js index 080037e4..107719d8 100644 --- a/dist/index.js +++ b/dist/index.js @@ -407,6 +407,76 @@ exports.addExtension = addExtension; /***/ }), +/***/ 387: +/***/ (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; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.fetch = void 0; +const https = __importStar(__nccwpck_require__(687)); +const url = __importStar(__nccwpck_require__(310)); +async function fetch(input_url, auth_token, redirect_count = 5) { + const fetch_promise = new Promise(resolve => { + const url_object = new url.URL(input_url); + const headers = { + 'User-Agent': `Mozilla/5.0 (${process.platform} ${process.arch}) setup-php` + }; + if (auth_token) { + headers.authorization = 'Bearer ' + auth_token; + } + const options = { + hostname: url_object.hostname, + path: url_object.pathname, + headers: headers + }; + const req = https.get(options, (res) => { + if (res.statusCode === 200) { + let body = ''; + res.setEncoding('utf8'); + res.on('data', chunk => (body += chunk)); + res.on('end', () => resolve({ data: `${body}` })); + } + else if ([301, 302, 303, 307, 308].includes(res.statusCode)) { + if (redirect_count > 0 && res.headers.location) { + fetch(res.headers.location, auth_token, redirect_count--).then(resolve); + } + else { + resolve({ error: `${res.statusCode}: Redirect error` }); + } + } + else { + resolve({ error: `${res.statusCode}: ${res.statusMessage}` }); + } + }); + req.end(); + }); + return await fetch_promise; +} +exports.fetch = fetch; +//# sourceMappingURL=fetch.js.map + +/***/ }), + /***/ 39: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { @@ -519,15 +589,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.addTools = exports.functionRecord = exports.getData = exports.addWPCLI = exports.addPHPUnitTools = exports.addPhive = exports.addPhing = exports.addPECL = exports.addDevTools = exports.addDeployer = exports.addComposer = exports.addBlackfirePlayer = exports.addPackage = exports.addArchive = exports.getPharUrl = exports.getUrl = exports.filterList = exports.getRelease = exports.getVersion = exports.getLatestVersion = exports.getSemverVersion = void 0; -const utils = __importStar(__nccwpck_require__(918)); const path_1 = __importDefault(__nccwpck_require__(17)); const fs_1 = __importDefault(__nccwpck_require__(147)); +const fetch = __importStar(__nccwpck_require__(387)); +const utils = __importStar(__nccwpck_require__(918)); async function getSemverVersion(data) { var _a; const search = data['version_prefix'] + data['version']; const url = `https://api.github.com/repos/${data['repository']}/git/matching-refs/tags%2F${search}.`; const token = await utils.readEnv('COMPOSER_TOKEN'); - const response = await utils.fetch(url, token); + const response = await fetch.fetch(url, token); if (response.error || response.data === '[]') { data['error'] = (_a = response.error) !== null && _a !== void 0 ? _a : `No version found with prefix ${search}.`; return data['version']; @@ -544,7 +615,7 @@ async function getLatestVersion(data) { if (!data['version'] && data['fetch_latest'] === 'false') { return 'latest'; } - const resp = await utils.fetch(`${data['github']}/${data['repository']}/releases.atom`); + const resp = await fetch.fetch(`${data['github']}/${data['repository']}/releases.atom`); if (resp['data']) { const releases = [ ...resp['data'].matchAll(/releases\/tag\/([a-zA-Z]*)?(\d+.\d+.\d+)"/g) @@ -889,11 +960,10 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.parseExtensionSource = exports.customPackage = exports.scriptTool = exports.scriptExtension = exports.joins = exports.getCommand = exports.getUnsupportedLog = exports.suppressOutput = exports.getExtensionPrefix = exports.CSVArray = exports.extensionArray = exports.addLog = exports.stepLog = exports.log = exports.color = exports.asyncForEach = exports.parseIniFile = exports.parseVersion = exports.getManifestURL = exports.fetch = exports.getInput = exports.readEnv = void 0; -const https = __importStar(__nccwpck_require__(687)); +exports.parseExtensionSource = exports.customPackage = exports.scriptTool = exports.scriptExtension = exports.joins = exports.getCommand = exports.getUnsupportedLog = exports.suppressOutput = exports.getExtensionPrefix = exports.CSVArray = exports.extensionArray = exports.addLog = exports.stepLog = exports.log = exports.color = exports.asyncForEach = exports.parseIniFile = exports.parseVersion = exports.getManifestURL = exports.getInput = exports.readEnv = void 0; const path = __importStar(__nccwpck_require__(17)); -const url = __importStar(__nccwpck_require__(310)); const core = __importStar(__nccwpck_require__(186)); +const fetch = __importStar(__nccwpck_require__(387)); async function readEnv(property) { const property_lc = property.toLowerCase(); const property_uc = property.toUpperCase(); @@ -920,36 +990,6 @@ async function getInput(name, mandatory) { } } exports.getInput = getInput; -async function fetch(input_url, auth_token) { - const fetch_promise = new Promise(resolve => { - const url_object = new url.URL(input_url); - const headers = { - 'User-Agent': `Mozilla/5.0 (${process.platform} ${process.arch}) setup-php` - }; - if (auth_token) { - headers.authorization = 'Bearer ' + auth_token; - } - const options = { - hostname: url_object.hostname, - path: url_object.pathname, - headers: headers - }; - const req = https.get(options, (res) => { - if (res.statusCode != 200) { - resolve({ error: `${res.statusCode}: ${res.statusMessage}` }); - } - else { - let body = ''; - res.setEncoding('utf8'); - res.on('data', chunk => (body += chunk)); - res.on('end', () => resolve({ data: `${body}` })); - } - }); - req.end(); - }); - return await fetch_promise; -} -exports.fetch = fetch; async function getManifestURL() { return 'https://raw.githubusercontent.com/shivammathur/setup-php/develop/src/configs/php-versions.json'; } @@ -958,7 +998,7 @@ async function parseVersion(version) { const manifest = await getManifestURL(); switch (true) { case /^(latest|nightly|\d+\.x)$/.test(version): - return JSON.parse((await fetch(manifest))['data'])[version]; + return JSON.parse((await fetch.fetch(manifest))['data'])[version]; default: switch (true) { case version.length > 1: diff --git a/package-lock.json b/package-lock.json index 2b5a3e29..5b65779d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "^27.4.7", "jest-circus": "^27.4.6", + "nock": "^13.2.4", "prettier": "^2.5.1", "simple-git-hooks": "^2.7.0", "ts-jest": "^27.1.3", @@ -4312,6 +4313,12 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, "node_modules/json5": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", @@ -4389,6 +4396,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", + "dev": true + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4528,6 +4541,21 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node_modules/nock": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.4.tgz", + "integrity": "sha512-8GPznwxcPNCH/h8B+XZcKjYPXnUV5clOKCjAqyjsiqA++MpNx9E9+t8YPp0MbThO+KauRo7aZJ1WuIZmOrT2Ug==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash.set": "^4.3.2", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4935,6 +4963,15 @@ "node": ">= 6" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -9151,6 +9188,12 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, "json5": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", @@ -9210,6 +9253,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", + "dev": true + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -9321,6 +9370,18 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "nock": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.4.tgz", + "integrity": "sha512-8GPznwxcPNCH/h8B+XZcKjYPXnUV5clOKCjAqyjsiqA++MpNx9E9+t8YPp0MbThO+KauRo7aZJ1WuIZmOrT2Ug==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash.set": "^4.3.2", + "propagate": "^2.0.0" + } + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -9618,6 +9679,12 @@ "sisteransi": "^1.0.5" } }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", diff --git a/package.json b/package.json index 0d489a5a..ec4ff8cb 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "^27.4.7", "jest-circus": "^27.4.6", + "nock": "^13.2.4", "prettier": "^2.5.1", "simple-git-hooks": "^2.7.0", "ts-jest": "^27.1.3", diff --git a/src/fetch.ts b/src/fetch.ts new file mode 100644 index 00000000..cda53b99 --- /dev/null +++ b/src/fetch.ts @@ -0,0 +1,54 @@ +import {IncomingMessage, OutgoingHttpHeaders} from 'http'; +import * as https from 'https'; +import * as url from 'url'; + +/** + * Function to fetch an URL + * + * @param input_url + * @param auth_token + */ +export async function fetch( + input_url: string, + auth_token?: string, + redirect_count = 5 +): Promise> { + const fetch_promise: Promise> = new Promise( + resolve => { + const url_object: url.UrlObject = new url.URL(input_url); + const headers: OutgoingHttpHeaders = { + 'User-Agent': `Mozilla/5.0 (${process.platform} ${process.arch}) setup-php` + }; + if (auth_token) { + headers.authorization = 'Bearer ' + auth_token; + } + const options: https.RequestOptions = { + hostname: url_object.hostname, + path: url_object.pathname, + headers: headers + }; + const req = https.get(options, (res: IncomingMessage) => { + if (res.statusCode === 200) { + let body = ''; + res.setEncoding('utf8'); + res.on('data', chunk => (body += chunk)); + res.on('end', () => resolve({data: `${body}`})); + } else if ( + [301, 302, 303, 307, 308].includes(res.statusCode as number) + ) { + if (redirect_count > 0 && res.headers.location) { + fetch(res.headers.location, auth_token, redirect_count--).then( + resolve + ); + } else { + resolve({error: `${res.statusCode}: Redirect error`}); + } + } else { + resolve({error: `${res.statusCode}: ${res.statusMessage}`}); + } + }); + req.end(); + } + ); + return await fetch_promise; +} diff --git a/src/tools.ts b/src/tools.ts index 90d1c1ec..d321d2a5 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -1,6 +1,7 @@ -import * as utils from './utils'; import path from 'path'; import fs from 'fs'; +import * as fetch from './fetch'; +import * as utils from './utils'; type RS = Record; type RSRS = Record; @@ -21,7 +22,7 @@ export async function getSemverVersion(data: RS): Promise { const search: string = data['version_prefix'] + data['version']; const url = `https://api.github.com/repos/${data['repository']}/git/matching-refs/tags%2F${search}.`; const token: string = await utils.readEnv('COMPOSER_TOKEN'); - const response: RS = await utils.fetch(url, token); + const response: RS = await fetch.fetch(url, token); if (response.error || response.data === '[]') { data['error'] = response.error ?? `No version found with prefix ${search}.`; return data['version']; @@ -42,7 +43,7 @@ export async function getLatestVersion(data: RS): Promise { if (!data['version'] && data['fetch_latest'] === 'false') { return 'latest'; } - const resp: Record = await utils.fetch( + const resp: Record = await fetch.fetch( `${data['github']}/${data['repository']}/releases.atom` ); if (resp['data']) { diff --git a/src/utils.ts b/src/utils.ts index ba10afcf..f654ed25 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,6 @@ -import {IncomingMessage, OutgoingHttpHeaders} from 'http'; -import * as https from 'https'; import * as path from 'path'; -import * as url from 'url'; import * as core from '@actions/core'; +import * as fetch from './fetch'; /** * Function to read environment variable and return a string value. @@ -46,46 +44,6 @@ export async function getInput( } } -/** - * Function to fetch an URL - * - * @param input_url - * @param auth_token - */ -export async function fetch( - input_url: string, - auth_token?: string -): Promise> { - const fetch_promise: Promise> = new Promise( - resolve => { - const url_object: url.UrlObject = new url.URL(input_url); - const headers: OutgoingHttpHeaders = { - 'User-Agent': `Mozilla/5.0 (${process.platform} ${process.arch}) setup-php` - }; - if (auth_token) { - headers.authorization = 'Bearer ' + auth_token; - } - const options: https.RequestOptions = { - hostname: url_object.hostname, - path: url_object.pathname, - headers: headers - }; - const req = https.get(options, (res: IncomingMessage) => { - if (res.statusCode != 200) { - resolve({error: `${res.statusCode}: ${res.statusMessage}`}); - } else { - let body = ''; - res.setEncoding('utf8'); - res.on('data', chunk => (body += chunk)); - res.on('end', () => resolve({data: `${body}`})); - } - }); - req.end(); - } - ); - return await fetch_promise; -} - /** Function to get manifest URL * */ @@ -102,7 +60,7 @@ export async function parseVersion(version: string): Promise { const manifest = await getManifestURL(); switch (true) { case /^(latest|nightly|\d+\.x)$/.test(version): - return JSON.parse((await fetch(manifest))['data'])[version]; + return JSON.parse((await fetch.fetch(manifest))['data'])[version]; default: switch (true) { case version.length > 1: