diff options
Diffstat (limited to 'remote/test/puppeteer/utils')
24 files changed, 3584 insertions, 0 deletions
diff --git a/remote/test/puppeteer/utils/bisect.js b/remote/test/puppeteer/utils/bisect.js new file mode 100755 index 0000000000..bbec3d2431 --- /dev/null +++ b/remote/test/puppeteer/utils/bisect.js @@ -0,0 +1,314 @@ +#!/usr/bin/env node +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const URL = require('url'); +const debug = require('debug'); +const pptr = require('..'); +const browserFetcher = pptr.createBrowserFetcher(); +const path = require('path'); +const fs = require('fs'); +const {fork, spawn, execSync} = require('child_process'); + +const COLOR_RESET = '\x1b[0m'; +const COLOR_RED = '\x1b[31m'; +const COLOR_GREEN = '\x1b[32m'; +const COLOR_YELLOW = '\x1b[33m'; + +const argv = require('minimist')(process.argv.slice(2), {}); + +const help = ` +Usage: + node bisect.js --good <revision> --bad <revision> <script> + +Parameters: + --good revision that is known to be GOOD, defaults to the chromium revision in src/revision.ts in the main branch + --bad revision that is known to be BAD, defaults to the chromium revision in src/revision.ts + --no-cache do not keep downloaded Chromium revisions + --unit-test pattern that identifies a unit tests that should be checked + --script path to a script that returns non-zero code for BAD revisions and 0 for good + +Example: + node utils/bisect.js --unit-test test + node utils/bisect.js --good 577361 --bad 599821 --script simple.js + node utils/bisect.js --good 577361 --bad 599821 --unit-test test +`; + +if (argv.h || argv.help) { + console.log(help); + process.exit(0); +} + +if (typeof argv.good !== 'number') { + argv.good = getChromiumRevision('main'); + if (typeof argv.good !== 'number') { + console.log( + COLOR_RED + + 'ERROR: Could not parse current Chromium revision' + + COLOR_RESET + ); + console.log(help); + process.exit(1); + } +} + +if (typeof argv.bad !== 'number') { + argv.bad = getChromiumRevision(); + if (typeof argv.bad !== 'number') { + console.log( + COLOR_RED + + 'ERROR: Could not parse Chromium revision in the main branch' + + COLOR_RESET + ); + console.log(help); + process.exit(1); + } +} + +if (!argv.script && !argv['unit-test']) { + console.log( + COLOR_RED + + 'ERROR: Expected to be given a script or a unit test to run' + + COLOR_RESET + ); + console.log(help); + process.exit(1); +} + +const scriptPath = argv.script ? path.resolve(argv.script) : null; + +if (argv.script && !fs.existsSync(scriptPath)) { + console.log( + COLOR_RED + + 'ERROR: Expected to be given a path to a script to run' + + COLOR_RESET + ); + console.log(help); + process.exit(1); +} + +(async (scriptPath, good, bad, pattern, noCache) => { + const span = Math.abs(good - bad); + console.log( + `Bisecting ${COLOR_YELLOW}${span}${COLOR_RESET} revisions in ${COLOR_YELLOW}~${ + span.toString(2).length + }${COLOR_RESET} iterations` + ); + + while (true) { + const middle = Math.round((good + bad) / 2); + const revision = await findDownloadableRevision(middle, good, bad); + if (!revision || revision === good || revision === bad) { + break; + } + let info = browserFetcher.revisionInfo(revision); + const shouldRemove = noCache && !info.local; + info = await downloadRevision(revision); + const exitCode = await (pattern + ? runUnitTest(pattern, info) + : runScript(scriptPath, info)); + if (shouldRemove) { + await browserFetcher.remove(revision); + } + let outcome; + if (exitCode) { + bad = revision; + outcome = COLOR_RED + 'BAD' + COLOR_RESET; + } else { + good = revision; + outcome = COLOR_GREEN + 'GOOD' + COLOR_RESET; + } + const span = Math.abs(good - bad); + let fromText = ''; + let toText = ''; + if (good < bad) { + fromText = COLOR_GREEN + good + COLOR_RESET; + toText = COLOR_RED + bad + COLOR_RESET; + } else { + fromText = COLOR_RED + bad + COLOR_RESET; + toText = COLOR_GREEN + good + COLOR_RESET; + } + console.log( + `- ${COLOR_YELLOW}r${revision}${COLOR_RESET} was ${outcome}. Bisecting [${fromText}, ${toText}] - ${COLOR_YELLOW}${span}${COLOR_RESET} revisions and ${COLOR_YELLOW}~${ + span.toString(2).length + }${COLOR_RESET} iterations` + ); + } + + const [fromSha, toSha] = await Promise.all([ + revisionToSha(Math.min(good, bad)), + revisionToSha(Math.max(good, bad)), + ]); + console.log( + `RANGE: https://chromium.googlesource.com/chromium/src/+log/${fromSha}..${toSha}` + ); +})(scriptPath, argv.good, argv.bad, argv['unit-test'], argv['no-cache']); + +function runScript(scriptPath, revisionInfo) { + const log = debug('bisect:runscript'); + log('Running script'); + const child = fork(scriptPath, [], { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + env: { + ...process.env, + PUPPETEER_EXECUTABLE_PATH: revisionInfo.executablePath, + }, + }); + return new Promise((resolve, reject) => { + child.on('error', err => { + return reject(err); + }); + child.on('exit', code => { + return resolve(code); + }); + }); +} + +function runUnitTest(pattern, revisionInfo) { + const log = debug('bisect:rununittest'); + log('Running unit test'); + const child = spawn('npm run test:chrome:headless', ['--', '-g', pattern], { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + shell: true, + env: { + ...process.env, + PUPPETEER_EXECUTABLE_PATH: revisionInfo.executablePath, + }, + }); + return new Promise((resolve, reject) => { + child.on('error', err => { + return reject(err); + }); + child.on('exit', code => { + return resolve(code); + }); + }); +} + +async function downloadRevision(revision) { + const log = debug('bisect:download'); + log(`Downloading ${revision}`); + let progressBar = null; + let lastDownloadedBytes = 0; + return await browserFetcher.download( + revision, + (downloadedBytes, totalBytes) => { + if (!progressBar) { + const ProgressBar = require('progress'); + progressBar = new ProgressBar( + `- downloading Chromium r${revision} - ${toMegabytes( + totalBytes + )} [:bar] :percent :etas `, + { + complete: '=', + incomplete: ' ', + width: 20, + total: totalBytes, + } + ); + } + const delta = downloadedBytes - lastDownloadedBytes; + lastDownloadedBytes = downloadedBytes; + progressBar.tick(delta); + } + ); + function toMegabytes(bytes) { + const mb = bytes / 1024 / 1024; + return `${Math.round(mb * 10) / 10} Mb`; + } +} + +async function findDownloadableRevision(rev, from, to) { + const log = debug('bisect:findrev'); + const min = Math.min(from, to); + const max = Math.max(from, to); + log(`Looking around ${rev} from [${min}, ${max}]`); + if (await browserFetcher.canDownload(rev)) { + return rev; + } + let down = rev; + let up = rev; + while (min <= down || up <= max) { + const [downOk, upOk] = await Promise.all([ + down > min ? probe(--down) : Promise.resolve(false), + up < max ? probe(++up) : Promise.resolve(false), + ]); + if (downOk) { + return down; + } + if (upOk) { + return up; + } + } + return null; + + async function probe(rev) { + const result = await browserFetcher.canDownload(rev); + log(` ${rev} - ${result ? 'OK' : 'missing'}`); + return result; + } +} + +async function revisionToSha(revision) { + const json = await fetchJSON( + 'https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/' + revision + ); + return json.git_sha; +} + +function fetchJSON(url) { + return new Promise((resolve, reject) => { + const agent = url.startsWith('https://') + ? require('https') + : require('http'); + const options = URL.parse(url); + options.method = 'GET'; + options.headers = { + 'Content-Type': 'application/json', + }; + const req = agent.request(options, function (res) { + let result = ''; + res.setEncoding('utf8'); + res.on('data', chunk => { + return (result += chunk); + }); + res.on('end', () => { + return resolve(JSON.parse(result)); + }); + }); + req.on('error', err => { + return reject(err); + }); + req.end(); + }); +} + +function getChromiumRevision(gitRevision = null) { + const fileName = 'src/revisions.ts'; + const command = gitRevision + ? `git show ${gitRevision}:${fileName}` + : `cat ${fileName}`; + const result = execSync(command, { + encoding: 'utf8', + shell: true, + }); + + const m = result.match(/chromium: '(\d+)'/); + if (!m) { + return null; + } + return parseInt(m[1], 10); +} diff --git a/remote/test/puppeteer/utils/check_availability.js b/remote/test/puppeteer/utils/check_availability.js new file mode 100755 index 0000000000..531d0a2a85 --- /dev/null +++ b/remote/test/puppeteer/utils/check_availability.js @@ -0,0 +1,359 @@ +#!/usr/bin/env node +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const assert = require('assert'); +const https = require('https'); +const BrowserFetcher = + require('../lib/cjs/puppeteer/node/BrowserFetcher.js').BrowserFetcher; + +const SUPPORTED_PLATFORMS = ['linux', 'mac', 'mac_arm', 'win32', 'win64']; + +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', +}; + +class Table { + /** + * @param {!Array<number>} columnWidths + */ + constructor(columnWidths) { + this.widths = columnWidths; + } + + /** + * @param {!Array<string>} values + */ + drawRow(values) { + assert(values.length === this.widths.length); + let row = ''; + for (let i = 0; i < values.length; ++i) { + row += padCenter(values[i], this.widths[i]); + } + console.log(row); + } +} + +const helpMessage = ` +This script checks availability of prebuilt Chromium snapshots. + +Usage: node check_availability.js [<options>] [<browser version(s)>] + +options + -f full mode checks availability of all the platforms, default mode + -r roll mode checks for the most recent stable Chromium roll candidate + -rb roll mode checks for the most recent beta Chromium roll candidate + -rd roll mode checks for the most recent dev Chromium roll candidate + -p $platform print the latest revision for the given platform (${SUPPORTED_PLATFORMS.join( + ',' + )}). + -h show this help + +browser version(s) + <revision> single revision number means checking for this specific revision + <from> <to> checks all the revisions within a given range, inclusively + +Examples + To check Chromium availability of a certain revision + node check_availability.js [revision] + + To find a Chromium roll candidate for current stable Linux version + node check_availability.js -r + use -rb for beta and -rd for dev versions. + + To check Chromium availability from the latest revision in a descending order + node check_availability.js +`; + +function main() { + const args = process.argv.slice(2); + + if (args.length > 3) { + console.log(helpMessage); + return; + } + + if (args.length === 0) { + checkOmahaProxyAvailability(); + return; + } + + if (args[0].startsWith('-')) { + const option = args[0].substring(1); + switch (option) { + case 'f': + break; + case 'r': + checkRollCandidate('stable'); + return; + case 'rb': + checkRollCandidate('beta'); + return; + case 'rd': + checkRollCandidate('dev'); + return; + case 'p': + printLatestRevisionForPlatform(args[1]); + return; + default: + console.log(helpMessage); + return; + } + args.splice(0, 1); // remove options arg since we are done with options + } + + if (args.length === 1) { + const revision = parseInt(args[0], 10); + checkRangeAvailability({ + fromRevision: revision, + toRevision: revision, + stopWhenAllAvailable: false, + }); + } else { + const fromRevision = parseInt(args[0], 10); + const toRevision = parseInt(args[1], 10); + checkRangeAvailability({ + fromRevision, + toRevision, + stopWhenAllAvailable: false, + }); + } +} + +async function checkOmahaProxyAvailability() { + const latestRevisions = ( + await Promise.all([ + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Mac/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Mac_Arm/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Win/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/LAST_CHANGE' + ), + ]) + ).map(s => { + return parseInt(s, 10); + }); + const from = Math.max(...latestRevisions); + await checkRangeAvailability({ + fromRevision: from, + toRevision: 0, + stopWhenAllAvailable: false, + }); +} + +async function printLatestRevisionForPlatform(platform) { + const latestRevisions = ( + await Promise.all([ + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Mac/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Mac_Arm/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Win/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/LAST_CHANGE' + ), + ]) + ).map(s => { + return parseInt(s, 10); + }); + const from = Math.max(...latestRevisions); + await checkRangeAvailability({ + fromRevision: from, + toRevision: 0, + stopWhenAllAvailable: true, + printAsTable: false, + platforms: [platform], + }); +} + +async function checkRollCandidate(channel) { + const omahaResponse = await fetch( + `https://omahaproxy.appspot.com/all.json?channel=${channel}&os=linux` + ); + const linuxInfo = JSON.parse(omahaResponse)[0]; + if (!linuxInfo) { + console.error(`no ${channel} linux information available from omahaproxy`); + return; + } + + const linuxRevision = parseInt( + linuxInfo.versions[0].branch_base_position, + 10 + ); + const currentRevision = parseInt( + require('../lib/cjs/puppeteer/revisions.js').PUPPETEER_REVISIONS.chromium, + 10 + ); + + checkRangeAvailability({ + fromRevision: linuxRevision, + toRevision: currentRevision, + stopWhenAllAvailable: true, + }); +} + +/** + * @param {*} options + */ +async function checkRangeAvailability({ + fromRevision, + toRevision, + stopWhenAllAvailable, + platforms, + printAsTable = true, +}) { + platforms = platforms || SUPPORTED_PLATFORMS; + let table; + if (printAsTable) { + table = new Table([ + 10, + ...platforms.map(() => { + return 7; + }), + ]); + table.drawRow([''].concat(platforms)); + } + + const fetchers = platforms.map(platform => { + return new BrowserFetcher('', {platform}); + }); + + const inc = fromRevision < toRevision ? 1 : -1; + const revisionToStop = toRevision + inc; // +inc so the range is fully inclusive + for ( + let revision = fromRevision; + revision !== revisionToStop; + revision += inc + ) { + const promises = fetchers.map(fetcher => { + return fetcher.canDownload(revision); + }); + const availability = await Promise.all(promises); + const allAvailable = availability.every(e => { + return !!e; + }); + if (table) { + const values = [ + ' ' + + (allAvailable ? colors.green + revision + colors.reset : revision), + ]; + for (let i = 0; i < availability.length; ++i) { + const decoration = availability[i] ? '+' : '-'; + const color = availability[i] ? colors.green : colors.red; + values.push(color + decoration + colors.reset); + } + table.drawRow(values); + } else { + if (allAvailable) { + console.log(revision); + } + } + if (allAvailable && stopWhenAllAvailable) { + break; + } + } +} + +/** + * @param {string} url + * @returns {!Promise<?string>} + */ +function fetch(url) { + let resolve; + const promise = new Promise(x => { + return (resolve = x); + }); + https + .get(url, response => { + if (response.statusCode !== 200) { + resolve(null); + return; + } + let body = ''; + response.on('data', function (chunk) { + body += chunk; + }); + response.on('end', function () { + resolve(body); + }); + }) + .on('error', function (e) { + // This is okay; the server may just be faster at closing than us after + // the body is fully sent. + if (e.message.includes('ECONNRESET')) { + return; + } + console.error(`Error fetching json from ${url}: ${e}`); + resolve(null); + }); + return promise; +} + +/** + * @param {number} size + * @returns {string} + */ +function spaceString(size) { + return new Array(size).fill(' ').join(''); +} + +/** + * @param {string} text + * @returns {string} + */ +function filterOutColors(text) { + for (const colorName in colors) { + const color = colors[colorName]; + text = text.replace(color, ''); + } + return text; +} + +/** + * @param {string} text + * @param {number} length + * @returns {string} + */ +function padCenter(text, length) { + const printableCharacters = filterOutColors(text); + if (printableCharacters.length >= length) { + return text; + } + const left = Math.floor((length - printableCharacters.length) / 2); + const right = Math.ceil((length - printableCharacters.length) / 2); + return spaceString(left) + text + spaceString(right); +} + +main(); diff --git a/remote/test/puppeteer/utils/generate_artifacts.ts b/remote/test/puppeteer/utils/generate_artifacts.ts new file mode 100644 index 0000000000..e150c45e2e --- /dev/null +++ b/remote/test/puppeteer/utils/generate_artifacts.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env node +import {writeFile} from 'fs/promises'; +import {job} from './internal/job.js'; +import {spawnAndLog} from './internal/util.js'; + +(async () => { + job('', async ({outputs}) => { + await writeFile(outputs[0]!, '{"type": "module"}'); + }) + .outputs(['lib/esm/package.json']) + .build(); + + job('', async ({outputs}) => { + spawnAndLog('api-extractor', 'run', '--local'); + spawnAndLog( + 'eslint', + '--ext=ts', + '--no-ignore', + '--no-eslintrc', + '-c=.eslintrc.types.cjs', + '--fix', + outputs[0]! + ); + }) + .inputs(['lib/esm/puppeteer/types.d.ts']) + .outputs(['lib/types.d.ts', 'docs/puppeteer.api.json']) + .build(); +})(); diff --git a/remote/test/puppeteer/utils/generate_docs.ts b/remote/test/puppeteer/utils/generate_docs.ts new file mode 100644 index 0000000000..35d2c8a57a --- /dev/null +++ b/remote/test/puppeteer/utils/generate_docs.ts @@ -0,0 +1,117 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {readFile, rm, writeFile} from 'fs/promises'; +import semver from 'semver'; +import {generateDocs} from './internal/custom_markdown_action.js'; +import {job} from './internal/job.js'; +import {spawnAndLog} from './internal/util.js'; + +function getOffsetAndLimit( + sectionName: string, + lines: string[] +): [offset: number, limit: number] { + const offset = + lines.findIndex(line => { + return line.includes(`<!-- ${sectionName}-start -->`); + }) + 1; + const limit = lines.slice(offset).findIndex(line => { + return line.includes(`<!-- ${sectionName}-end -->`); + }); + return [offset, limit]; +} + +function spliceIntoSection( + sectionName: string, + content: string, + sectionContent: string +): string { + const lines = content.split('\n'); + const [offset, limit] = getOffsetAndLimit(sectionName, lines); + lines.splice(offset, limit, ...sectionContent.split('\n')); + return lines.join('\n'); +} + +(async () => { + const job1 = job('', async ({inputs, outputs}) => { + const content = await readFile(inputs[0]!, 'utf-8'); + const sectionContent = ` +--- +sidebar_position: 1 +--- +`; + await writeFile(outputs[0]!, sectionContent + content); + }) + .inputs(['README.md']) + .outputs(['docs/index.md']) + .build(); + + // Chrome Versions + const job2 = job('', async ({inputs, outputs}) => { + let content = await readFile(inputs[2]!, {encoding: 'utf8'}); + const {versionsPerRelease} = await import(inputs[0]!); + const versionsArchived = JSON.parse(await readFile(inputs[1]!, 'utf8')); + + // Generate versions + const buffer: string[] = []; + for (const [chromiumVersion, puppeteerVersion] of versionsPerRelease) { + if (puppeteerVersion === 'NEXT') { + continue; + } + if (versionsArchived.includes(puppeteerVersion.substring(1))) { + buffer.push( + ` * Chromium ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](https://github.com/puppeteer/puppeteer/blob/${puppeteerVersion}/docs/api/index.md)` + ); + } else if (semver.lt(puppeteerVersion, '15.0.0')) { + buffer.push( + ` * Chromium ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](https://github.com/puppeteer/puppeteer/blob/${puppeteerVersion}/docs/api.md)` + ); + } else if (semver.gte(puppeteerVersion, '15.3.0')) { + buffer.push( + ` * Chromium ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](https://pptr.dev/${puppeteerVersion.slice( + 1 + )})` + ); + } else { + buffer.push( + ` * Chromium ${chromiumVersion} - Puppeteer ${puppeteerVersion}` + ); + } + } + content = spliceIntoSection('version', content, buffer.join('\n')); + + await writeFile(outputs[0]!, content); + }) + .inputs([ + 'versions.js', + 'website/versionsArchived.json', + 'docs/chromium-support.md', + ]) + .outputs(['docs/chromium-support.md']) + .build(); + + await Promise.all([job1, job2]); + + // Generate documentation + job('', async ({inputs, outputs}) => { + await rm(outputs[0]!, {recursive: true, force: true}); + generateDocs(inputs[0]!, outputs[0]!); + spawnAndLog('prettier', '--ignore-path', 'none', '--write', 'docs'); + }) + .inputs(['docs/puppeteer.api.json']) + .outputs(['docs/api']) + .build(); +})(); diff --git a/remote/test/puppeteer/utils/generate_sources.ts b/remote/test/puppeteer/utils/generate_sources.ts new file mode 100644 index 0000000000..1507d11e5e --- /dev/null +++ b/remote/test/puppeteer/utils/generate_sources.ts @@ -0,0 +1,106 @@ +#!/usr/bin/env node +import {createHash} from 'crypto'; +import esbuild from 'esbuild'; +import {mkdir, mkdtemp, readFile, rm, writeFile} from 'fs/promises'; +import {sync as glob} from 'glob'; +import path from 'path'; +import {job} from './internal/job.js'; + +const INCLUDED_FOLDERS = ['common', 'node', 'generated', 'util']; + +(async () => { + await job('', async ({outputs}) => { + await Promise.all( + outputs.map(outputs => { + return mkdir(outputs, {recursive: true}); + }) + ); + }) + .outputs(['src/generated']) + .build(); + + await job('', async ({name, inputs, outputs}) => { + const input = inputs.find(input => { + return input.endsWith('injected.ts'); + })!; + const template = await readFile( + inputs.find(input => { + return input.includes('injected.ts.tmpl'); + })!, + 'utf8' + ); + const tmp = await mkdtemp(name); + await esbuild.build({ + entryPoints: [input], + bundle: true, + outdir: tmp, + format: 'cjs', + platform: 'browser', + target: 'ES2019', + }); + const baseName = path.basename(input); + const content = await readFile( + path.join(tmp, baseName.replace('.ts', '.js')), + 'utf-8' + ); + const scriptContent = template.replace( + 'SOURCE_CODE', + JSON.stringify(content) + ); + await writeFile(outputs[0]!, scriptContent); + await rm(tmp, {recursive: true, force: true}); + }) + .inputs(['src/templates/injected.ts.tmpl', 'src/injected/**/*.ts']) + .outputs(['src/generated/injected.ts']) + .build(); + + const sources = glob( + `src/{@(${INCLUDED_FOLDERS.join('|')})/*.ts,!(types|puppeteer-core).ts}` + ); + await job('', async ({outputs}) => { + let types = + '// AUTOGENERATED - Use `npm run generate:sources` to regenerate.\n\n'; + for (const input of sources.map(source => { + return `.${source.slice(3)}`; + })) { + types += `export * from '${input.replace('.ts', '.js')}';\n`; + } + await writeFile(outputs[0]!, types); + }) + .value( + sources + .reduce((hmac, value) => { + return hmac.update(value); + }, createHash('sha256')) + .digest('hex') + ) + .outputs(['src/types.ts']) + .build(); + + if (process.env['PUBLISH']) { + job('', async ({inputs}) => { + const version = JSON.parse(await readFile(inputs[0]!, 'utf8')).version; + await writeFile( + inputs[1]!, + ( + await readFile(inputs[1]!, { + encoding: 'utf-8', + }) + ).replace("'NEXT'", `'v${version}'`) + ); + }) + .inputs(['package.json', 'versions.js']) + .build(); + } + + job('', async ({inputs, outputs}) => { + const version = JSON.parse(await readFile(inputs[0]!, 'utf8')).version; + await writeFile( + outputs[0]!, + (await readFile(inputs[1]!, 'utf8')).replace('PACKAGE_VERSION', version) + ); + }) + .inputs(['package.json', 'src/templates/version.ts.tmpl']) + .outputs(['src/generated/version.ts']) + .build(); +})(); diff --git a/remote/test/puppeteer/utils/get_deprecated_version_range.js b/remote/test/puppeteer/utils/get_deprecated_version_range.js new file mode 100644 index 0000000000..280bbf8ab0 --- /dev/null +++ b/remote/test/puppeteer/utils/get_deprecated_version_range.js @@ -0,0 +1,30 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { + versionsPerRelease, + lastMaintainedChromiumVersion, +} = require('../versions.js'); +let version = versionsPerRelease.get(lastMaintainedChromiumVersion); +if (version.toLowerCase() === 'next') { + console.error('Unexpected NEXT Puppeteer version in versions.js'); + process.exit(1); +} +if (version.startsWith('v')) { + version = version.substring(1); +} +console.log('<' + version); +process.exit(0); diff --git a/remote/test/puppeteer/utils/internal/custom_markdown_action.ts b/remote/test/puppeteer/utils/internal/custom_markdown_action.ts new file mode 100644 index 0000000000..acd304df76 --- /dev/null +++ b/remote/test/puppeteer/utils/internal/custom_markdown_action.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ApiModel} from '@microsoft/api-extractor-model'; +import {MarkdownDocumenter} from './custom_markdown_documenter.js'; + +export const generateDocs = (jsonPath: string, outputDir: string): void => { + const apiModel = new ApiModel(); + apiModel.loadPackage(jsonPath); + + const markdownDocumenter: MarkdownDocumenter = new MarkdownDocumenter({ + apiModel: apiModel, + documenterConfig: undefined, + outputFolder: outputDir, + }); + markdownDocumenter.generateFiles(); +}; diff --git a/remote/test/puppeteer/utils/internal/custom_markdown_documenter.ts b/remote/test/puppeteer/utils/internal/custom_markdown_documenter.ts new file mode 100644 index 0000000000..b9caf88b27 --- /dev/null +++ b/remote/test/puppeteer/utils/internal/custom_markdown_documenter.ts @@ -0,0 +1,1452 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the +// MIT license. See LICENSE in the project root for license information. + +// Taken from +// https://github.com/microsoft/rushstack/blob/main/apps/api-documenter/src/documenters/MarkdownDocumenter.ts +// This file has been edited to morph into Docusaurus's expected inputs. + +import { + ApiClass, + ApiDeclaredItem, + ApiDocumentedItem, + ApiEnum, + ApiInitializerMixin, + ApiInterface, + ApiItem, + ApiItemKind, + ApiModel, + ApiNamespace, + ApiOptionalMixin, + ApiPackage, + ApiParameterListMixin, + ApiPropertyItem, + ApiProtectedMixin, + ApiReadonlyMixin, + ApiReleaseTagMixin, + ApiReturnTypeMixin, + ApiStaticMixin, + ApiTypeAlias, + Excerpt, + ExcerptToken, + ExcerptTokenKind, + IResolveDeclarationReferenceResult, + ReleaseTag, +} from '@microsoft/api-extractor-model'; +import { + DocBlock, + DocCodeSpan, + DocComment, + DocFencedCode, + DocLinkTag, + DocNodeContainer, + DocNodeKind, + DocParagraph, + DocPlainText, + DocSection, + StandardTags, + StringBuilder, + TSDocConfiguration, +} from '@microsoft/tsdoc'; +import { + FileSystem, + NewlineKind, + PackageName, +} from '@rushstack/node-core-library'; +import * as path from 'path'; + +import {DocumenterConfig} from '@microsoft/api-documenter/lib/documenters/DocumenterConfig'; +import {CustomMarkdownEmitter} from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter'; +import {CustomDocNodes} from '@microsoft/api-documenter/lib/nodes/CustomDocNodeKind'; +import {DocEmphasisSpan} from '@microsoft/api-documenter/lib/nodes/DocEmphasisSpan'; +import {DocHeading} from '@microsoft/api-documenter/lib/nodes/DocHeading'; +import {DocNoteBox} from '@microsoft/api-documenter/lib/nodes/DocNoteBox'; +import {DocTable} from '@microsoft/api-documenter/lib/nodes/DocTable'; +import {DocTableCell} from '@microsoft/api-documenter/lib/nodes/DocTableCell'; +import {DocTableRow} from '@microsoft/api-documenter/lib/nodes/DocTableRow'; +import {MarkdownDocumenterAccessor} from '@microsoft/api-documenter/lib/plugin/MarkdownDocumenterAccessor'; +import { + IMarkdownDocumenterFeatureOnBeforeWritePageArgs, + MarkdownDocumenterFeatureContext, +} from '@microsoft/api-documenter/lib/plugin/MarkdownDocumenterFeature'; +import {PluginLoader} from '@microsoft/api-documenter/lib/plugin/PluginLoader'; +import {Utilities} from '@microsoft/api-documenter/lib/utils/Utilities'; + +export interface IMarkdownDocumenterOptions { + apiModel: ApiModel; + documenterConfig: DocumenterConfig | undefined; + outputFolder: string; +} + +/** + * Renders API documentation in the Markdown file format. + * For more info: https://en.wikipedia.org/wiki/Markdown + */ +export class MarkdownDocumenter { + private readonly _apiModel: ApiModel; + private readonly _documenterConfig: DocumenterConfig | undefined; + private readonly _tsdocConfiguration: TSDocConfiguration; + private readonly _markdownEmitter: CustomMarkdownEmitter; + private readonly _outputFolder: string; + private readonly _pluginLoader: PluginLoader; + + public constructor(options: IMarkdownDocumenterOptions) { + this._apiModel = options.apiModel; + this._documenterConfig = options.documenterConfig; + this._outputFolder = options.outputFolder; + this._tsdocConfiguration = CustomDocNodes.configuration; + this._markdownEmitter = new CustomMarkdownEmitter(this._apiModel); + + this._pluginLoader = new PluginLoader(); + } + + public generateFiles(): void { + if (this._documenterConfig) { + this._pluginLoader.load(this._documenterConfig, () => { + return new MarkdownDocumenterFeatureContext({ + apiModel: this._apiModel, + outputFolder: this._outputFolder, + documenter: new MarkdownDocumenterAccessor({ + getLinkForApiItem: (apiItem: ApiItem) => { + console.log(apiItem); + return this._getLinkFilenameForApiItem(apiItem); + }, + }), + }); + }); + } + + console.log(); + this._deleteOldOutputFiles(); + + this._writeApiItemPage(this._apiModel.members[0]!); + + if (this._pluginLoader.markdownDocumenterFeature) { + this._pluginLoader.markdownDocumenterFeature.onFinished({}); + } + } + + private _writeApiItemPage(apiItem: ApiItem): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + const output: DocSection = new DocSection({ + configuration: this._tsdocConfiguration, + }); + + const scopedName: string = apiItem.getScopedNameWithinPackage(); + + switch (apiItem.kind) { + case ApiItemKind.Class: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} class`}) + ); + break; + case ApiItemKind.Enum: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} enum`}) + ); + break; + case ApiItemKind.Interface: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} interface`}) + ); + break; + case ApiItemKind.Constructor: + case ApiItemKind.ConstructSignature: + output.appendNode(new DocHeading({configuration, title: scopedName})); + break; + case ApiItemKind.Method: + case ApiItemKind.MethodSignature: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} method`}) + ); + break; + case ApiItemKind.Function: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} function`}) + ); + break; + case ApiItemKind.Model: + output.appendNode( + new DocHeading({configuration, title: `API Reference`}) + ); + break; + case ApiItemKind.Namespace: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} namespace`}) + ); + break; + case ApiItemKind.Package: + console.log(`Writing ${apiItem.displayName} package`); + output.appendNode( + new DocHeading({ + configuration, + title: `API Reference`, + }) + ); + break; + case ApiItemKind.Property: + case ApiItemKind.PropertySignature: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} property`}) + ); + break; + case ApiItemKind.TypeAlias: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} type`}) + ); + break; + case ApiItemKind.Variable: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} variable`}) + ); + break; + default: + throw new Error('Unsupported API item kind: ' + apiItem.kind); + } + + if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) { + if (apiItem.releaseTag === ReleaseTag.Beta) { + this._writeBetaWarning(output); + } + } + + const decoratorBlocks: DocBlock[] = []; + + if (apiItem instanceof ApiDocumentedItem) { + const tsdocComment: DocComment | undefined = apiItem.tsdocComment; + + if (tsdocComment) { + decoratorBlocks.push( + ...tsdocComment.customBlocks.filter(block => { + return ( + block.blockTag.tagNameWithUpperCase === + StandardTags.decorator.tagNameWithUpperCase + ); + }) + ); + + if (tsdocComment.deprecatedBlock) { + output.appendNode( + new DocNoteBox({configuration: this._tsdocConfiguration}, [ + new DocParagraph({configuration: this._tsdocConfiguration}, [ + new DocPlainText({ + configuration: this._tsdocConfiguration, + text: 'Warning: This API is now obsolete. ', + }), + ]), + ...tsdocComment.deprecatedBlock.content.nodes, + ]) + ); + } + + this._appendSection(output, tsdocComment.summarySection); + } + } + + if (apiItem instanceof ApiDeclaredItem) { + if (apiItem.excerpt.text.length > 0) { + output.appendNode( + new DocParagraph({configuration}, [ + new DocEmphasisSpan({configuration, bold: true}, [ + new DocPlainText({configuration, text: 'Signature:'}), + ]), + ]) + ); + + let code: string; + switch (apiItem.parent?.kind) { + case ApiItemKind.Class: + code = `class ${ + apiItem.parent.displayName + } {${apiItem.getExcerptWithModifiers()}}`; + break; + case ApiItemKind.Interface: + code = `interface ${ + apiItem.parent.displayName + } {${apiItem.getExcerptWithModifiers()}}`; + break; + default: + code = apiItem.getExcerptWithModifiers(); + } + output.appendNode( + new DocFencedCode({ + configuration, + code: code, + language: 'typescript', + }) + ); + } + + this._writeHeritageTypes(output, apiItem); + } + + if (decoratorBlocks.length > 0) { + output.appendNode( + new DocParagraph({configuration}, [ + new DocEmphasisSpan({configuration, bold: true}, [ + new DocPlainText({configuration, text: 'Decorators:'}), + ]), + ]) + ); + for (const decoratorBlock of decoratorBlocks) { + output.appendNodes(decoratorBlock.content.nodes); + } + } + + let appendRemarks = true; + switch (apiItem.kind) { + case ApiItemKind.Class: + case ApiItemKind.Interface: + case ApiItemKind.Namespace: + case ApiItemKind.Package: + this._writeRemarksSection(output, apiItem); + appendRemarks = false; + break; + } + + switch (apiItem.kind) { + case ApiItemKind.Class: + this._writeClassTables(output, apiItem as ApiClass); + break; + case ApiItemKind.Enum: + this._writeEnumTables(output, apiItem as ApiEnum); + break; + case ApiItemKind.Interface: + this._writeInterfaceTables(output, apiItem as ApiInterface); + break; + case ApiItemKind.Constructor: + case ApiItemKind.ConstructSignature: + case ApiItemKind.Method: + case ApiItemKind.MethodSignature: + case ApiItemKind.Function: + this._writeParameterTables(output, apiItem as ApiParameterListMixin); + this._writeThrowsSection(output, apiItem); + break; + case ApiItemKind.Namespace: + this._writePackageOrNamespaceTables(output, apiItem as ApiNamespace); + break; + case ApiItemKind.Model: + this._writeModelTable(output, apiItem as ApiModel); + break; + case ApiItemKind.Package: + this._writePackageOrNamespaceTables(output, apiItem as ApiPackage); + break; + case ApiItemKind.Property: + case ApiItemKind.PropertySignature: + break; + case ApiItemKind.TypeAlias: + break; + case ApiItemKind.Variable: + break; + default: + throw new Error('Unsupported API item kind: ' + apiItem.kind); + } + + if (appendRemarks) { + this._writeRemarksSection(output, apiItem); + } + + const filename: string = path.join( + this._outputFolder, + this._getFilenameForApiItem(apiItem) + ); + const stringBuilder: StringBuilder = new StringBuilder(); + + this._markdownEmitter.emit(stringBuilder, output, { + contextApiItem: apiItem, + onGetFilenameForApiItem: (apiItemForFilename: ApiItem) => { + return this._getLinkFilenameForApiItem(apiItemForFilename); + }, + }); + + let pageContent: string = stringBuilder.toString(); + + if (this._pluginLoader.markdownDocumenterFeature) { + // Allow the plugin to customize the pageContent + const eventArgs: IMarkdownDocumenterFeatureOnBeforeWritePageArgs = { + apiItem: apiItem, + outputFilename: filename, + pageContent: pageContent, + }; + this._pluginLoader.markdownDocumenterFeature.onBeforeWritePage(eventArgs); + pageContent = eventArgs.pageContent; + } + + pageContent = + `---\nsidebar_label: ${this._getSidebarLabelForApiItem(apiItem)}\n---` + + pageContent; + pageContent = pageContent.replace('##', '#'); + pageContent = pageContent.replace(/<!-- -->/g, ''); + pageContent = pageContent.replace(/\\\*\\\*/g, '**'); + pageContent = pageContent.replace(/<b>|<\/b>/g, '**'); + FileSystem.writeFile(filename, pageContent, { + convertLineEndings: this._documenterConfig + ? this._documenterConfig.newlineKind + : NewlineKind.CrLf, + }); + } + + private _writeHeritageTypes( + output: DocSection, + apiItem: ApiDeclaredItem + ): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + if (apiItem instanceof ApiClass) { + if (apiItem.extendsType) { + const extendsParagraph: DocParagraph = new DocParagraph( + {configuration}, + [ + new DocEmphasisSpan({configuration, bold: true}, [ + new DocPlainText({configuration, text: 'Extends: '}), + ]), + ] + ); + this._appendExcerptWithHyperlinks( + extendsParagraph, + apiItem.extendsType.excerpt + ); + output.appendNode(extendsParagraph); + } + if (apiItem.implementsTypes.length > 0) { + const extendsParagraph: DocParagraph = new DocParagraph( + {configuration}, + [ + new DocEmphasisSpan({configuration, bold: true}, [ + new DocPlainText({configuration, text: 'Implements: '}), + ]), + ] + ); + let needsComma = false; + for (const implementsType of apiItem.implementsTypes) { + if (needsComma) { + extendsParagraph.appendNode( + new DocPlainText({configuration, text: ', '}) + ); + } + this._appendExcerptWithHyperlinks( + extendsParagraph, + implementsType.excerpt + ); + needsComma = true; + } + output.appendNode(extendsParagraph); + } + } + + if (apiItem instanceof ApiInterface) { + if (apiItem.extendsTypes.length > 0) { + const extendsParagraph: DocParagraph = new DocParagraph( + {configuration}, + [ + new DocEmphasisSpan({configuration, bold: true}, [ + new DocPlainText({configuration, text: 'Extends: '}), + ]), + ] + ); + let needsComma = false; + for (const extendsType of apiItem.extendsTypes) { + if (needsComma) { + extendsParagraph.appendNode( + new DocPlainText({configuration, text: ', '}) + ); + } + this._appendExcerptWithHyperlinks( + extendsParagraph, + extendsType.excerpt + ); + needsComma = true; + } + output.appendNode(extendsParagraph); + } + } + + if (apiItem instanceof ApiTypeAlias) { + const refs: ExcerptToken[] = apiItem.excerptTokens.filter(token => { + return ( + token.kind === ExcerptTokenKind.Reference && + token.canonicalReference && + this._apiModel.resolveDeclarationReference( + token.canonicalReference, + undefined + ).resolvedApiItem + ); + }); + if (refs.length > 0) { + const referencesParagraph: DocParagraph = new DocParagraph( + {configuration}, + [ + new DocEmphasisSpan({configuration, bold: true}, [ + new DocPlainText({configuration, text: 'References: '}), + ]), + ] + ); + let needsComma = false; + const visited: Set<string> = new Set(); + for (const ref of refs) { + if (visited.has(ref.text)) { + continue; + } + visited.add(ref.text); + + if (needsComma) { + referencesParagraph.appendNode( + new DocPlainText({configuration, text: ', '}) + ); + } + + this._appendExcerptTokenWithHyperlinks(referencesParagraph, ref); + needsComma = true; + } + output.appendNode(referencesParagraph); + } + } + } + + private _writeRemarksSection(output: DocSection, apiItem: ApiItem): void { + if (apiItem instanceof ApiDocumentedItem) { + const tsdocComment: DocComment | undefined = apiItem.tsdocComment; + + if (tsdocComment) { + // Write the @remarks block + if (tsdocComment.remarksBlock) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Remarks', + }) + ); + this._appendSection(output, tsdocComment.remarksBlock.content); + } + + // Write the @example blocks + const exampleBlocks: DocBlock[] = tsdocComment.customBlocks.filter( + x => { + return ( + x.blockTag.tagNameWithUpperCase === + StandardTags.example.tagNameWithUpperCase + ); + } + ); + + let exampleNumber = 1; + for (const exampleBlock of exampleBlocks) { + const heading: string = + exampleBlocks.length > 1 ? `Example ${exampleNumber}` : 'Example'; + + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: heading, + }) + ); + + this._appendSection(output, exampleBlock.content); + + ++exampleNumber; + } + } + } + } + + private _writeThrowsSection(output: DocSection, apiItem: ApiItem): void { + if (apiItem instanceof ApiDocumentedItem) { + const tsdocComment: DocComment | undefined = apiItem.tsdocComment; + + if (tsdocComment) { + // Write the @throws blocks + const throwsBlocks: DocBlock[] = tsdocComment.customBlocks.filter(x => { + return ( + x.blockTag.tagNameWithUpperCase === + StandardTags.throws.tagNameWithUpperCase + ); + }); + + if (throwsBlocks.length > 0) { + const heading = 'Exceptions'; + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: heading, + }) + ); + + for (const throwsBlock of throwsBlocks) { + this._appendSection(output, throwsBlock.content); + } + } + } + } + } + + /** + * GENERATE PAGE: MODEL + */ + private _writeModelTable(output: DocSection, apiModel: ApiModel): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const packagesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Package', 'Description'], + }); + + for (const apiMember of apiModel.members) { + const row: DocTableRow = new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createDescriptionCell(apiMember), + ]); + + switch (apiMember.kind) { + case ApiItemKind.Package: + packagesTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + } + } + + if (packagesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Packages', + }) + ); + output.appendNode(packagesTable); + } + } + + /** + * GENERATE PAGE: PACKAGE or NAMESPACE + */ + private _writePackageOrNamespaceTables( + output: DocSection, + apiContainer: ApiPackage | ApiNamespace + ): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const classesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Class', 'Description'], + }); + + const enumerationsTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Enumeration', 'Description'], + }); + + const functionsTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Function', 'Description'], + }); + + const interfacesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Interface', 'Description'], + }); + + const namespacesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Namespace', 'Description'], + }); + + const variablesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Variable', 'Description'], + }); + + const typeAliasesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Type Alias', 'Description'], + }); + + const apiMembers: readonly ApiItem[] = + apiContainer.kind === ApiItemKind.Package + ? (apiContainer as ApiPackage).entryPoints[0]!.members + : (apiContainer as ApiNamespace).members; + + for (const apiMember of apiMembers) { + const row: DocTableRow = new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createDescriptionCell(apiMember), + ]); + + switch (apiMember.kind) { + case ApiItemKind.Class: + classesTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + + case ApiItemKind.Enum: + enumerationsTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + + case ApiItemKind.Interface: + interfacesTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + + case ApiItemKind.Namespace: + namespacesTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + + case ApiItemKind.Function: + functionsTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + + case ApiItemKind.TypeAlias: + typeAliasesTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + + case ApiItemKind.Variable: + variablesTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + } + } + + if (classesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Classes', + }) + ); + output.appendNode(classesTable); + } + + if (enumerationsTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Enumerations', + }) + ); + output.appendNode(enumerationsTable); + } + if (functionsTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Functions', + }) + ); + output.appendNode(functionsTable); + } + + if (interfacesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Interfaces', + }) + ); + output.appendNode(interfacesTable); + } + + if (namespacesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Namespaces', + }) + ); + output.appendNode(namespacesTable); + } + + if (variablesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Variables', + }) + ); + output.appendNode(variablesTable); + } + + if (typeAliasesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Type Aliases', + }) + ); + output.appendNode(typeAliasesTable); + } + } + + /** + * GENERATE PAGE: CLASS + */ + private _writeClassTables(output: DocSection, apiClass: ApiClass): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const eventsTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Property', 'Modifiers', 'Type', 'Description'], + }); + + const constructorsTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Constructor', 'Modifiers', 'Description'], + }); + + const propertiesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Property', 'Modifiers', 'Type', 'Description'], + }); + + const methodsTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Method', 'Modifiers', 'Description'], + }); + + for (const apiMember of apiClass.members) { + switch (apiMember.kind) { + case ApiItemKind.Constructor: { + constructorsTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createModifiersCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + + this._writeApiItemPage(apiMember); + break; + } + case ApiItemKind.Method: { + methodsTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createModifiersCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + + this._writeApiItemPage(apiMember); + break; + } + case ApiItemKind.Property: { + if ((apiMember as ApiPropertyItem).isEventProperty) { + eventsTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createModifiersCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + } else { + propertiesTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createModifiersCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + } + + this._writeApiItemPage(apiMember); + break; + } + } + } + + if (eventsTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Events', + }) + ); + output.appendNode(eventsTable); + } + + if (constructorsTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Constructors', + }) + ); + output.appendNode(constructorsTable); + } + + if (propertiesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Properties', + }) + ); + output.appendNode(propertiesTable); + } + + if (methodsTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Methods', + }) + ); + output.appendNode(methodsTable); + } + } + + /** + * GENERATE PAGE: ENUM + */ + private _writeEnumTables(output: DocSection, apiEnum: ApiEnum): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const enumMembersTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Member', 'Value', 'Description'], + }); + + for (const apiEnumMember of apiEnum.members) { + enumMembersTable.addRow( + new DocTableRow({configuration}, [ + new DocTableCell({configuration}, [ + new DocParagraph({configuration}, [ + new DocPlainText({ + configuration, + text: Utilities.getConciseSignature(apiEnumMember), + }), + ]), + ]), + this._createInitializerCell(apiEnumMember), + this._createDescriptionCell(apiEnumMember), + ]) + ); + } + + if (enumMembersTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Enumeration Members', + }) + ); + output.appendNode(enumMembersTable); + } + } + + /** + * GENERATE PAGE: INTERFACE + */ + private _writeInterfaceTables( + output: DocSection, + apiClass: ApiInterface + ): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const eventsTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Property', 'Modifiers', 'Type', 'Description'], + }); + + const propertiesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Property', 'Modifiers', 'Type', 'Description'], + }); + + const methodsTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Method', 'Description'], + }); + + for (const apiMember of apiClass.members) { + switch (apiMember.kind) { + case ApiItemKind.ConstructSignature: + case ApiItemKind.MethodSignature: { + methodsTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + + this._writeApiItemPage(apiMember); + break; + } + case ApiItemKind.PropertySignature: { + if ((apiMember as ApiPropertyItem).isEventProperty) { + eventsTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createModifiersCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + } else { + propertiesTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createModifiersCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + } + + this._writeApiItemPage(apiMember); + break; + } + } + } + + if (eventsTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Events', + }) + ); + output.appendNode(eventsTable); + } + + if (propertiesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Properties', + }) + ); + output.appendNode(propertiesTable); + } + + if (methodsTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Methods', + }) + ); + output.appendNode(methodsTable); + } + } + + /** + * GENERATE PAGE: FUNCTION-LIKE + */ + private _writeParameterTables( + output: DocSection, + apiParameterListMixin: ApiParameterListMixin + ): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const parametersTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Parameter', 'Type', 'Description'], + }); + for (const apiParameter of apiParameterListMixin.parameters) { + const parameterDescription: DocSection = new DocSection({configuration}); + + if (apiParameter.isOptional) { + parameterDescription.appendNodesInParagraph([ + new DocEmphasisSpan({configuration, italic: true}, [ + new DocPlainText({configuration, text: '(Optional)'}), + ]), + new DocPlainText({configuration, text: ' '}), + ]); + } + + if (apiParameter.tsdocParamBlock) { + this._appendAndMergeSection( + parameterDescription, + apiParameter.tsdocParamBlock.content + ); + } + + parametersTable.addRow( + new DocTableRow({configuration}, [ + new DocTableCell({configuration}, [ + new DocParagraph({configuration}, [ + new DocPlainText({configuration, text: apiParameter.name}), + ]), + ]), + new DocTableCell({configuration}, [ + this._createParagraphForTypeExcerpt( + apiParameter.parameterTypeExcerpt + ), + ]), + new DocTableCell({configuration}, parameterDescription.nodes), + ]) + ); + } + + if (parametersTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Parameters', + }) + ); + output.appendNode(parametersTable); + } + + if (ApiReturnTypeMixin.isBaseClassOf(apiParameterListMixin)) { + const returnTypeExcerpt: Excerpt = + apiParameterListMixin.returnTypeExcerpt; + output.appendNode( + new DocParagraph({configuration}, [ + new DocEmphasisSpan({configuration, bold: true}, [ + new DocPlainText({configuration, text: 'Returns:'}), + ]), + ]) + ); + + output.appendNode(this._createParagraphForTypeExcerpt(returnTypeExcerpt)); + + if (apiParameterListMixin instanceof ApiDocumentedItem) { + if ( + apiParameterListMixin.tsdocComment && + apiParameterListMixin.tsdocComment.returnsBlock + ) { + this._appendSection( + output, + apiParameterListMixin.tsdocComment.returnsBlock.content + ); + } + } + } + } + + private _createParagraphForTypeExcerpt(excerpt: Excerpt): DocParagraph { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const paragraph: DocParagraph = new DocParagraph({configuration}); + if (!excerpt.text.trim()) { + paragraph.appendNode( + new DocPlainText({configuration, text: '(not declared)'}) + ); + } else { + this._appendExcerptWithHyperlinks(paragraph, excerpt); + } + + return paragraph; + } + + private _appendExcerptWithHyperlinks( + docNodeContainer: DocNodeContainer, + excerpt: Excerpt + ): void { + for (const token of excerpt.spannedTokens) { + this._appendExcerptTokenWithHyperlinks(docNodeContainer, token); + } + } + + private _appendExcerptTokenWithHyperlinks( + docNodeContainer: DocNodeContainer, + token: ExcerptToken + ): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + // Markdown doesn't provide a standardized syntax for hyperlinks inside code + // spans, so we will render the type expression as DocPlainText. Instead of + // creating multiple DocParagraphs, we can simply discard any newlines and + // let the renderer do normal word-wrapping. + const unwrappedTokenText: string = token.text.replace(/[\r\n]+/g, ' '); + + // If it's hyperlinkable, then append a DocLinkTag + if (token.kind === ExcerptTokenKind.Reference && token.canonicalReference) { + const apiItemResult: IResolveDeclarationReferenceResult = + this._apiModel.resolveDeclarationReference( + token.canonicalReference, + undefined + ); + + if (apiItemResult.resolvedApiItem) { + docNodeContainer.appendNode( + new DocLinkTag({ + configuration, + tagName: '@link', + linkText: unwrappedTokenText, + urlDestination: this._getLinkFilenameForApiItem( + apiItemResult.resolvedApiItem + ), + }) + ); + return; + } + } + + // Otherwise append non-hyperlinked text + docNodeContainer.appendNode( + new DocPlainText({configuration, text: unwrappedTokenText}) + ); + } + + private _createTitleCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + let linkText: string = Utilities.getConciseSignature(apiItem); + if (ApiOptionalMixin.isBaseClassOf(apiItem) && apiItem.isOptional) { + linkText += '?'; + } + + return new DocTableCell({configuration}, [ + new DocParagraph({configuration}, [ + new DocLinkTag({ + configuration, + tagName: '@link', + linkText: linkText, + urlDestination: this._getLinkFilenameForApiItem(apiItem), + }), + ]), + ]); + } + + /** + * This generates a DocTableCell for an ApiItem including the summary section + * and "(BETA)" annotation. + * + * @remarks + * We mostly assume that the input is an ApiDocumentedItem, but it's easier to + * perform this as a runtime check than to have each caller perform a type + * cast. + */ + private _createDescriptionCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const section: DocSection = new DocSection({configuration}); + + if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) { + if (apiItem.releaseTag === ReleaseTag.Beta) { + section.appendNodesInParagraph([ + new DocEmphasisSpan({configuration, bold: true, italic: true}, [ + new DocPlainText({configuration, text: '(BETA)'}), + ]), + new DocPlainText({configuration, text: ' '}), + ]); + } + } + + if (ApiOptionalMixin.isBaseClassOf(apiItem) && apiItem.isOptional) { + section.appendNodesInParagraph([ + new DocEmphasisSpan({configuration, italic: true}, [ + new DocPlainText({configuration, text: '(Optional)'}), + ]), + new DocPlainText({configuration, text: ' '}), + ]); + } + + if (apiItem instanceof ApiDocumentedItem) { + if (apiItem.tsdocComment !== undefined) { + this._appendAndMergeSection( + section, + apiItem.tsdocComment.summarySection + ); + } + } + + return new DocTableCell({configuration}, section.nodes); + } + + private _createModifiersCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const section: DocSection = new DocSection({configuration}); + + if (ApiProtectedMixin.isBaseClassOf(apiItem)) { + if (apiItem.isProtected) { + section.appendNode( + new DocParagraph({configuration}, [ + new DocCodeSpan({configuration, code: 'protected'}), + ]) + ); + } + } + + if (ApiReadonlyMixin.isBaseClassOf(apiItem)) { + if (apiItem.isReadonly) { + section.appendNode( + new DocParagraph({configuration}, [ + new DocCodeSpan({configuration, code: 'readonly'}), + ]) + ); + } + } + + if (ApiStaticMixin.isBaseClassOf(apiItem)) { + if (apiItem.isStatic) { + section.appendNode( + new DocParagraph({configuration}, [ + new DocCodeSpan({configuration, code: 'static'}), + ]) + ); + } + } + + return new DocTableCell({configuration}, section.nodes); + } + + private _createPropertyTypeCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const section: DocSection = new DocSection({configuration}); + + if (apiItem instanceof ApiPropertyItem) { + section.appendNode( + this._createParagraphForTypeExcerpt(apiItem.propertyTypeExcerpt) + ); + } + + return new DocTableCell({configuration}, section.nodes); + } + + private _createInitializerCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const section: DocSection = new DocSection({configuration}); + + if (ApiInitializerMixin.isBaseClassOf(apiItem)) { + if (apiItem.initializerExcerpt) { + section.appendNodeInParagraph( + new DocCodeSpan({ + configuration, + code: apiItem.initializerExcerpt.text, + }) + ); + } + } + + return new DocTableCell({configuration}, section.nodes); + } + + private _writeBetaWarning(output: DocSection): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + const betaWarning: string = + 'This API is provided as a preview for developers and may change' + + ' based on feedback that we receive. Do not use this API in a production environment.'; + output.appendNode( + new DocNoteBox({configuration}, [ + new DocParagraph({configuration}, [ + new DocPlainText({configuration, text: betaWarning}), + ]), + ]) + ); + } + + private _appendSection(output: DocSection, docSection: DocSection): void { + for (const node of docSection.nodes) { + output.appendNode(node); + } + } + + private _appendAndMergeSection( + output: DocSection, + docSection: DocSection + ): void { + let firstNode = true; + for (const node of docSection.nodes) { + if (firstNode) { + if (node.kind === DocNodeKind.Paragraph) { + output.appendNodesInParagraph(node.getChildNodes()); + firstNode = false; + continue; + } + } + firstNode = false; + + output.appendNode(node); + } + } + + private _getSidebarLabelForApiItem(apiItem: ApiItem): string { + if (apiItem.kind === ApiItemKind.Package) { + return 'API'; + } + + let baseName = ''; + for (const hierarchyItem of apiItem.getHierarchy()) { + // For overloaded methods, add a suffix such as "MyClass.myMethod_2". + let qualifiedName: string = hierarchyItem.displayName; + if (ApiParameterListMixin.isBaseClassOf(hierarchyItem)) { + if (hierarchyItem.overloadIndex > 1) { + // Subtract one for compatibility with earlier releases of API Documenter. + // (This will get revamped when we fix GitHub issue #1308) + qualifiedName += `_${hierarchyItem.overloadIndex - 1}`; + } + } + + switch (hierarchyItem.kind) { + case ApiItemKind.Model: + case ApiItemKind.EntryPoint: + case ApiItemKind.EnumMember: + case ApiItemKind.Package: + break; + default: + baseName += qualifiedName + '.'; + } + } + return baseName.slice(0, baseName.length - 1); + } + + private _getFilenameForApiItem(apiItem: ApiItem): string { + if (apiItem.kind === ApiItemKind.Package) { + return 'index.md'; + } + + let baseName = ''; + for (const hierarchyItem of apiItem.getHierarchy()) { + // For overloaded methods, add a suffix such as "MyClass.myMethod_2". + let qualifiedName: string = Utilities.getSafeFilenameForName( + hierarchyItem.displayName + ); + if (ApiParameterListMixin.isBaseClassOf(hierarchyItem)) { + if (hierarchyItem.overloadIndex > 1) { + // Subtract one for compatibility with earlier releases of API Documenter. + // (This will get revamped when we fix GitHub issue #1308) + qualifiedName += `_${hierarchyItem.overloadIndex - 1}`; + } + } + + switch (hierarchyItem.kind) { + case ApiItemKind.Model: + case ApiItemKind.EntryPoint: + case ApiItemKind.EnumMember: + break; + case ApiItemKind.Package: + baseName = Utilities.getSafeFilenameForName( + PackageName.getUnscopedName(hierarchyItem.displayName) + ); + break; + default: + baseName += '.' + qualifiedName; + } + } + return baseName + '.md'; + } + + private _getLinkFilenameForApiItem(apiItem: ApiItem): string { + return './' + this._getFilenameForApiItem(apiItem); + } + + private _deleteOldOutputFiles(): void { + console.log('Deleting old output from ' + this._outputFolder); + FileSystem.ensureEmptyFolder(this._outputFolder); + } +} diff --git a/remote/test/puppeteer/utils/internal/job.ts b/remote/test/puppeteer/utils/internal/job.ts new file mode 100644 index 0000000000..2e331a7552 --- /dev/null +++ b/remote/test/puppeteer/utils/internal/job.ts @@ -0,0 +1,161 @@ +import {createHash} from 'crypto'; +import {existsSync, Stats} from 'fs'; +import {mkdir, readFile, stat, writeFile} from 'fs/promises'; +import {glob} from 'glob'; +import {tmpdir} from 'os'; +import {dirname, join, resolve} from 'path'; +import {chdir} from 'process'; + +const packageRoot = resolve(join(__dirname, '..', '..')); +chdir(packageRoot); + +interface JobContext { + name: string; + inputs: string[]; + outputs: string[]; +} + +class JobBuilder { + #inputs: string[] = []; + #outputs: string[] = []; + #callback: (ctx: JobContext) => Promise<void>; + #name: string; + #value = ''; + #force = false; + + constructor(name: string, callback: (ctx: JobContext) => Promise<void>) { + this.#name = name; + this.#callback = callback; + } + + get jobHash(): string { + return createHash('sha256').update(this.#name).digest('hex'); + } + + force() { + this.#force = true; + return this; + } + + value(value: string) { + this.#value = value; + return this; + } + + inputs(inputs: string[]): JobBuilder { + this.#inputs = inputs.flatMap(value => { + if (glob.hasMagic(value)) { + return glob.sync(value).map(value => { + // Glob doesn't support `\` on Windows, so we join here. + return join(packageRoot, value); + }); + } + return join(packageRoot, value); + }); + return this; + } + + outputs(outputs: string[]): JobBuilder { + if (!this.#name) { + this.#name = outputs.join(' and '); + } + + this.#outputs = outputs.map(value => { + return join(packageRoot, value); + }); + return this; + } + + async build(): Promise<void> { + console.log(`Running job ${this.#name}...`); + // For debugging. + if (this.#force) { + return this.#run(); + } + // In case we deleted an output file on purpose. + if (!this.getOutputStats()) { + return this.#run(); + } + // Run if the job has a value, but it changes. + if (this.#value) { + if (!(await this.isValueDifferent())) { + return; + } + return this.#run(); + } + // Always run when there is no output. + if (!this.#outputs.length) { + return this.#run(); + } + // Make-like comparator. + if (!(await this.areInputsNewer())) { + return; + } + return this.#run(); + } + + async isValueDifferent(): Promise<boolean> { + const file = join(tmpdir(), `puppeteer/${this.jobHash}.txt`); + await mkdir(dirname(file), {recursive: true}); + if (!existsSync(file)) { + await writeFile(file, this.#value); + return true; + } + return this.#value !== (await readFile(file, 'utf8')); + } + + #outputStats?: Stats[]; + async getOutputStats(): Promise<Stats[] | undefined> { + if (this.#outputStats) { + return this.#outputStats; + } + try { + this.#outputStats = await Promise.all( + this.#outputs.map(output => { + return stat(output); + }) + ); + } catch {} + return this.#outputStats; + } + + async areInputsNewer(): Promise<boolean> { + const inputStats = await Promise.all( + this.#inputs.map(input => { + return stat(input); + }) + ); + const outputStats = await this.getOutputStats(); + if ( + outputStats && + outputStats.reduce(reduceMinTime, Infinity) > + inputStats.reduce(reduceMaxTime, 0) + ) { + return false; + } + return true; + } + + #run(): Promise<void> { + return this.#callback({ + name: this.#name, + inputs: this.#inputs, + outputs: this.#outputs, + }); + } +} + +export const job = ( + name: string, + callback: (ctx: JobContext) => Promise<void> +): JobBuilder => { + return new JobBuilder(name, callback); +}; + +const reduceMaxTime = (time: number, stat: Stats) => { + return time < stat.mtimeMs ? stat.mtimeMs : time; +}; + +const reduceMinTime = (time: number, stat: Stats) => { + return time > stat.mtimeMs ? stat.mtimeMs : time; +}; diff --git a/remote/test/puppeteer/utils/internal/util.ts b/remote/test/puppeteer/utils/internal/util.ts new file mode 100644 index 0000000000..4ebbe8b86b --- /dev/null +++ b/remote/test/puppeteer/utils/internal/util.ts @@ -0,0 +1,14 @@ +import {spawnSync} from 'child_process'; + +export const spawnAndLog = (...args: string[]): void => { + const {stdout, stderr} = spawnSync(args[0]!, args.slice(1), { + encoding: 'utf-8', + shell: true, + }); + if (stdout) { + console.log(stdout); + } + if (stderr) { + console.error(stderr); + } +}; diff --git a/remote/test/puppeteer/utils/prepare_puppeteer_core.js b/remote/test/puppeteer/utils/prepare_puppeteer_core.js new file mode 100755 index 0000000000..fe7a0dfc73 --- /dev/null +++ b/remote/test/puppeteer/utils/prepare_puppeteer_core.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs'); +const path = require('path'); + +const packagePath = path.join(__dirname, '..', 'package.json'); +const json = require(packagePath); + +delete json.scripts.install; + +json.name = 'puppeteer-core'; +json.main = './lib/cjs/puppeteer/puppeteer-core.js'; +json.exports['.'].import = './lib/esm/puppeteer/puppeteer-core.js'; +json.exports['.'].require = './lib/cjs/puppeteer/puppeteer-core.js'; + +fs.writeFileSync(packagePath, JSON.stringify(json, null, ' ')); diff --git a/remote/test/puppeteer/utils/remove_version_suffix.js b/remote/test/puppeteer/utils/remove_version_suffix.js new file mode 100644 index 0000000000..091a35ec9b --- /dev/null +++ b/remote/test/puppeteer/utils/remove_version_suffix.js @@ -0,0 +1,26 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs'); +const json = fs.readFileSync('./package.json', 'utf8').toString(); +const pkg = JSON.parse(json); +const oldVersion = pkg.version; +const version = oldVersion.replace(/-post$/, ''); +const updated = json.replace( + `"version": "${oldVersion}"`, + `"version": "${version}"` +); +fs.writeFileSync('./package.json', updated); diff --git a/remote/test/puppeteer/utils/testserver/LICENSE b/remote/test/puppeteer/utils/testserver/LICENSE new file mode 100644 index 0000000000..afdfe50e72 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/remote/test/puppeteer/utils/testserver/README.md b/remote/test/puppeteer/utils/testserver/README.md new file mode 100644 index 0000000000..d22b2da449 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/README.md @@ -0,0 +1,18 @@ +# TestServer + +This test server is used internally by Puppeteer to test Puppeteer itself. + +### Example + +```ts +const {TestServer} = require('@pptr/testserver'); + +(async(() => { + const httpServer = await TestServer.create(__dirname, 8000), + const httpsServer = await TestServer.createHTTPS(__dirname, 8001) + httpServer.setRoute('/hello', (req, res) => { + res.end('Hello, world!'); + }); + console.log('HTTP and HTTPS servers are running!'); +})(); +``` diff --git a/remote/test/puppeteer/utils/testserver/cert.pem b/remote/test/puppeteer/utils/testserver/cert.pem new file mode 100644 index 0000000000..fd3838535a --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWDCCAkCgAwIBAgIUM8Tmw+D1j+eVz9x9So4zRVqFsKowDQYJKoZIhvcNAQEL +BQAwGjEYMBYGA1UEAwwPcHVwcGV0ZWVyLXRlc3RzMB4XDTIwMDUxMzA4MDQyOVoX +DTMwMDUxMTA4MDQyOVowGjEYMBYGA1UEAwwPcHVwcGV0ZWVyLXRlc3RzMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApWbbhgc6CnWywd8xGETT1mfLi3wi +KIbpAUHghLF4sj0jXz8vLh/4oicpQ12d6bsz+IAi7qrdXNh11P5nEej6/Gx4fWzB +gGdrJFGPqsvXuhYdzZAmy6xOaWcLIJeQ543bXv3YeST7EGRXJBc/ocTo2jIGTGjq +hksFaid910VQlX3KGOLTDMUCk00TeEYBTTUx47PWoIsxVqbl2RzVXRSWL5hlPWlW +29/BQtBGmsXxZyWtqqHudiUulGBSr4LcPyicZLI8nqCqD0ioS0TEmGh61nRBuwBa +xmLCvPmpt0+sDuOU+1bme3w8juvTVToBIFxGB86rADd3ys+8NeZzXqi+bQIDAQAB +o4GVMIGSMB0GA1UdDgQWBBT/m3vdkZpQyVQFdYrKHVoAHXDFODAfBgNVHSMEGDAW +gBT/m3vdkZpQyVQFdYrKHVoAHXDFODAPBgNVHRMBAf8EBTADAQH/MD8GA1UdEQQ4 +MDaCGHd3dy5wdXBwZXRlZXItdGVzdHMudGVzdIIad3d3LnB1cHBldGVlci10ZXN0 +cy0xLnRlc3QwDQYJKoZIhvcNAQELBQADggEBAI1qp5ZppV1R3e8XxzwwkFDPFN8W +Pe3AoqhAKyJnJl1NUn9q3sroEeSQRhODWUHCd7lENzhsT+3mzonNNkN9B/hq0rpK +KHHczXILDqdyuxH3LxQ1VHGE8VN2NbdkfobtzAsA3woiJxOuGeusXJnKB4kJQeIP +V+BMEZWeaSDC2PREkG7GOezmE1/WDUCYaorPw2whdCA5wJvTW3zXpJjYhfsld+5z +KuErx4OCxRJij73/BD9SpLxDEY1cdl819F1IvxsRGhmTIaSly2hQLrhOgo1jgZtV +FGCa6DSlXnQGLaV+N+ssR0lkCksNrNBVDfA1bP5bT/4VCcwUWwm9TUeF0Qo= +-----END CERTIFICATE----- diff --git a/remote/test/puppeteer/utils/testserver/key.pem b/remote/test/puppeteer/utils/testserver/key.pem new file mode 100644 index 0000000000..cbc3acb229 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQClZtuGBzoKdbLB +3zEYRNPWZ8uLfCIohukBQeCEsXiyPSNfPy8uH/iiJylDXZ3puzP4gCLuqt1c2HXU +/mcR6Pr8bHh9bMGAZ2skUY+qy9e6Fh3NkCbLrE5pZwsgl5Dnjdte/dh5JPsQZFck +Fz+hxOjaMgZMaOqGSwVqJ33XRVCVfcoY4tMMxQKTTRN4RgFNNTHjs9agizFWpuXZ +HNVdFJYvmGU9aVbb38FC0EaaxfFnJa2qoe52JS6UYFKvgtw/KJxksjyeoKoPSKhL +RMSYaHrWdEG7AFrGYsK8+am3T6wO45T7VuZ7fDyO69NVOgEgXEYHzqsAN3fKz7w1 +5nNeqL5tAgMBAAECggEAKPveo0xBHnxhidZzBM9xKixX7D0a/a3IKI6ZQmfzPz8U +97HhT+2OHyfS+qVEzribPRULEtZ1uV7Ne7R5958iKc/63yFGpTl6++nVzn1p++sl +AV2Zr1gHqehlgnLr7eRhmh0OOZ5nM32ZdhDorH3tMLu6gc5xZktKkS4t6Vx8hj3a +Docx+rbawp8GRd0p7I6vzIE3bsDab8hC+RTRO63q2G0BqgKwV9ZNtJxQgcDJ5L8N +6gtM2z5nKXAIOCbCQYa1PsrDh3IRA/ZNxEeA9G3YQjwlZYCWmdRRplgDraYxcTBO +oQGjaLwICNdcprMacPD6cCSgrI+PadzyMsAuk9SgpQKBgQDO9PT4gK40Pm+Damxv ++tWYBFmvn3vasmyolc1zVDltsxQbQTjKhVpTLXTTGmrIhDXEIIV9I4rg164WptQs +6Brp2EwYR7ZJIrjvXs/9i2QTW1ZXvhdiWpB3s+RXD5VHGovHUadcI6wOgw2Cl+Jk +zXjSIgyXKM99N1MAonuR7DyzTwKBgQDMmPX+9vWZMpS/gc6JLQiPPoGszE6tYjXg +W3LpRUNqmO0/bDDjslbebDgrGAmhlkJlxzH6gz96VmGm1evEGPEet3euy8S9zuM3 +LCgEM9Ulqa3JbInwtKupmKv76Im+XWLLSxAXbfiel1zFRRwxI99A3ad0QRZ6Bov5 +3cHJBwvzgwKBgAU5HW2gIcVjxgC1EOOKmxVpFrJd/gw48JEYpsTAXWqtWFaPwNUr +pGnw/b/OLN++pnS6tWPBH+Ioz1X3A+fWO8enE9SRCsKxw6UW6XzmpbHvXjB8ta5f +xsGeoqan2AahXuG659RlehQrro2bM7WDkgcLoPG3r/TjDo83ipLWOXn1AoGAKWiL +4R56dpcWI+xRsNG8ecFc3Ww8QDswTEg16aBrFJf+7GcpPexKSJn+hDpJOLsAlTjL +lLgbkNcKzIlfPkEOC/l175quJvxIYFI/hxo2eXjuA2ZERMNMOvb7V/CocC7WX+7B +Qvyu5OodjI+ANTHdbXNvAMhrlCbfDaMkJVuXv6ECgYBzvY4aYmVoFsr+72/EfLls +Dz9pi55tUUWc61w6ovd+iliawvXeGi4wibtTH4iGj/C2sJIaMmOD99NQ7Oi/x89D +oMgSUemkoFL8FGsZGyZ7szqxyON1jP42Bm2MQrW5kIf7Y4yaIGhoak5JNxn2JUyV +gupVbY1mQ1GTPByxHeLh1w== +-----END PRIVATE KEY----- diff --git a/remote/test/puppeteer/utils/testserver/lib/index.d.ts b/remote/test/puppeteer/utils/testserver/lib/index.d.ts new file mode 100644 index 0000000000..055c9863d7 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/lib/index.d.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// <reference types="node" /> +/// <reference types="node" /> +import { IncomingMessage, ServerResponse } from 'http'; +import { ServerOptions as HttpsServerOptions } from 'https'; +declare type TestIncomingMessage = IncomingMessage & { + postBody?: Promise<string>; +}; +export declare class TestServer { + #private; + PORT: number; + PREFIX: string; + CROSS_PROCESS_PREFIX: string; + EMPTY_PAGE: string; + static create(dirPath: string): Promise<TestServer>; + static createHTTPS(dirPath: string): Promise<TestServer>; + constructor(dirPath: string, sslOptions?: HttpsServerOptions); + get port(): number; + enableHTTPCache(pathPrefix: string): void; + setAuth(path: string, username: string, password: string): void; + enableGzip(path: string): void; + setCSP(path: string, csp: string): void; + stop(): Promise<void>; + setRoute(path: string, handler: (req: IncomingMessage, res: ServerResponse) => void): void; + setRedirect(from: string, to: string): void; + waitForRequest(path: string): Promise<TestIncomingMessage>; + reset(): void; + serveFile(request: IncomingMessage, response: ServerResponse, pathName: string): void; +} +export {}; +//# sourceMappingURL=index.d.ts.map
\ No newline at end of file diff --git a/remote/test/puppeteer/utils/testserver/lib/index.d.ts.map b/remote/test/puppeteer/utils/testserver/lib/index.d.ts.map new file mode 100644 index 0000000000..a2c07af2df --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/lib/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;;;AAIH,OAAO,EAEL,eAAe,EAGf,cAAc,EACf,MAAM,MAAM,CAAC;AACd,OAAO,EAGL,aAAa,IAAI,kBAAkB,EACpC,MAAM,OAAO,CAAC;AAcf,aAAK,mBAAmB,GAAG,eAAe,GAAG;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;CAAC,CAAC;AAE1E,qBAAa,UAAU;;IACrB,IAAI,EAAG,MAAM,CAAC;IACd,MAAM,EAAG,MAAM,CAAC;IAChB,oBAAoB,EAAG,MAAM,CAAC;IAC9B,UAAU,EAAG,MAAM,CAAC;WAmBP,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;WAY5C,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;gBAgBlD,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,kBAAkB;IA2B5D,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAIzC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAI/D,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI9B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAIjC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAW3B,QAAQ,CACN,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,IAAI,GAC3D,IAAI;IAIP,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAO3C,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAe1D,KAAK,IAAI,IAAI;IA8Db,SAAS,CACP,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,cAAc,EACxB,QAAQ,EAAE,MAAM,GACf,IAAI;CAoDR"}
\ No newline at end of file diff --git a/remote/test/puppeteer/utils/testserver/lib/index.js b/remote/test/puppeteer/utils/testserver/lib/index.js new file mode 100644 index 0000000000..8cfcc71139 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/lib/index.js @@ -0,0 +1,261 @@ +"use strict"; +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; +}; +var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +var _TestServer_dirPath, _TestServer_server, _TestServer_wsServer, _TestServer_startTime, _TestServer_cachedPathPrefix, _TestServer_connections, _TestServer_routes, _TestServer_auths, _TestServer_csp, _TestServer_gzipRoutes, _TestServer_requestSubscribers, _TestServer_onServerConnection, _TestServer_onRequest, _TestServer_onWebSocketConnection; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.TestServer = void 0; +const assert_1 = __importDefault(require("assert")); +const fs_1 = require("fs"); +const http_1 = require("http"); +const https_1 = require("https"); +const mime_1 = require("mime"); +const path_1 = require("path"); +const ws_1 = require("ws"); +const zlib_1 = require("zlib"); +class TestServer { + constructor(dirPath, sslOptions) { + _TestServer_dirPath.set(this, void 0); + _TestServer_server.set(this, void 0); + _TestServer_wsServer.set(this, void 0); + _TestServer_startTime.set(this, new Date()); + _TestServer_cachedPathPrefix.set(this, void 0); + _TestServer_connections.set(this, new Set()); + _TestServer_routes.set(this, new Map()); + _TestServer_auths.set(this, new Map()); + _TestServer_csp.set(this, new Map()); + _TestServer_gzipRoutes.set(this, new Set()); + _TestServer_requestSubscribers.set(this, new Map()); + _TestServer_onServerConnection.set(this, (connection) => { + __classPrivateFieldGet(this, _TestServer_connections, "f").add(connection); + // ECONNRESET is a legit error given + // that tab closing simply kills process. + connection.on('error', error => { + if (error.code !== 'ECONNRESET') { + throw error; + } + }); + connection.once('close', () => { + return __classPrivateFieldGet(this, _TestServer_connections, "f").delete(connection); + }); + }); + _TestServer_onRequest.set(this, (request, response) => { + request.on('error', (error) => { + if (error.code === 'ECONNRESET') { + response.end(); + } + else { + throw error; + } + }); + request.postBody = new Promise(resolve => { + let body = ''; + request.on('data', (chunk) => { + return (body += chunk); + }); + request.on('end', () => { + return resolve(body); + }); + }); + (0, assert_1.default)(request.url); + const url = new URL(request.url, `https://${request.headers.host}`); + const path = url.pathname + url.search; + const auth = __classPrivateFieldGet(this, _TestServer_auths, "f").get(path); + if (auth) { + const credentials = Buffer.from((request.headers.authorization || '').split(' ')[1] || '', 'base64').toString(); + if (credentials !== `${auth.username}:${auth.password}`) { + response.writeHead(401, { + 'WWW-Authenticate': 'Basic realm="Secure Area"', + }); + response.end('HTTP Error 401 Unauthorized: Access is denied'); + return; + } + } + const subscriber = __classPrivateFieldGet(this, _TestServer_requestSubscribers, "f").get(path); + if (subscriber) { + subscriber.resolve.call(undefined, request); + __classPrivateFieldGet(this, _TestServer_requestSubscribers, "f").delete(path); + } + const handler = __classPrivateFieldGet(this, _TestServer_routes, "f").get(path); + if (handler) { + handler.call(undefined, request, response); + } + else { + this.serveFile(request, response, path); + } + }); + _TestServer_onWebSocketConnection.set(this, (socket) => { + socket.send('opened'); + }); + __classPrivateFieldSet(this, _TestServer_dirPath, dirPath, "f"); + if (sslOptions) { + __classPrivateFieldSet(this, _TestServer_server, (0, https_1.createServer)(sslOptions, __classPrivateFieldGet(this, _TestServer_onRequest, "f")), "f"); + } + else { + __classPrivateFieldSet(this, _TestServer_server, (0, http_1.createServer)(__classPrivateFieldGet(this, _TestServer_onRequest, "f")), "f"); + } + __classPrivateFieldGet(this, _TestServer_server, "f").on('connection', __classPrivateFieldGet(this, _TestServer_onServerConnection, "f")); + __classPrivateFieldSet(this, _TestServer_wsServer, new ws_1.Server({ server: __classPrivateFieldGet(this, _TestServer_server, "f") }), "f"); + __classPrivateFieldGet(this, _TestServer_wsServer, "f").on('connection', __classPrivateFieldGet(this, _TestServer_onWebSocketConnection, "f")); + } + static async create(dirPath) { + let res; + const promise = new Promise(resolve => { + res = resolve; + }); + const server = new TestServer(dirPath); + __classPrivateFieldGet(server, _TestServer_server, "f").once('listening', res); + __classPrivateFieldGet(server, _TestServer_server, "f").listen(0); + await promise; + return server; + } + static async createHTTPS(dirPath) { + let res; + const promise = new Promise(resolve => { + res = resolve; + }); + const server = new TestServer(dirPath, { + key: (0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'key.pem')), + cert: (0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'cert.pem')), + passphrase: 'aaaa', + }); + __classPrivateFieldGet(server, _TestServer_server, "f").once('listening', res); + __classPrivateFieldGet(server, _TestServer_server, "f").listen(0); + await promise; + return server; + } + get port() { + return __classPrivateFieldGet(this, _TestServer_server, "f").address().port; + } + enableHTTPCache(pathPrefix) { + __classPrivateFieldSet(this, _TestServer_cachedPathPrefix, pathPrefix, "f"); + } + setAuth(path, username, password) { + __classPrivateFieldGet(this, _TestServer_auths, "f").set(path, { username, password }); + } + enableGzip(path) { + __classPrivateFieldGet(this, _TestServer_gzipRoutes, "f").add(path); + } + setCSP(path, csp) { + __classPrivateFieldGet(this, _TestServer_csp, "f").set(path, csp); + } + async stop() { + this.reset(); + for (const socket of __classPrivateFieldGet(this, _TestServer_connections, "f")) { + socket.destroy(); + } + __classPrivateFieldGet(this, _TestServer_connections, "f").clear(); + await new Promise(x => { + return __classPrivateFieldGet(this, _TestServer_server, "f").close(x); + }); + } + setRoute(path, handler) { + __classPrivateFieldGet(this, _TestServer_routes, "f").set(path, handler); + } + setRedirect(from, to) { + this.setRoute(from, (_, res) => { + res.writeHead(302, { location: to }); + res.end(); + }); + } + waitForRequest(path) { + const subscriber = __classPrivateFieldGet(this, _TestServer_requestSubscribers, "f").get(path); + if (subscriber) { + return subscriber.promise; + } + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + __classPrivateFieldGet(this, _TestServer_requestSubscribers, "f").set(path, { resolve, reject, promise }); + return promise; + } + reset() { + __classPrivateFieldGet(this, _TestServer_routes, "f").clear(); + __classPrivateFieldGet(this, _TestServer_auths, "f").clear(); + __classPrivateFieldGet(this, _TestServer_csp, "f").clear(); + __classPrivateFieldGet(this, _TestServer_gzipRoutes, "f").clear(); + const error = new Error('Static Server has been reset'); + for (const subscriber of __classPrivateFieldGet(this, _TestServer_requestSubscribers, "f").values()) { + subscriber.reject.call(undefined, error); + } + __classPrivateFieldGet(this, _TestServer_requestSubscribers, "f").clear(); + } + serveFile(request, response, pathName) { + if (pathName === '/') { + pathName = '/index.html'; + } + const filePath = (0, path_1.join)(__classPrivateFieldGet(this, _TestServer_dirPath, "f"), pathName.substring(1)); + if (__classPrivateFieldGet(this, _TestServer_cachedPathPrefix, "f") && filePath.startsWith(__classPrivateFieldGet(this, _TestServer_cachedPathPrefix, "f"))) { + if (request.headers['if-modified-since']) { + response.statusCode = 304; // not modified + response.end(); + return; + } + response.setHeader('Cache-Control', 'public, max-age=31536000'); + response.setHeader('Last-Modified', __classPrivateFieldGet(this, _TestServer_startTime, "f").toISOString()); + } + else { + response.setHeader('Cache-Control', 'no-cache, no-store'); + } + const csp = __classPrivateFieldGet(this, _TestServer_csp, "f").get(pathName); + if (csp) { + response.setHeader('Content-Security-Policy', csp); + } + (0, fs_1.readFile)(filePath, (err, data) => { + if (err) { + response.statusCode = 404; + response.end(`File not found: ${filePath}`); + return; + } + const mimeType = (0, mime_1.getType)(filePath); + if (mimeType) { + const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(mimeType); + const contentType = isTextEncoding + ? `${mimeType}; charset=utf-8` + : mimeType; + response.setHeader('Content-Type', contentType); + } + if (__classPrivateFieldGet(this, _TestServer_gzipRoutes, "f").has(pathName)) { + response.setHeader('Content-Encoding', 'gzip'); + (0, zlib_1.gzip)(data, (_, result) => { + response.end(result); + }); + } + else { + response.end(data); + } + }); + } +} +exports.TestServer = TestServer; +_TestServer_dirPath = new WeakMap(), _TestServer_server = new WeakMap(), _TestServer_wsServer = new WeakMap(), _TestServer_startTime = new WeakMap(), _TestServer_cachedPathPrefix = new WeakMap(), _TestServer_connections = new WeakMap(), _TestServer_routes = new WeakMap(), _TestServer_auths = new WeakMap(), _TestServer_csp = new WeakMap(), _TestServer_gzipRoutes = new WeakMap(), _TestServer_requestSubscribers = new WeakMap(), _TestServer_onServerConnection = new WeakMap(), _TestServer_onRequest = new WeakMap(), _TestServer_onWebSocketConnection = new WeakMap(); +//# sourceMappingURL=index.js.map
\ No newline at end of file diff --git a/remote/test/puppeteer/utils/testserver/lib/index.js.map b/remote/test/puppeteer/utils/testserver/lib/index.js.map new file mode 100644 index 0000000000..2ab772d086 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/lib/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;;;;;;;;;;;;;;;;AAEH,oDAA4B;AAC5B,2BAA0C;AAC1C,+BAMc;AACd,iCAIe;AACf,+BAA4C;AAE5C,+BAA0B;AAE1B,2BAAwD;AACxD,+BAA0B;AAU1B,MAAa,UAAU;IAmDrB,YAAY,OAAe,EAAE,UAA+B;QA7C5D,sCAAiB;QACjB,qCAAkC;QAClC,uCAA2B;QAE3B,gCAAa,IAAI,IAAI,EAAE,EAAC;QACxB,+CAA2B;QAE3B,kCAAe,IAAI,GAAG,EAAU,EAAC;QACjC,6BAAU,IAAI,GAAG,EAGd,EAAC;QACJ,4BAAS,IAAI,GAAG,EAAgD,EAAC;QACjE,0BAAO,IAAI,GAAG,EAAkB,EAAC;QACjC,iCAAc,IAAI,GAAG,EAAU,EAAC;QAChC,yCAAsB,IAAI,GAAG,EAAsB,EAAC;QA2CpD,yCAAsB,CAAC,UAAkB,EAAQ,EAAE;YACjD,uBAAA,IAAI,+BAAa,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAClC,oCAAoC;YACpC,yCAAyC;YACzC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE;gBAC7B,IAAK,KAA+B,CAAC,IAAI,KAAK,YAAY,EAAE;oBAC1D,MAAM,KAAK,CAAC;iBACb;YACH,CAAC,CAAC,CAAC;YACH,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE;gBAC5B,OAAO,uBAAA,IAAI,+BAAa,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAC9C,CAAC,CAAC,CAAC;QACL,CAAC,EAAC;QA0EF,gCAA8B,CAC5B,OAA4B,EAC5B,QAAQ,EACF,EAAE;YACR,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAqB,EAAE,EAAE;gBAC5C,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,EAAE;oBAC/B,QAAQ,CAAC,GAAG,EAAE,CAAC;iBAChB;qBAAM;oBACL,MAAM,KAAK,CAAC;iBACb;YACH,CAAC,CAAC,CAAC;YACH,OAAO,CAAC,QAAQ,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;gBACvC,IAAI,IAAI,GAAG,EAAE,CAAC;gBACd,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;oBACnC,OAAO,CAAC,IAAI,IAAI,KAAK,CAAC,CAAC;gBACzB,CAAC,CAAC,CAAC;gBACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;oBACrB,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;gBACvB,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YACH,IAAA,gBAAM,EAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACpB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,WAAW,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;YACpE,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC;YACvC,MAAM,IAAI,GAAG,uBAAA,IAAI,yBAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACnC,IAAI,IAAI,EAAE;gBACR,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAC7B,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EACzD,QAAQ,CACT,CAAC,QAAQ,EAAE,CAAC;gBACb,IAAI,WAAW,KAAK,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,EAAE,EAAE;oBACvD,QAAQ,CAAC,SAAS,CAAC,GAAG,EAAE;wBACtB,kBAAkB,EAAE,2BAA2B;qBAChD,CAAC,CAAC;oBACH,QAAQ,CAAC,GAAG,CAAC,+CAA+C,CAAC,CAAC;oBAC9D,OAAO;iBACR;aACF;YACD,MAAM,UAAU,GAAG,uBAAA,IAAI,sCAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACtD,IAAI,UAAU,EAAE;gBACd,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;gBAC5C,uBAAA,IAAI,sCAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;aACvC;YACD,MAAM,OAAO,GAAG,uBAAA,IAAI,0BAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,OAAO,EAAE;gBACX,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;aAC5C;iBAAM;gBACL,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;aACzC;QACH,CAAC,EAAC;QAuDF,4CAAyB,CAAC,MAAiB,EAAQ,EAAE;YACnD,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACxB,CAAC,EAAC;QA3MA,uBAAA,IAAI,uBAAY,OAAO,MAAA,CAAC;QAExB,IAAI,UAAU,EAAE;YACd,uBAAA,IAAI,sBAAW,IAAA,oBAAiB,EAAC,UAAU,EAAE,uBAAA,IAAI,6BAAW,CAAC,MAAA,CAAC;SAC/D;aAAM;YACL,uBAAA,IAAI,sBAAW,IAAA,mBAAgB,EAAC,uBAAA,IAAI,6BAAW,CAAC,MAAA,CAAC;SAClD;QACD,uBAAA,IAAI,0BAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,uBAAA,IAAI,sCAAoB,CAAC,CAAC;QACxD,uBAAA,IAAI,wBAAa,IAAI,WAAe,CAAC,EAAC,MAAM,EAAE,uBAAA,IAAI,0BAAQ,EAAC,CAAC,MAAA,CAAC;QAC7D,uBAAA,IAAI,4BAAU,CAAC,EAAE,CAAC,YAAY,EAAE,uBAAA,IAAI,yCAAuB,CAAC,CAAC;IAC/D,CAAC;IAvCD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAe;QACjC,IAAI,GAA8B,CAAC;QACnC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;YACpC,GAAG,GAAG,OAAO,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;QACvC,uBAAA,MAAM,0BAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;QACtC,uBAAA,MAAM,0BAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,OAAO,CAAC;QACd,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,OAAe;QACtC,IAAI,GAA8B,CAAC;QACnC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;YACpC,GAAG,GAAG,OAAO,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,OAAO,EAAE;YACrC,GAAG,EAAE,IAAA,iBAAY,EAAC,IAAA,WAAI,EAAC,SAAS,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;YACnD,IAAI,EAAE,IAAA,iBAAY,EAAC,IAAA,WAAI,EAAC,SAAS,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;YACrD,UAAU,EAAE,MAAM;SACnB,CAAC,CAAC;QACH,uBAAA,MAAM,0BAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;QACtC,uBAAA,MAAM,0BAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,OAAO,CAAC;QACd,OAAO,MAAM,CAAC;IAChB,CAAC;IA6BD,IAAI,IAAI;QACN,OAAQ,uBAAA,IAAI,0BAAQ,CAAC,OAAO,EAAkB,CAAC,IAAI,CAAC;IACtD,CAAC;IAED,eAAe,CAAC,UAAkB;QAChC,uBAAA,IAAI,gCAAqB,UAAU,MAAA,CAAC;IACtC,CAAC;IAED,OAAO,CAAC,IAAY,EAAE,QAAgB,EAAE,QAAgB;QACtD,uBAAA,IAAI,yBAAO,CAAC,GAAG,CAAC,IAAI,EAAE,EAAC,QAAQ,EAAE,QAAQ,EAAC,CAAC,CAAC;IAC9C,CAAC;IAED,UAAU,CAAC,IAAY;QACrB,uBAAA,IAAI,8BAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,MAAM,CAAC,IAAY,EAAE,GAAW;QAC9B,uBAAA,IAAI,uBAAK,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,KAAK,MAAM,MAAM,IAAI,uBAAA,IAAI,+BAAa,EAAE;YACtC,MAAM,CAAC,OAAO,EAAE,CAAC;SAClB;QACD,uBAAA,IAAI,+BAAa,CAAC,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE;YACpB,OAAO,uBAAA,IAAI,0BAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC/B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,QAAQ,CACN,IAAY,EACZ,OAA4D;QAE5D,uBAAA,IAAI,0BAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAClC,CAAC;IAED,WAAW,CAAC,IAAY,EAAE,EAAU;QAClC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;YAC7B,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAC,QAAQ,EAAE,EAAE,EAAC,CAAC,CAAC;YACnC,GAAG,CAAC,GAAG,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC;IAED,cAAc,CAAC,IAAY;QACzB,MAAM,UAAU,GAAG,uBAAA,IAAI,sCAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtD,IAAI,UAAU,EAAE;YACd,OAAO,UAAU,CAAC,OAAO,CAAC;SAC3B;QACD,IAAI,OAA0C,CAAC;QAC/C,IAAI,MAAiC,CAAC;QACtC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAkB,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACxD,OAAO,GAAG,GAAG,CAAC;YACd,MAAM,GAAG,GAAG,CAAC;QACf,CAAC,CAAC,CAAC;QACH,uBAAA,IAAI,sCAAoB,CAAC,GAAG,CAAC,IAAI,EAAE,EAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAC,CAAC,CAAC;QAC/D,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK;QACH,uBAAA,IAAI,0BAAQ,CAAC,KAAK,EAAE,CAAC;QACrB,uBAAA,IAAI,yBAAO,CAAC,KAAK,EAAE,CAAC;QACpB,uBAAA,IAAI,uBAAK,CAAC,KAAK,EAAE,CAAC;QAClB,uBAAA,IAAI,8BAAY,CAAC,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QACxD,KAAK,MAAM,UAAU,IAAI,uBAAA,IAAI,sCAAoB,CAAC,MAAM,EAAE,EAAE;YAC1D,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;SAC1C;QACD,uBAAA,IAAI,sCAAoB,CAAC,KAAK,EAAE,CAAC;IACnC,CAAC;IAoDD,SAAS,CACP,OAAwB,EACxB,QAAwB,EACxB,QAAgB;QAEhB,IAAI,QAAQ,KAAK,GAAG,EAAE;YACpB,QAAQ,GAAG,aAAa,CAAC;SAC1B;QACD,MAAM,QAAQ,GAAG,IAAA,WAAI,EAAC,uBAAA,IAAI,2BAAS,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QAE5D,IAAI,uBAAA,IAAI,oCAAkB,IAAI,QAAQ,CAAC,UAAU,CAAC,uBAAA,IAAI,oCAAkB,CAAC,EAAE;YACzE,IAAI,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE;gBACxC,QAAQ,CAAC,UAAU,GAAG,GAAG,CAAC,CAAC,eAAe;gBAC1C,QAAQ,CAAC,GAAG,EAAE,CAAC;gBACf,OAAO;aACR;YACD,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,0BAA0B,CAAC,CAAC;YAChE,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,uBAAA,IAAI,6BAAW,CAAC,WAAW,EAAE,CAAC,CAAC;SACpE;aAAM;YACL,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,oBAAoB,CAAC,CAAC;SAC3D;QACD,MAAM,GAAG,GAAG,uBAAA,IAAI,uBAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACpC,IAAI,GAAG,EAAE;YACP,QAAQ,CAAC,SAAS,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;SACpD;QAED,IAAA,aAAQ,EAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;YAC/B,IAAI,GAAG,EAAE;gBACP,QAAQ,CAAC,UAAU,GAAG,GAAG,CAAC;gBAC1B,QAAQ,CAAC,GAAG,CAAC,mBAAmB,QAAQ,EAAE,CAAC,CAAC;gBAC5C,OAAO;aACR;YACD,MAAM,QAAQ,GAAG,IAAA,cAAW,EAAC,QAAQ,CAAC,CAAC;YACvC,IAAI,QAAQ,EAAE;gBACZ,MAAM,cAAc,GAAG,yCAAyC,CAAC,IAAI,CACnE,QAAQ,CACT,CAAC;gBACF,MAAM,WAAW,GAAG,cAAc;oBAChC,CAAC,CAAC,GAAG,QAAQ,iBAAiB;oBAC9B,CAAC,CAAC,QAAQ,CAAC;gBACb,QAAQ,CAAC,SAAS,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;aACjD;YACD,IAAI,uBAAA,IAAI,8BAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;gBAClC,QAAQ,CAAC,SAAS,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;gBAC/C,IAAA,WAAI,EAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;oBACvB,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBACvB,CAAC,CAAC,CAAC;aACJ;iBAAM;gBACL,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;aACpB;QACH,CAAC,CAAC,CAAC;IACL,CAAC;CAKF;AAhQD,gCAgQC"}
\ No newline at end of file diff --git a/remote/test/puppeteer/utils/testserver/package.json b/remote/test/puppeteer/utils/testserver/package.json new file mode 100644 index 0000000000..1029f16d0a --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pptr/testserver", + "version": "0.5.0", + "description": "testing server", + "main": "lib/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/puppeteer/puppeteer/tree/main/utils/testserver" + }, + "author": "The Chromium Authors", + "license": "Apache-2.0" +} diff --git a/remote/test/puppeteer/utils/testserver/src/index.ts b/remote/test/puppeteer/utils/testserver/src/index.ts new file mode 100644 index 0000000000..c98ed9b4d0 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/src/index.ts @@ -0,0 +1,302 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import {readFile, readFileSync} from 'fs'; +import { + createServer as createHttpServer, + IncomingMessage, + RequestListener, + Server as HttpServer, + ServerResponse, +} from 'http'; +import { + createServer as createHttpsServer, + Server as HttpsServer, + ServerOptions as HttpsServerOptions, +} from 'https'; +import {getType as getMimeType} from 'mime'; +import {AddressInfo} from 'net'; +import {join} from 'path'; +import {Duplex} from 'stream'; +import {Server as WebSocketServer, WebSocket} from 'ws'; +import {gzip} from 'zlib'; + +interface Subscriber { + resolve: (msg: IncomingMessage) => void; + reject: (err?: Error) => void; + promise: Promise<IncomingMessage>; +} + +type TestIncomingMessage = IncomingMessage & {postBody?: Promise<string>}; + +export class TestServer { + PORT!: number; + PREFIX!: string; + CROSS_PROCESS_PREFIX!: string; + EMPTY_PAGE!: string; + + #dirPath: string; + #server: HttpsServer | HttpServer; + #wsServer: WebSocketServer; + + #startTime = new Date(); + #cachedPathPrefix?: string; + + #connections = new Set<Duplex>(); + #routes = new Map< + string, + (msg: IncomingMessage, res: ServerResponse) => void + >(); + #auths = new Map<string, {username: string; password: string}>(); + #csp = new Map<string, string>(); + #gzipRoutes = new Set<string>(); + #requestSubscribers = new Map<string, Subscriber>(); + + static async create(dirPath: string): Promise<TestServer> { + let res!: (value: unknown) => void; + const promise = new Promise(resolve => { + res = resolve; + }); + const server = new TestServer(dirPath); + server.#server.once('listening', res); + server.#server.listen(0); + await promise; + return server; + } + + static async createHTTPS(dirPath: string): Promise<TestServer> { + let res!: (value: unknown) => void; + const promise = new Promise(resolve => { + res = resolve; + }); + const server = new TestServer(dirPath, { + key: readFileSync(join(__dirname, '..', 'key.pem')), + cert: readFileSync(join(__dirname, '..', 'cert.pem')), + passphrase: 'aaaa', + }); + server.#server.once('listening', res); + server.#server.listen(0); + await promise; + return server; + } + + constructor(dirPath: string, sslOptions?: HttpsServerOptions) { + this.#dirPath = dirPath; + + if (sslOptions) { + this.#server = createHttpsServer(sslOptions, this.#onRequest); + } else { + this.#server = createHttpServer(this.#onRequest); + } + this.#server.on('connection', this.#onServerConnection); + this.#wsServer = new WebSocketServer({server: this.#server}); + this.#wsServer.on('connection', this.#onWebSocketConnection); + } + + #onServerConnection = (connection: Duplex): void => { + this.#connections.add(connection); + // ECONNRESET is a legit error given + // that tab closing simply kills process. + connection.on('error', error => { + if ((error as NodeJS.ErrnoException).code !== 'ECONNRESET') { + throw error; + } + }); + connection.once('close', () => { + return this.#connections.delete(connection); + }); + }; + + get port(): number { + return (this.#server.address() as AddressInfo).port; + } + + enableHTTPCache(pathPrefix: string): void { + this.#cachedPathPrefix = pathPrefix; + } + + setAuth(path: string, username: string, password: string): void { + this.#auths.set(path, {username, password}); + } + + enableGzip(path: string): void { + this.#gzipRoutes.add(path); + } + + setCSP(path: string, csp: string): void { + this.#csp.set(path, csp); + } + + async stop(): Promise<void> { + this.reset(); + for (const socket of this.#connections) { + socket.destroy(); + } + this.#connections.clear(); + await new Promise(x => { + return this.#server.close(x); + }); + } + + setRoute( + path: string, + handler: (req: IncomingMessage, res: ServerResponse) => void + ): void { + this.#routes.set(path, handler); + } + + setRedirect(from: string, to: string): void { + this.setRoute(from, (_, res) => { + res.writeHead(302, {location: to}); + res.end(); + }); + } + + waitForRequest(path: string): Promise<TestIncomingMessage> { + const subscriber = this.#requestSubscribers.get(path); + if (subscriber) { + return subscriber.promise; + } + let resolve!: (value: IncomingMessage) => void; + let reject!: (reason?: Error) => void; + const promise = new Promise<IncomingMessage>((res, rej) => { + resolve = res; + reject = rej; + }); + this.#requestSubscribers.set(path, {resolve, reject, promise}); + return promise; + } + + reset(): void { + this.#routes.clear(); + this.#auths.clear(); + this.#csp.clear(); + this.#gzipRoutes.clear(); + const error = new Error('Static Server has been reset'); + for (const subscriber of this.#requestSubscribers.values()) { + subscriber.reject.call(undefined, error); + } + this.#requestSubscribers.clear(); + } + + #onRequest: RequestListener = ( + request: TestIncomingMessage, + response + ): void => { + request.on('error', (error: {code: string}) => { + if (error.code === 'ECONNRESET') { + response.end(); + } else { + throw error; + } + }); + request.postBody = new Promise(resolve => { + let body = ''; + request.on('data', (chunk: string) => { + return (body += chunk); + }); + request.on('end', () => { + return resolve(body); + }); + }); + assert(request.url); + const url = new URL(request.url, `https://${request.headers.host}`); + const path = url.pathname + url.search; + const auth = this.#auths.get(path); + if (auth) { + const credentials = Buffer.from( + (request.headers.authorization || '').split(' ')[1] || '', + 'base64' + ).toString(); + if (credentials !== `${auth.username}:${auth.password}`) { + response.writeHead(401, { + 'WWW-Authenticate': 'Basic realm="Secure Area"', + }); + response.end('HTTP Error 401 Unauthorized: Access is denied'); + return; + } + } + const subscriber = this.#requestSubscribers.get(path); + if (subscriber) { + subscriber.resolve.call(undefined, request); + this.#requestSubscribers.delete(path); + } + const handler = this.#routes.get(path); + if (handler) { + handler.call(undefined, request, response); + } else { + this.serveFile(request, response, path); + } + }; + + serveFile( + request: IncomingMessage, + response: ServerResponse, + pathName: string + ): void { + if (pathName === '/') { + pathName = '/index.html'; + } + const filePath = join(this.#dirPath, pathName.substring(1)); + + if (this.#cachedPathPrefix && filePath.startsWith(this.#cachedPathPrefix)) { + if (request.headers['if-modified-since']) { + response.statusCode = 304; // not modified + response.end(); + return; + } + response.setHeader('Cache-Control', 'public, max-age=31536000'); + response.setHeader('Last-Modified', this.#startTime.toISOString()); + } else { + response.setHeader('Cache-Control', 'no-cache, no-store'); + } + const csp = this.#csp.get(pathName); + if (csp) { + response.setHeader('Content-Security-Policy', csp); + } + + readFile(filePath, (err, data) => { + if (err) { + response.statusCode = 404; + response.end(`File not found: ${filePath}`); + return; + } + const mimeType = getMimeType(filePath); + if (mimeType) { + const isTextEncoding = /^text\/|^application\/(javascript|json)/.test( + mimeType + ); + const contentType = isTextEncoding + ? `${mimeType}; charset=utf-8` + : mimeType; + response.setHeader('Content-Type', contentType); + } + if (this.#gzipRoutes.has(pathName)) { + response.setHeader('Content-Encoding', 'gzip'); + gzip(data, (_, result) => { + response.end(result); + }); + } else { + response.end(data); + } + }); + } + + #onWebSocketConnection = (socket: WebSocket): void => { + socket.send('opened'); + }; +} diff --git a/remote/test/puppeteer/utils/testserver/tsconfig.json b/remote/test/puppeteer/utils/testserver/tsconfig.json new file mode 100644 index 0000000000..c2576c2564 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "composite": true, + "module": "CommonJS", + "outDir": "lib", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/remote/test/puppeteer/utils/tsconfig.json b/remote/test/puppeteer/utils/tsconfig.json new file mode 100644 index 0000000000..2552d66004 --- /dev/null +++ b/remote/test/puppeteer/utils/tsconfig.json @@ -0,0 +1,12 @@ +/** + * This configuration only exists for the API Extractor tool and for VSCode to use. It is NOT the tsconfig used for compilation. + * For CJS builds, `tsconfig.cjs.json` is used, and for ESM, it's `tsconfig.esm.json`. + * See the details in CONTRIBUTING.md that describes our TypeScript setup. + */ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "module": "CommonJS" + } +} |