diff --git a/.eslintrc.yaml b/.eslintrc.yaml index b8b666983..88673e93f 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -29,10 +29,10 @@ rules: # Not possible while strictNullChecks is disabled in tsconfig. "@typescript-eslint/no-non-null-assertion": "error" no-unused-vars: off - "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "destructuredArrayIgnorePattern": "^_" }] + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "destructuredArrayIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_"}] no-use-before-define: off "@typescript-eslint/no-use-before-define": ["error", { "functions": false }] - "@typescript-eslint/no-var-requires": off # Remove this override once all files use ES6 style imports. + "@typescript-eslint/no-require-imports": off # Remove this override once all files use ES6 style imports. prefer-const: ["error", {"destructuring": "all"}] react/no-array-index-key: error react/prop-types: off # Remove this override once all props have been typed using PropTypes or TypeScript. diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3eca90d18..c257f4e67 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [20, 22, 24] + node: [24, 26] steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v6 @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [20, 22, 24] + node: [24, 26] steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v6 @@ -38,7 +38,7 @@ jobs: node-version: ${{ matrix.node }} # Build is implicit with the "prepare" life cycle script - run: npm ci --loglevel verbose - - run: npm run get-data + - run: npm run fetch-test-data - run: npx playwright install chromium - run: npm run smoke-test diff --git a/DEV_DOCS.md b/DEV_DOCS.md index 6f462d251..2e7928245 100644 --- a/DEV_DOCS.md +++ b/DEV_DOCS.md @@ -93,7 +93,7 @@ General advice for writing tests: > Smoke tests are currently very limited but we may expand these in the future -1. Fetch datasets with `npm run get-data`. +1. Fetch testing datasets with `npm run fetch-test-data`. 2. Install the testing browser with `npx playwright install chromium`. 3. Run `npm run smoke-test`. @@ -110,15 +110,9 @@ If embarking on this journey, consider using Playwright since it is already used Linting via eslint: `npm run lint`, typechecking via `npm type-check` -## Heroku review apps - - -A Heroku pipeline for this repository is connected to GitHub under the nextstrain-bot user account. The Review Apps feature facilitates manual review of changes by automatically creating a test instance from the PR source branch and adding a link to it on the GitHub PR page. These apps are based on configuration in [app.json](./app.json). - - #### Test on downstream repositories -Additionally, a GitHub Actions workflow has been set up to generate PRs in downstream repositories that reflect the new changes in Auspice. To use it, add the label [preview on auspice.us](https://github.com/nextstrain/auspice/labels/preview%20on%20auspice.us) and/or [preview on nextstrain.org](https://github.com/nextstrain/auspice/labels/preview%20on%20nextstrain.org). +A GitHub Actions workflow has been set up to generate PRs in downstream repositories that reflect the new changes in Auspice. To use it, add the label [preview on auspice.us](https://github.com/nextstrain/auspice/labels/preview%20on%20auspice.us) and/or [preview on nextstrain.org](https://github.com/nextstrain/auspice/labels/preview%20on%20nextstrain.org). ## git-lfs diff --git a/README.md b/README.md index 4f35c93a4..92990bf98 100644 --- a/README.md +++ b/README.md @@ -39,20 +39,11 @@ See [the relevant page on Auspice docs](https://docs.nextstrain.org/projects/aus To get up & running, you'll need datasets to visualise. (Please see the [nextstrain docs](https://nextstrain.org/docs/) for tutorials on how to run your own analyses.) -If you've installed auspice from `npm` you may get datasets to display via: +If you've installed auspice from `npm` you may get an example dataset to display via: ```bash mkdir data curl http://data.nextstrain.org/zika.json --compressed -o data/zika.json -curl http://data.nextstrain.org/ncov.json --compressed -o data/ncov.json -... -``` - -If you've installed auspice from source, we have a helper script to download a number of datasets for testing: - -```bash -# from the auspice src directory -npm run get-data ``` ### Obtain narratives to view locally diff --git a/app.json b/app.json deleted file mode 100644 index ac12f1bd8..000000000 --- a/app.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "auspice", - "scripts": {}, - "env": { - "HOST": { - "description": "For Auspice server; see https://docs.nextstrain.org/projects/auspice/en/stable/server/index.html#deploying-via-heroku", - "value": "0.0.0.0" - }, - "NPM_CONFIG_PRODUCTION": { - "description": "Runs with NODE_ENV=production; see https://devcenter.heroku.com/articles/nodejs-support#configuring-npm", - "value": "true" - } - }, - "formation": { - "web": { - "quantity": 1 - } - }, - "addons": [], - "buildpacks": [ - { - "url": "heroku/nodejs" - } - ], - "stack": "heroku-24" -} diff --git a/auspice.js b/auspice.js index 6b352c44b..292d4058d 100755 --- a/auspice.js +++ b/auspice.js @@ -1,11 +1,11 @@ #!/usr/bin/env node -const argparse = require('argparse'); -const version = require('./src/version').version; -const view = require("./cli/view"); -const build = require("./cli/build"); -const develop = require("./cli/develop"); -const convert = require("./cli/convert"); +import argparse from 'argparse'; +import { version } from './src/version.js'; +import * as view from "./cli/view.ts"; +import * as build from "./cli/build.ts"; +import * as develop from "./cli/develop.ts"; +import * as convert from "./cli/convert.js"; const parser = new argparse.ArgumentParser({ version: version, @@ -38,5 +38,3 @@ if (args.subcommand === "build") { } else if (args.subcommand === "convert") { convert.run(args); } - -// console.dir(args); diff --git a/babel.config.js b/babel.config.cjs similarity index 90% rename from babel.config.js rename to babel.config.cjs index 25c781165..6f1bd17b0 100644 --- a/babel.config.js +++ b/babel.config.cjs @@ -1,4 +1,4 @@ -const utils = require('./cli/utils'); +const utils = require('./cli/utils.ts'); /* What variables does this config depend on? * process.env.BABEL_EXTENSION_PATH -- a resolved path @@ -31,6 +31,9 @@ module.exports = function babelConfig(api) { if (api.env("development")) { plugins.push(["react-hot-loader/babel", { safetyNet: false }]); } + if (api.env('test')) { + plugins.push("babel-plugin-transform-import-meta"); + } if (process.env.BABEL_INCLUDE_TIMING_FUNCTIONS === "false") { plugins.push(["strip-function-call", {strip: ["timerStart", "timerEnd"]}]); } diff --git a/cli/build.js b/cli/build.ts similarity index 86% rename from cli/build.js rename to cli/build.ts index ad4cb34e0..c86e0267f 100644 --- a/cli/build.js +++ b/cli/build.ts @@ -1,14 +1,20 @@ -const webpack = require("webpack"); -const path = require("path"); -const generateWebpackConfig = require("../webpack.config.js").default; -const utils = require("./utils"); +import webpack from "webpack"; +import path from "path"; +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; +import * as utils from "./utils.ts"; + +const require = createRequire(import.meta.url); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const generateWebpackConfig = require("../webpack.config.cjs").default; const SUPPRESS = require('argparse').Const.SUPPRESS; -const addParser = (parser) => { +const addParser = (parser): void => { const description = `Build the client source code bundle. For development, you may want to use "auspice develop" which recompiles code on the fly as changes are made. You may provide customisations (e.g. code, options) to this step to modify the functionality and appearance of auspice. - To serve the bundle you will need a server such as "auspice view". + To serve the bundle you will need a server such as "auspice view". `; const subparser = parser.addParser('build', {addHelp: true, description}); @@ -24,7 +30,7 @@ const addParser = (parser) => { subparser.addArgument('--serverless', {action: "storeTrue", help: SUPPRESS}); }; -const run = (args) => { +const run = (args): void => { /* webpack set up */ const extensionPath = args.extend ? path.resolve(args.extend) : undefined; @@ -64,14 +70,14 @@ const run = (args) => { /* Where should the built files be saved? (or sourced??) * This may grow more complex over time */ -function getCustomOutputPath(extensions) { +function getCustomOutputPath(extensions): string | false { if (extensions && path.resolve(__dirname, "..") !== process.cwd()) { return process.cwd(); } return false; } -module.exports = { +export { addParser, run }; diff --git a/cli/convert.js b/cli/convert.js index fa0ef3938..b9335f00d 100644 --- a/cli/convert.js +++ b/cli/convert.js @@ -1,7 +1,7 @@ /* eslint no-console: off */ -const fs = require("fs"); -const convertFromV1 = require("./server/convertJsonSchemas").convertFromV1; -const utils = require("./utils"); +import fs from "fs"; +import { convertFromV1 } from "./server/convertJsonSchemas.js"; +import * as utils from "./utils.ts"; const addParser = (parser) => { @@ -35,7 +35,7 @@ const run = (args) => { fs.writeFileSync(args.output, JSON.stringify(v2, null, args.minify_json ? 0 : 2)); }; -module.exports = { +export { addParser, run }; diff --git a/cli/dev/reverse-proxy.js b/cli/dev/reverse-proxy.js index 3de2062a1..8c349b82c 100644 --- a/cli/dev/reverse-proxy.js +++ b/cli/dev/reverse-proxy.js @@ -1,7 +1,6 @@ - -const utils = require("../utils"); -const { URL } = require("url"); -const fetch = require("node-fetch"); +import * as utils from "../utils.ts"; +import { URL } from "url"; +import fetch from "node-fetch"; const PROXY = process.env.PROXY || `http://localhost:5000` @@ -59,8 +58,6 @@ async function proxy(req, res) { } } -module.exports = { - getAvailable: proxy, - getDataset: proxy, - getNarrative: proxy, -}; \ No newline at end of file +export const getAvailable = proxy; +export const getDataset = proxy; +export const getNarrative = proxy; diff --git a/cli/develop.js b/cli/develop.ts similarity index 69% rename from cli/develop.js rename to cli/develop.ts index e6d6831fc..71249f839 100644 --- a/cli/develop.js +++ b/cli/develop.ts @@ -1,17 +1,26 @@ /* eslint no-console: off */ -const path = require("path"); -const express = require("express"); -const webpack = require("webpack"); -const webpackDevMiddleware = require("webpack-dev-middleware"); -const webpackHotMiddleware = require("webpack-hot-middleware"); -const utils = require("./utils"); -const { loadAndAddHandlers, serveRelativeFilepaths } = require("./view"); -const version = require('../src/version').version; -const chalk = require('chalk'); -const generateWebpackConfig = require("../webpack.config.js").default; +import path from "path"; +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; +import express from "express"; +import webpack from "webpack"; +import webpackDevMiddleware from "webpack-dev-middleware"; +import webpackHotMiddleware from "webpack-hot-middleware"; +import * as utils from "./utils.ts"; +import * as view from "./view.ts"; +import { version } from '../src/version.js'; +import _chalk from 'chalk'; +/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */ +const chalk = _chalk as any as import('chalk').Chalk; +import { processPathArguments } from "./server/processPaths.ts"; + +const require = createRequire(import.meta.url); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const generateWebpackConfig = require("../webpack.config.cjs").default; const SUPPRESS = require('argparse').Const.SUPPRESS; -const addParser = (parser) => { +const addParser = (parser): void => { const description = `Launch auspice in development mode. This runs a local server and uses hot-reloading to allow automatic updating as you edit the code. NOTE: there is a speed penalty for this ability and this should never be used for production. @@ -21,8 +30,7 @@ const addParser = (parser) => { subparser.addArgument('--verbose', {action: "storeTrue", help: "Print more verbose progress messages in the terminal."}); subparser.addArgument('--extend', {action: "store", metavar: "JSON", help: "Client customisations to be applied. See documentation for more details. Note that hot reloading does not currently work for these customisations."}); subparser.addArgument('--handlers', {action: "store", metavar: "JS", help: "Overwrite the provided server handlers for client requests. See documentation for more details."}); - subparser.addArgument('--datasetDir', {metavar: "PATH", help: "Directory where datasets (JSONs) are sourced. This is ignored if you define custom handlers."}); - subparser.addArgument('--narrativeDir', {metavar: "PATH", help: "Directory where narratives (Markdown files) are sourced. This is ignored if you define custom handlers."}); + view.addDatasetNarrativePathArgs(subparser) subparser.addArgument('--includeTiming', {action: "storeTrue", help: "Do not strip timing functions. With these included the browser console will print timing measurements for a number of functions."}); /* there are some options which we deliberately do not document via `--help`. See build.js for explanations. */ @@ -30,7 +38,9 @@ const addParser = (parser) => { }; -const run = (args) => { +const run = async (args): Promise => { + const dataPaths = processPathArguments(args) + /* Basic server set up */ const app = express(); app.set('port', process.env.PORT || 4000); @@ -38,7 +48,7 @@ const run = (args) => { const baseDir = path.resolve(__dirname, ".."); utils.verbose(`Serving index / favicon etc from "${baseDir}"`); - app.get("/favicon.png", (req, res) => {res.sendFile(path.join(baseDir, "favicon.png"));}); + app.get("/favicon.png", (_req, res) => {res.sendFile(path.join(baseDir, "favicon.png"));}); /* webpack set up */ const extensionPath = args.extend ? path.resolve(args.extend) : undefined; @@ -51,7 +61,7 @@ const run = (args) => { process.env.BABEL_EXTENSION_PATH = extensionPath; /* Redirects / to webpack-generated index */ - app.use((req, res, next) => { + app.use((req, _res, next) => { if (!/^\/__webpack_hmr|^\/charon|\.[A-Za-z0-9]{1,5}$/.test(req.path)) { req.url = webpackConfig.output.publicPath; } @@ -69,12 +79,20 @@ const run = (args) => { let handlerMsg = ""; if (args.gh_pages) { - handlerMsg = serveRelativeFilepaths({app, dir: path.resolve(args.gh_pages)}); + handlerMsg = view.serveRelativeFilepaths({app, dir: path.resolve(args.gh_pages)}); + } else if (args.handlers) { + handlerMsg = await view.customRouteHandlers(app, path.resolve(args.handlers)); } else { - handlerMsg = loadAndAddHandlers({app, handlersArg: args.handlers, datasetDir: args.datasetDir, narrativeDir: args.narrativeDir}); + handlerMsg = view.defaultRouteHandlers({app, dataPaths}); } + /* Handle unhandled API (charon) routes */ + app.get("/charon*", (req, res) => { + const errorMessage = "Query unhandled -- " + req.originalUrl; + utils.warn(errorMessage); + return res.status(500).type("text/plain").send(errorMessage); + }); - app.get("*", (req, res) => res.redirect("/")); + app.get("*", (_req, res) => res.redirect("/")); const server = app.listen(app.get('port'), app.get('host'), () => { utils.log("\n\n---------------------------------------------------"); @@ -95,8 +113,8 @@ const run = (args) => { utils.error(`Host ${app.get('host')} is not a valid address. The server could not be started. If you did not provide a HOST environment variable when starting the app you may have HOST already set in your system. You can either change that variable, or override HOST when starting the app. Example commands to fix: - ${chalk.yellow('HOST="localhost" auspice develop')} - ${chalk.yellow('HOST="localhost" npm run develop')}`); + ${chalk.yellow('HOST="localhost" auspice develop ')} + ${chalk.yellow('HOST="localhost" npm run develop -- ')}`); } utils.error(`Uncaught error in app.listen(). Code: ${error.code}`); @@ -105,7 +123,7 @@ const run = (args) => { }; -module.exports = { +export { addParser, run }; diff --git a/cli/server/convertJsonSchemas.js b/cli/server/convertJsonSchemas.js index f95e16629..d36bb102f 100644 --- a/cli/server/convertJsonSchemas.js +++ b/cli/server/convertJsonSchemas.js @@ -1,4 +1,4 @@ -const utils = require("../utils"); +import * as utils from "../utils.ts"; /** In auspice v1, the `prettyString` function was used extensively to transform values * for "nicer" display. v2 JSONs intentially avoid this -- the strings are intended to @@ -397,7 +397,7 @@ const setNodeBranchAttrs = (v2) => { }; -const convertFromV1 = ({tree, meta}) => { +export const convertFromV1 = ({tree, meta}) => { const v2 = {version: "v2", meta: { extensions: { original_version: "v1" }}}; // set metadata setColorings(v2["meta"], meta); @@ -411,8 +411,3 @@ const convertFromV1 = ({tree, meta}) => { removeNonV2TreeProps(v2); return v2; }; - - -module.exports = { - convertFromV1 -}; diff --git a/cli/server/getAvailable.js b/cli/server/getAvailable.js deleted file mode 100644 index c7e66418b..000000000 --- a/cli/server/getAvailable.js +++ /dev/null @@ -1,101 +0,0 @@ -const utils = require("../utils"); -const fs = require('fs'); -const { promisify } = require('util'); -const { findAvailableSecondTreeOptions } = require('./getDatasetHelpers'); - -const readdir = promisify(fs.readdir); - -const getAvailableDatasets = async (localDataPath) => { - const datasets = []; - /* NOTE: if there are v1 & v2 files with the same name the v2 JSON is - * preferentially fetched. E.g. if `zika.json`, `zika_meta.json` and - * `zika_tree.json` exist then only `zika.json` is viewable in auspice. - */ - try { - const files = await readdir(localDataPath); - /* v2 files -- match JSONs not ending with `_tree.json`, `_meta.json`, - `_tip-frequencies.json`, `_seq.json` */ - const v2Files = files.filter((file) => ( - file.endsWith(".json") && - !file.includes("manifest") && - !file.endsWith("_tree.json") && - !file.endsWith("_meta.json") && - !file.endsWith("_tip-frequencies.json") && - !file.endsWith("_root-sequence.json") && - !file.endsWith("_seq.json") && - !file.endsWith("_measurements.json") - )) - .map((file) => file - .replace(".json", "") - .split("_") - .join("/") - ); - - v2Files.forEach((filepath) => { - datasets.push({ - request: filepath, - v2: true, - secondTreeOptions: findAvailableSecondTreeOptions(filepath, v2Files) - }); - }); - - /* v1 files -- match files ending with `_tree.json` */ - const v1Files = files - .filter((file) => file.endsWith("_tree.json")) - .map((file) => file - .replace("_tree.json", "") - .split("_") - .join("/") - ); - - v1Files.forEach((filepath) => { - datasets.push({ - request: filepath, - v2: false, - secondTreeOptions: findAvailableSecondTreeOptions(filepath, v1Files) - }); - }); - } catch (err) { - utils.warn(`Couldn't collect available dataset files (path searched: ${localDataPath})`); - utils.verbose(err); - } - return datasets; -}; - -const getAvailableNarratives = async (localNarrativesPath) => { - let narratives = []; - try { - narratives = await readdir(localNarrativesPath); - narratives = narratives - .filter((file) => file.endsWith(".md") && file!=="README.md") - .map((file) => file.replace(".md", "")) - .map((file) => file.split("_").join("/")) - .map((filepath) => `narratives/${filepath}`) - .map((filepath) => ({request: filepath})); - } catch (err) { - utils.warn(`Couldn't collect available narratives (path searched: ${localNarrativesPath})`); - utils.verbose(err); - } - return narratives; -}; - -const setUpGetAvailableHandler = ({datasetsPath, narrativesPath}) => { - /* return Auspice's default handler for "/charon/getAvailable" requests. - * Remember that auspice itself only serves local files. - * Servers often use their own handler instead of this. - */ - return async (req, res) => { - utils.log("GET AVAILABLE returning locally available datasets & narratives"); - const datasets = await getAvailableDatasets(datasetsPath); - const narratives = await getAvailableNarratives(narrativesPath); - res.json({datasets, narratives}); - }; - -}; - - -module.exports = { - setUpGetAvailableHandler, - getAvailableDatasets, - getAvailableNarratives -}; diff --git a/cli/server/getAvailable.ts b/cli/server/getAvailable.ts new file mode 100644 index 000000000..888a2d689 --- /dev/null +++ b/cli/server/getAvailable.ts @@ -0,0 +1,143 @@ +import * as utils from "../utils.ts"; +import fs from 'fs'; +import path from "path"; +import { promisify } from 'util'; +import { findAvailableSecondTreeOptions } from './getDatasetHelpers.ts'; + +const readdir = promisify(fs.readdir); + +export interface DatasetInfo { + request: string; + v2: boolean; + secondTreeOptions: string[]; + fileType: string; + dir: string; + buildUrl?: string; +} + +export interface NarrativeInfo { + fullPath: string; + request: string; + fileType: string; +} + +export const getAvailableDatasets = (dir, files): DatasetInfo[] => { + const datasets = []; + /* NOTE: if there are v1 & v2 files with the same name the v2 JSON is + * preferentially fetched. E.g. if `zika.json`, `zika_meta.json` and + * `zika_tree.json` exist then only `zika.json` is viewable in auspice. + */ + + /* v2 files -- match JSONs not ending with `_tree.json`, `_meta.json`, + `_tip-frequencies.json`, `_seq.json` */ + const v2Files = files.filter((file) => ( + file.endsWith(".json") && + !file.includes("manifest") && + !file.endsWith("_tree.json") && + !file.endsWith("_meta.json") && + !file.endsWith("_tip-frequencies.json") && + !file.endsWith("_root-sequence.json") && + !file.endsWith("_seq.json") && + !file.endsWith("_measurements.json") + )) + .map((file) => file + .replace(".json", "") + .split("_") + .join("/") + ); + + v2Files.forEach((filepath) => { + datasets.push({ + request: filepath, + v2: true, + secondTreeOptions: findAvailableSecondTreeOptions(filepath, v2Files), + fileType: 'dataset', + dir, + }); + }); + + /* v1 files -- match files ending with `_tree.json` */ + const v1Files = files + .filter((file) => file.endsWith("_tree.json")) + .map((file) => file + .replace("_tree.json", "") + .split("_") + .join("/") + ); + + v1Files.forEach((filepath) => { + datasets.push({ + request: filepath, + v2: false, + secondTreeOptions: findAvailableSecondTreeOptions(filepath, v1Files), + fileType: 'dataset', + dir, + }); + }); + return datasets; +}; + +export const getAvailableNarratives = (dir, files): NarrativeInfo[] => { + return files + .filter((file) => file.endsWith(".md") && file!=="README.md") + .map((file) => ({ + fullPath: path.join(dir, file), + request: 'narratives/' + file.replace(".md", "").split("_").join("/"), + fileType: 'narrative', + })); +}; + +export const setUpGetAvailableHandler = (dataPaths) => { + /* return Auspice's default handler for "/charon/getAvailable" requests. + * Remember that auspice itself only serves local files. + * Servers often use their own handler instead of this. + */ + return async (_req, res): Promise => { + utils.log("GET AVAILABLE returning locally available datasets & narratives"); + let resources: (DatasetInfo | NarrativeInfo)[] = [] + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */ + for (const [p, dataTypes] of Object.entries(dataPaths) as [string, Set][]) { + try { + // FUTURE TODO - recurse into subfolders (readdir can do this natively) + const files = await readdir(p); + if (dataTypes.has('datasets')) { + resources.push(...getAvailableDatasets(p, files)) + } + if (dataTypes.has('narratives')) { + resources.push(...getAvailableNarratives(p, files)) + } + } catch (err) { + utils.warn(`Couldn't collect files (path searched: ${p})`); + utils.verbose(err); + } + } + resources = unique(resources) + + /* convert to the expected API response structure */ + res.json(availableResponseStructure(resources)); + }; + +}; + +function availableResponseStructure(resources: (DatasetInfo | NarrativeInfo)[]): {datasets: {request: string, buildUrl?: string, secondTreeOptions?: string[]}[], narratives: {request: string}[]} { + const datasets = resources.filter((r): r is DatasetInfo => r.fileType==='dataset') + .map((r) => ({request: r.request, buildUrl: r.buildUrl, secondTreeOptions: r.secondTreeOptions})); + const narratives = resources.filter((r): r is NarrativeInfo => r.fileType==='narrative') + .map((r) => ({request: r.request})); + return {datasets, narratives}; +} + +/** + * First match wins + */ +function unique(resources: (DatasetInfo | NarrativeInfo)[]): (DatasetInfo | NarrativeInfo)[] { + const seen = {dataset: new Set(), narrative: new Set()}; + return resources.filter((r) => { + if (seen[r.fileType].has(r.request)) { + // utils.verbose(`Dropping duplicate ${r.request} from available`); // Too verbose! + return false; + } + seen[r.fileType].add(r.request); + return true; + }) +} diff --git a/cli/server/getDataset.js b/cli/server/getDataset.js deleted file mode 100644 index a7965bee3..000000000 --- a/cli/server/getDataset.js +++ /dev/null @@ -1,25 +0,0 @@ -const getAvailable = require("./getAvailable"); -const helpers = require("./getDatasetHelpers"); - -const setUpGetDatasetHandler = ({datasetsPath}) => { - return async (req, res) => { - try { - const availableDatasets = await getAvailable.getAvailableDatasets(datasetsPath); - const info = helpers.interpretRequest(req, datasetsPath); - const redirected = helpers.redirectIfDatapathMatchFound(res, info, availableDatasets); - if (redirected) return; - helpers.makeFetchAddresses(info, datasetsPath, availableDatasets); - await helpers.sendJson(res, info); - } catch (err) { - console.trace(err); - // Throw 404 when not available - const errorCode = err.message.endsWith("not in available datasets") ? 404 : 500; - return helpers.handleError(res, `couldn't fetch JSONs`, err.message, errorCode); - } - }; -}; - - -module.exports = { - setUpGetDatasetHandler -}; diff --git a/cli/server/getDataset.ts b/cli/server/getDataset.ts new file mode 100644 index 000000000..e9a2b5331 --- /dev/null +++ b/cli/server/getDataset.ts @@ -0,0 +1,104 @@ +import { promisify } from 'util'; +import path from "path"; +import fs from 'fs'; +import * as getAvailable from "./getAvailable.ts"; +import type { DatasetInfo } from "./getAvailable.ts"; +import * as helpers from "./getDatasetHelpers.ts"; +import * as utils from "../utils.ts"; + +const readdir = promisify(fs.readdir); + +/** + * Returns a route handler which responds to requests by serving the relevant JSON file + * from disk. + */ +export const setUpGetDatasetHandler = (dataPaths) => { + return async (req, res): Promise => { + + let requestInfo; + try { + requestInfo = helpers.interpretRequest(req); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return helpers.handleError(res, msg, msg, 400); + } + + const matchResult = await matchDatasetFile(dataPaths, requestInfo); + + if (!Array.isArray(matchResult)) { + return await helpers.sendJson(res, { ...requestInfo, address: matchResult }); + } + + /* No perfect match - attempt to find a close one iff type=dataset */ + if (requestInfo.dataType==='dataset') { + const closeMatchRequest = helpers.closestMatch(requestInfo.parts, matchResult); + if (closeMatchRequest) { + utils.verbose(`Redirecting request to ${closeMatchRequest}`); + return res.redirect(`./getDataset?prefix=/${closeMatchRequest}`); + } + } + + /* fallthrough to 404 error */ + const msg = `no matching file for ${requestInfo.dataType || 'unknown data type'}`; + return helpers.handleError(res, msg, msg, 404); + }; +}; + + +/** Charon sidecar type query -> filename suffix */ +const SIDECARS = { + 'root-sequence': '_root-sequence.json', + 'tip-frequencies': '_tip-frequencies.json', + measurements: '_measurements.json', +} + +/** Walk through folders (dataPaths) looking for a file on disk which + * exactly matches (in terms of dataset name & type). We return the + * first exact match ("first match wins"), which mirrors `getAvailable` + * + * Return either: + * - string: the path to a v2 main dataset / sidecar JSON + * - object: the paths for (v1) meta/tree dataset JSONs + * - array: no matching file found. The array represents all available datasets + */ +async function matchDatasetFile(dataPaths, requestInfo): Promise { + /** + * Iterate through the dataPaths and return the first match we find + * (this "first match wins" approach mirrors that of `getAvailable`) + */ + const allAvailableDatasets = [] + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */ + for (const [dir, dataTypes] of Object.entries(dataPaths) as [string, Set][]) { + if (!dataTypes.has('datasets')) continue; + let files: string[] = []; + try { + files = await readdir(dir); + } catch (err) { + utils.warn(`Error reading datasets from ${dir}: ${err instanceof Error ? err.message : err}`) + } + + if (requestInfo.dataType==='dataset') { + // list all available datasets -- includes v1 (meta+tree) & v2 ("main") dataset + const availableDatasets = getAvailable.getAvailableDatasets(dir, files); + allAvailableDatasets.push(...availableDatasets); + for (const resource of availableDatasets) { + if (resource.request === requestInfo.parts.join("/")) { + const address = resource.v2 ? + path.join(resource.dir, `${requestInfo.parts.join("_")}.json`) : + ({ + meta: path.join(resource.dir, `${requestInfo.parts.join("_")}_meta.json`), + tree: path.join(resource.dir, `${requestInfo.parts.join("_")}_tree.json`) + }); + return address; + } + } + } else if (Object.hasOwn(SIDECARS, requestInfo.dataType)) { + const expectedFileName = requestInfo.parts.join("_") + SIDECARS[requestInfo.dataType]; + const matches = files.filter((f) => f === expectedFileName); + if (matches.length) { + return path.join(dir, matches[0]); + } + } + } + return allAvailableDatasets; +} diff --git a/cli/server/getDatasetHelpers.js b/cli/server/getDatasetHelpers.ts similarity index 61% rename from cli/server/getDatasetHelpers.js rename to cli/server/getDatasetHelpers.ts index 82a5b537d..74fcd3da3 100644 --- a/cli/server/getDatasetHelpers.js +++ b/cli/server/getDatasetHelpers.ts @@ -8,34 +8,35 @@ * */ +import * as utils from "../utils.ts"; +import queryString from "query-string"; +import { convertFromV1 } from "./convertJsonSchemas.js"; +import fs from "fs"; -const utils = require("../utils"); -const queryString = require("query-string"); -const path = require("path"); -const convertFromV1 = require("./convertJsonSchemas").convertFromV1; -const fs = require("fs"); - -const handleError = (res, clientMsg, serverMsg="", code=500) => { +export const handleError = (res, clientMsg, serverMsg="", code=500): void => { utils.warn(`${clientMsg} -- ${serverMsg}`); return res.status(code).type("text/plain").send(clientMsg); }; -const splitPrefixIntoParts = (url) => url +const splitPrefixIntoParts = (url): string[] => url .replace(/^\//, '') .replace(/\/$/, '') .split("/"); /** - * Basic interpretation of an auspice request - * @returns {Object} keys: `dataType`, `parts` + * Parses the dataset URL query into an object with two keys 'parts' and 'dataType' + * + * @returns {Object} ret + * @returns {string[]} ret.parts is the dataset name spit on '/' character + * @returns {string} ret.dataType is the dataset type ('dataset', 'root-sequence' etc) */ -const interpretRequest = (req) => { - utils.log(`GET DATASET query received: ${req.url.split('?')[1]}`); +export const interpretRequest = (req): {parts: string[], dataType?: string} => { const query = queryString.parse(req.url.split('?')[1]); + utils.log(`[GET DATASET] ${Object.entries(query).map(([k,v]) => `${k}: '${v}'`).join(', ')}`); if (!query.prefix) throw new Error("'prefix' not defined in request"); const datanameParts = splitPrefixIntoParts(query.prefix); - const info = {parts: datanameParts}; + const info: {parts: string[], dataType?: string} = {parts: datanameParts}; /* query.type is used to indicate which sidecar file should be fetched, without it we fetch the "main" dataset JSON. See the `Dataset` constructor in `./src/actions/loadData.js` for which sidecars auspice may try to fetch */ @@ -46,66 +47,36 @@ const interpretRequest = (req) => { } else if (sidecars.includes(query.type)) { info.dataType = query.type; } else { - throw new Error(`Unknown file type '${query.type}' requested.`); + throw new Error(`Unknown dataset type '${query.type}' requested.`); } return info; }; /** - * Given a request, if the dataset does not exist then either - * (a) redirect to an appropriate dataset if possible & return `true` - * (b) throw. - * If the dataset existed then return `false` as no redirect necessary. + * Given a request for which there is no perfect match, return a close match + * if one exists else return false */ -const redirectIfDatapathMatchFound = (res, info, availableDatasets) => { +export const closestMatch = (requestedDatasetParts, availableDatasets): string | false => { let matchingDatasets = availableDatasets; let i; - const matchDatasetRequest = (d) => d.request.split("/")[i] === info.parts[i]; + const matchDatasetRequest = (d): boolean => d.request.split("/")[i] === requestedDatasetParts[i]; // Filter gradually by path fragment, starting from the root - for (i = 0; i < info.parts.length; i++) { + for (i = 0; i < requestedDatasetParts.length; i++) { const newMatchingDatasets = matchingDatasets.filter(matchDatasetRequest); if (!newMatchingDatasets.length) break; matchingDatasets = newMatchingDatasets; } - // If root fragment does cannot be matched, throw - if (!i) throw new Error(`${info.parts.join("/")} not in available datasets`); + // If root fragment cannot be matched return false (no closest match found) + if (!i) return false; // If best match is not equal to path requested, redirect - if (matchingDatasets[0].request !== info.parts.join("/")) { - res.redirect(`./getDataset?prefix=/${matchingDatasets[0].request}`); - return true; + if (matchingDatasets[0].request !== requestedDatasetParts.join("/")) { + return matchingDatasets[0].request; } return false; }; -/** - * sets info.address. - * if we need v1 datasets then `info.address` will be an object with `meta` - * and `tree` keys. Otherwise `info.address` will be a string of the fetch - * path. - * @sideEffect sets `info.address` {Object | string} - * @throws - */ -const makeFetchAddresses = (info, datasetsPath, availableDatasets) => { - if (info.dataType !== "dataset") { - info.address = path.join( - datasetsPath, - `${info.parts.join("_")}_${info.dataType}.json` - ); - } else { - const requestStr = info.parts.join("/"); // TO DO - const availableInfo = availableDatasets.filter((d) => d.request === requestStr)[0]; - if (availableInfo.v2) { - info.address = path.join(datasetsPath, `${info.parts.join("_")}.json`); - } else { - info.address = { - meta: path.join(datasetsPath, `${info.parts.join("_")}_meta.json`), - tree: path.join(datasetsPath, `${info.parts.join("_")}_tree.json`) - }; - } - } -}; -const sendJson = async (res, info) => { +export const sendJson = async (res, info): Promise => { if (typeof info.address === "string") { /* In general, JSONs are designed such that no server modifications are needed by the server. This allows us to read as a stream and @@ -140,7 +111,7 @@ const sendJson = async (res, info) => { * (c) differ from the `currentDatasetUrl` by only 1 part * Note: the "parts" of the URL are formed by splitting it on `"/"` */ -const findAvailableSecondTreeOptions = (currentDatasetUrl, availableDatasetUrls) => { +export const findAvailableSecondTreeOptions = (currentDatasetUrl, availableDatasetUrls): string[] => { const currentDatasetUrlArr = currentDatasetUrl.split('/'); const availableTangleTreeOptions = availableDatasetUrls.filter((datasetUrl) => { @@ -170,12 +141,3 @@ const findAvailableSecondTreeOptions = (currentDatasetUrl, availableDatasetUrls) return availableTangleTreeOptions; }; - -module.exports = { - interpretRequest, - redirectIfDatapathMatchFound, - makeFetchAddresses, - handleError, - sendJson, - findAvailableSecondTreeOptions -}; diff --git a/cli/server/getNarrative.js b/cli/server/getNarrative.js deleted file mode 100644 index e3112bdc6..000000000 --- a/cli/server/getNarrative.js +++ /dev/null @@ -1,40 +0,0 @@ -const queryString = require("query-string"); -const path = require("path"); -const fs = require("fs"); -const utils = require("../utils"); - -const setUpGetNarrativeHandler = ({narrativesPath}) => { - return async (req, res) => { - const query = queryString.parse(req.url.split('?')[1]); - const prefix = query.prefix || ""; - const filename = prefix - .replace(/.+narratives\//, "") // remove the URL up to (& including) "narratives/" - .replace(/\/$/, "") // remove ending slash - .replace(/\//g, "_") // change slashes to underscores - .concat(".md"); // add .md suffix - - const type = query.type ? query.type.toLowerCase() : null; - - const pathName = path.join(narrativesPath, filename); - utils.log("trying to access & parse local narrative file: " + pathName); - try { - const fileContents = fs.readFileSync(pathName, 'utf8'); - if (type === "md" || type === "markdown") { - // we could stream the response (as we sometimes do for getDataset) but narrative files are small - // so the expected time savings / server overhead is small. - res.send(fileContents); - } else { - throw new Error(`Unknown format requested: ${type}`); - } - utils.verbose("SUCCESS"); - } catch (err) { - const errorMessage = `Narratives couldn't be served -- ${err.message}`; - utils.warn(errorMessage); - res.status(500).type("text/plain").send(errorMessage); - } - }; -}; - -module.exports = { - setUpGetNarrativeHandler, -}; diff --git a/cli/server/getNarrative.ts b/cli/server/getNarrative.ts new file mode 100644 index 000000000..6fe7fd39c --- /dev/null +++ b/cli/server/getNarrative.ts @@ -0,0 +1,49 @@ +import queryString from "query-string"; +import { promisify } from 'util'; +import fs from "fs"; +import * as utils from "../utils.ts"; +import * as getAvailable from "./getAvailable.ts"; + +const readdir = promisify(fs.readdir); + + +export const setUpGetNarrativeHandler = (dataPaths) => { + return async (req, res): Promise => { + utils.log(`GET NARRATIVE request received: ${req.url}`); + const query = queryString.parse(req.url.split('?')[1]); + const type = query.type ? query.type.toLowerCase() : null; + if (!(type === "md" || type === "markdown")) { + return res.status(400).type("text/plain").send(`Unknown format requested: ${type}`); + } + if (!query.prefix || !(String(query.prefix).startsWith('narratives/') || String(query.prefix).startsWith('/narratives/'))) { + return res.status(400).type("text/plain").send(`Query must include a prefix starting with [/]narratives/`); + } + + const requestStr = query.prefix // we know this starts with [/]narratives/ + .replace(/^\//, "") // remove leading slash + .replace(/\/$/, "") // remove ending slash + + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */ + for (const [p, dataTypes] of Object.entries(dataPaths) as [string, Set][]) { + if (!dataTypes.has('narratives')) continue; + try { + const files = await readdir(p); + const match = getAvailable.getAvailableNarratives(p, files) + .filter((d) => d.request===requestStr)[0] + if (match) { + // we could stream the response (as we sometimes do for getDataset) but narrative files are small + // so the expected time savings / server overhead is small. + return res.send(fs.readFileSync(match.fullPath, 'utf8')); + } + // else go scan the next dataPaths (directory) + } catch (err) { + const errorMessage = `Narratives couldn't be served -- ${err instanceof Error ? err.message : err}`; + utils.warn(errorMessage); + return res.status(500).type("text/plain").send(errorMessage); + } + } + const errorMessage = `No matching narrative found`; + utils.warn(errorMessage); + res.status(404).type("text/plain").send(errorMessage); + } +}; diff --git a/cli/server/processPaths.ts b/cli/server/processPaths.ts new file mode 100644 index 000000000..298c5c8a6 --- /dev/null +++ b/cli/server/processPaths.ts @@ -0,0 +1,78 @@ +import * as utils from "../utils.ts"; + +type AvailableResourceTypes = Set<'datasets' | 'narratives'>; + +/** + * Returns an object linking (absolute) paths to a set whose members indicate whether + * the path should be searched for dataset JSONs, narrative markdown files, or both. + * + * Historically the CLI took a single dataset path and a single narrative path, and + * that behaviour is still allowed here in addition to the newer approach of supplying + * any number of paths which can contain datasets and/or narratives. The two approaches + * are mutually exclusive and enforcement of this happens here. + */ +export function processPathArguments(args): Record { + + if (args.handlers) { + if (args.datasetDir || args.narrativeDir || args.path.length>0) { + utils.error("Can't provide data paths if you use custom handlers") + } + return {} + } + + if (args.gh_pages) { + if (args.handlers || args.datasetDir || args.narrativeDir || args.path.length > 0) { + utils.error("Can't provide data paths or handlers if you use --gh-pages") + } + return {} + } + + if (args.path.length === 0) { + if (!args.datasetDir && !args.narrativeDir) { + utils.error("Please provide paths to search for datasets and narratives: 'auspice ... PATH [PATH ...]'"); + } else if (!args.datasetDir) { // i.e. we do have --narrativeDir + utils.error("Please provide a '--datasetDir ' with datasets, or alternatively use the new positional-args invocation: 'auspice ... PATH [PATH ...]'"); + } + // loud deprecation warnings! + if (args.datasetDir) { + utils.warn(`[DEPRECATED] Instead of 'auspice ... --datasetDir ${args.datasetDir}' please use 'auspice ... ${args.datasetDir}' `) + } + if (args.narrativeDir) { + utils.warn(`[DEPRECATED] Instead of 'auspice ... --narrativeDir ${args.narrativeDir}' please use 'auspice ... ${args.narrativeDir}' `) + } + } else { // else: we do have positional path arguments + if (args.datasetDir || args.narrativeDir) { + utils.error("Incompatible arguments defining paths for datasets and/or narratives: " + + "You must either specify the paths as named arguments ('--datasetDir' and optionally '--narrativeDir') " + + "or specify one or more positional arguments."); + } + } + + + const dataPaths: Record = Object.fromEntries( + (args.path.length ? + args.path.map(utils.cleanUpPathname) : + [utils.cleanUpPathname(args.datasetDir)] + ).map((p: string) => [p, new Set(['datasets'])]) + ); + + const narrativePaths = args.path.length ? + args.path.map(utils.cleanUpPathname) : + args.narrativeDir ? + [utils.cleanUpPathname(args.narrativeDir)] : + []; // if no '--narrativeDir' then use auspice without narratives + + for (const narrativePath of narrativePaths) { + if (narrativePath in dataPaths) { + dataPaths[narrativePath].add('narratives'); + } else { + dataPaths[narrativePath] = new Set(['narratives']); + } + } + + if ("undefined" in dataPaths) { + utils.error("One or more provided paths does not exist") + } + + return dataPaths; +} diff --git a/cli/tsconfig.json b/cli/tsconfig.json new file mode 100644 index 000000000..08e517a38 --- /dev/null +++ b/cli/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "allowImportingTsExtensions": true, + "types": ["node"], + "lib": ["ES2022"] + }, + "include": ["./**/*"], + "exclude": [] +} diff --git a/cli/utils.js b/cli/utils.ts similarity index 55% rename from cli/utils.js rename to cli/utils.ts index 4a82d0aa5..f686242a0 100644 --- a/cli/utils.js +++ b/cli/utils.ts @@ -1,29 +1,31 @@ /* eslint no-console: off */ -const fs = require('fs'); -const chalk = require('chalk'); -const path = require("path"); +import fs from 'fs'; +import _chalk from 'chalk'; +// chalk 2.x types aren't compatible with nodenext module resolution +/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */ +const chalk = _chalk as any as import('chalk').Chalk; +import path from 'path'; +import { fileURLToPath } from 'url'; -const verbose = (msg) => { +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export const verbose = (msg): void => { if (global.AUSPICE_VERBOSE) { console.log(chalk.greenBright(`[verbose]\t${msg}`)); } }; -const log = (msg) => { +export const log = (msg): void => { console.log(chalk.blueBright(msg)); }; -const warn = (msg) => { +export const warn = (msg): void => { console.warn(chalk.yellowBright(`[warning]\t${msg}`)); }; -const error = (msg) => { +export const error = (msg): never => { console.error(chalk.redBright(`[error]\t${msg}`)); process.exit(2); }; -const isNpmGlobalInstall = () => { - return __dirname.indexOf("lib/node_modules/auspice") !== -1; -}; - -const cleanUpPathname = (pathIn) => { +export const cleanUpPathname = (pathIn): string | undefined => { let pathOut = pathIn; if (!pathOut.endsWith("/")) pathOut += "/"; if (pathOut.startsWith("~")) { @@ -31,34 +33,13 @@ const cleanUpPathname = (pathIn) => { } pathOut = path.resolve(pathOut); if (!fs.existsSync(pathOut)) { + warn(`path ${pathOut} doesn't exist`) return undefined; } return pathOut; }; -const getCurrentDirectoriesFor = (type) => { - const cwd = process.cwd(); - const folderName = type === "data" ? "auspice" : "narratives"; - if (fs.existsSync(path.join(cwd, folderName))) { - return cleanUpPathname(path.join(cwd, folderName)); - } - return cleanUpPathname(cwd); -}; - -const resolveLocalDirectory = (providedPath, isNarratives) => { - let localPath; - if (providedPath) { - localPath = cleanUpPathname(providedPath); - } else if (isNpmGlobalInstall()) { - localPath = getCurrentDirectoriesFor(isNarratives ? "narratives" : "data"); - } else { - // fallback to the auspice source directory - localPath = path.join(path.resolve(__dirname), "..", isNarratives ? "narratives" : "data"); - } - return localPath; -}; - -const readFilePromise = (fileName) => { +export const readFilePromise = (fileName): Promise => { return new Promise((resolve, reject) => { fs.readFile(fileName, 'utf8', (err, data) => { if (err) { @@ -76,7 +57,7 @@ const readFilePromise = (fileName) => { /* write an index.html file to the current working directory * Optionally set the hrefs for local files to relative links (needed for github pages) */ -const exportIndexDotHtml = ({relative=false}) => { +export const exportIndexDotHtml = ({relative=false}): void => { if (path.resolve(__dirname, "..") === process.cwd()) { warn("Cannot export index.html to the auspice source directory."); return; @@ -91,13 +72,3 @@ const exportIndexDotHtml = ({relative=false}) => { } fs.writeFileSync(outputFilePath, data); }; - -module.exports = { - verbose, - log, - warn, - error, - resolveLocalDirectory, - readFilePromise, - exportIndexDotHtml -}; diff --git a/cli/view.js b/cli/view.ts similarity index 61% rename from cli/view.js rename to cli/view.ts index 3fa582572..062d22d0b 100644 --- a/cli/view.js +++ b/cli/view.ts @@ -1,18 +1,27 @@ /* eslint no-console: off */ -/* eslint global-require: off */ - -const path = require("path"); -const fs = require("fs"); -const express = require("express"); -const expressStaticGzip = require("express-static-gzip"); -const compression = require('compression'); -const nakedRedirect = require('express-naked-redirect'); -const utils = require("./utils"); -const version = require('../src/version').version; -const chalk = require('chalk'); +/* eslint-disable @typescript-eslint/explicit-function-return-type, @typescript-eslint/consistent-type-assertions */ + +import path from "path"; +import fs from "fs"; +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; +import express from "express"; +import expressStaticGzip from "express-static-gzip"; +import compression from 'compression'; +import nakedRedirect from 'express-naked-redirect'; +import * as utils from "./utils.ts"; +import { version } from '../src/version.js'; +import _chalk from 'chalk'; +const chalk = _chalk as any as import('chalk').Chalk; +import { processPathArguments } from "./server/processPaths.ts"; +import { setUpGetAvailableHandler } from "./server/getAvailable.ts"; +import { setUpGetDatasetHandler } from "./server/getDataset.ts"; +import { setUpGetNarrativeHandler } from "./server/getNarrative.ts"; + +const require = createRequire(import.meta.url); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const SUPPRESS = require('argparse').Const.SUPPRESS; - const addParser = (parser) => { const description = `Launch a local server to view locally available datasets & narratives. The handlers for (auspice) client requests can be overridden here (see documentation for more details). @@ -22,13 +31,24 @@ const addParser = (parser) => { const subparser = parser.addParser('view', {addHelp: true, description}); subparser.addArgument('--verbose', {action: "storeTrue", help: "Print more verbose logging messages."}); subparser.addArgument('--handlers', {action: "store", metavar: "JS", help: "Overwrite the provided server handlers for client requests. See documentation for more details."}); - subparser.addArgument('--datasetDir', {metavar: "PATH", help: "Directory where datasets (JSONs) are sourced. This is ignored if you define custom handlers."}); - subparser.addArgument('--narrativeDir', {metavar: "PATH", help: "Directory where narratives (Markdown files) are sourced. This is ignored if you define custom handlers."}); + addDatasetNarrativePathArgs(subparser) subparser.addArgument('--customBuildOnly', {action: "storeTrue", help: "Error if a custom build is not found."}); /* there are some options which we deliberately do not document via `--help`. */ subparser.addArgument('--gh-pages', {action: "store", help: SUPPRESS}); /* related to the "static-site-generation" or "github-pages" */ }; +function addDatasetNarrativePathArgs(parser) { + parser.addArgument('--datasetDir', { + metavar: "PATH", + help: "[DEPRECATED] Directory where datasets (JSONs) are sourced. This cannot be used with positional arg(s) or custom handlers." + }); + parser.addArgument('--narrativeDir', { + metavar: "PATH", + help: "[DEPRECATED] Directory where narratives (Markdown files) are sourced. This cannot be used with positional arg(s) or custom handlers." + }); + parser.addArgument('path', {action: "store", nargs: '*', help: "Directories where datasets and/or narratives are stored"}); +} + const serveRelativeFilepaths = ({app, dir}) => { app.get("*.json", (req, res) => { const filePath = path.join(dir, req.originalUrl); @@ -38,42 +58,29 @@ const serveRelativeFilepaths = ({app, dir}) => { return `JSON requests will be served relative to ${dir}.`; }; -const loadAndAddHandlers = ({app, handlersArg, datasetDir, narrativeDir}) => { - /* load server handlers, either from provided path or the defaults */ - const handlers = {}; - let datasetsPath, narrativesPath; - if (handlersArg) { - const handlersPath = path.resolve(handlersArg); - utils.verbose(`Loading handlers from ${handlersPath}`); - const inject = require(handlersPath); - handlers.getAvailable = inject.getAvailable; - handlers.getDataset = inject.getDataset; - handlers.getNarrative = inject.getNarrative; - } else { - datasetsPath = utils.resolveLocalDirectory(datasetDir, false); - narrativesPath = utils.resolveLocalDirectory(narrativeDir, true); - handlers.getAvailable = require("./server/getAvailable") - .setUpGetAvailableHandler({datasetsPath, narrativesPath}); - handlers.getDataset = require("./server/getDataset") - .setUpGetDatasetHandler({datasetsPath}); - handlers.getNarrative = require("./server/getNarrative") - .setUpGetNarrativeHandler({narrativesPath}); - } - - /* apply handlers */ - app.get("/charon/getAvailable", handlers.getAvailable); - app.get("/charon/getDataset", handlers.getDataset); - app.get("/charon/getNarrative", handlers.getNarrative); - app.get("/charon*", (req, res) => { - const errorMessage = "Query unhandled -- " + req.originalUrl; - utils.warn(errorMessage); - return res.status(500).type("text/plain").send(errorMessage); - }); +async function customRouteHandlers(app, handlersPath) { + utils.verbose(`Loading handlers from ${handlersPath}`); + const customCode = await import(handlersPath); + app.get("/charon/getAvailable", customCode.getAvailable); + app.get("/charon/getDataset", customCode.getDataset); + app.get("/charon/getNarrative", customCode.getNarrative); + return `Custom server handlers provided.`; +} - return handlersArg ? - `Custom server handlers provided.` : - `Looking for datasets in ${datasetsPath}\nLooking for narratives in ${narrativesPath}`; -}; +/** + * Adds route handlers for the three canonical charon routes to the *app* + */ +function defaultRouteHandlers({app, dataPaths}) { + app.get("/charon/getAvailable", setUpGetAvailableHandler(dataPaths)); + app.get("/charon/getDataset", setUpGetDatasetHandler(dataPaths)); + app.get("/charon/getNarrative", setUpGetNarrativeHandler(dataPaths)); + const sep = "\n - " + return 'Looking for datasets & narratives in the following paths,\n' + + '(if there are multiple matches then the first one will be used)' + sep + + (Object.entries(dataPaths) as [string, Set][]) + .map(([p, dataTypes]) => `${p} (${Array.from(dataTypes).join(', ')})`) + .join(sep); +} const getAuspiceBuild = (customBuildOnly) => { const cwd = path.resolve(process.cwd()); @@ -108,7 +115,9 @@ function hasAuspiceBuild(directory) { ) } -const run = (args) => { +const run = async (args) => { + const dataPaths = processPathArguments(args) + /* Basic server set up */ const app = express(); app.set('port', process.env.PORT || 4000); @@ -119,7 +128,7 @@ const run = (args) => { const auspiceBuild = getAuspiceBuild(args.customBuildOnly); utils.verbose(`Serving favicon from "${auspiceBuild.baseDir}"`); utils.verbose(`Serving index and built javascript from "${auspiceBuild.distDir}"`); - app.get("/favicon.png", (req, res) => {res.sendFile(path.join(auspiceBuild.baseDir, "favicon.png"));}); + app.get("/favicon.png", (_req, res) => {res.sendFile(path.join(auspiceBuild.baseDir, "favicon.png"));}); app.use("/dist", expressStaticGzip(auspiceBuild.distDir, { maxAge: '30d', enableBrotli: true, @@ -128,12 +137,20 @@ const run = (args) => { let handlerMsg = ""; if (args.gh_pages) { handlerMsg = serveRelativeFilepaths({app, dir: path.resolve(args.gh_pages)}); + } else if (args.handlers) { + handlerMsg = await customRouteHandlers(app, path.resolve(args.handlers)); } else { - handlerMsg = loadAndAddHandlers({app, handlersArg: args.handlers, datasetDir: args.datasetDir, narrativeDir: args.narrativeDir}); + handlerMsg = defaultRouteHandlers({app, dataPaths}); } + /* Handle unhandled API (charon) routes */ + app.get("/charon*", (req, res) => { + const errorMessage = "Query unhandled -- " + req.originalUrl; + utils.warn(errorMessage); + return res.status(500).type("text/plain").send(errorMessage); + }); /* this must be the last "get" handler, else the "*" swallows all other requests */ - app.get("*", (req, res) => { + app.get("*", (_req, res) => { res.sendFile(path.join(auspiceBuild.baseDir, "dist/index.html"), {headers: {"Cache-Control": "no-cache, no-store, must-revalidate"}}); }); @@ -155,8 +172,8 @@ const run = (args) => { utils.error(`Host ${app.get('host')} is not a valid address. The server could not be started. If you did not provide a HOST environment variable when starting the app you may have HOST already set in your system. You can either change that variable, or override HOST when starting the app. Example commands to fix: - ${chalk.yellow('HOST="localhost" auspice view')} - ${chalk.yellow('HOST="localhost" npm run view')}`); + ${chalk.yellow('HOST="localhost" auspice view ')} + ${chalk.yellow('HOST="localhost" npm run view -- ')}`); } utils.error(`Uncaught error in app.listen(). Code: ${error.code}`); @@ -164,9 +181,12 @@ const run = (args) => { }; -module.exports = { +export { addParser, run, - loadAndAddHandlers, + addDatasetNarrativePathArgs, + processPathArguments, + defaultRouteHandlers, + customRouteHandlers, serveRelativeFilepaths }; diff --git a/docs/introduction/how-to-run.rst b/docs/introduction/how-to-run.rst index 808acd005..c96cd189e 100644 --- a/docs/introduction/how-to-run.rst +++ b/docs/introduction/how-to-run.rst @@ -8,6 +8,10 @@ Auspice is run as a command line program -- ``auspice`` -- with various subcomma - ``auspice develop --help`` - ``auspice convert --help`` +.. note:: + + If you just want to visualise your JSON datasets (or a newick tree) in Auspice, you can use the stand-alone web-app `auspice.us `__ which allows drag-and-drop of files onto the browser. + How to Get an Example Dataset Up and Running -------------------------------------------- @@ -22,7 +26,7 @@ And then run ``auspice`` via: .. code:: bash - auspice view --datasetDir datasets + auspice view datasets This will allow you to run Auspice locally (i.e. from your computer) and view the dataset which is behind `nextstrain.org/zika `__. :ref:`See below ` for how to download all of the data available on `nextstrain.org `__. @@ -31,15 +35,9 @@ To analyse your own data, please see the tutorials on the `nextstrain docs `. @@ -95,9 +93,6 @@ Datasets JSONs include: * Frequency JSON (optional) - `example here `__ * Generates the frequencies panel, e.g. on `nextstrain.org/flu `__. -.. note:: - - We are working on ways to make datasets in Newick / Nexus formats available. You can see an early prototype of this at `auspice-us.herokuapp.com `__ where you can drop on Newick (and CSV) files. Using BEAST trees is possible, but you have to use Augur to convert them first. .. note:: @@ -123,12 +118,3 @@ Narratives For narratives, please see `Writing a Narrative `__ for a description of the file format. .. _introduction-obtaining-a-set-of-input-files: - -Obtaining a Set of Input Files ------------------------------- - -If you'd like to download the dataset JSONs which are behind the core-datasets shown on `nextstrain.org `__, then you can run `this script `__ which will create a ``./data`` directory for you. - -The nextstrain-maintained narratives are stored in the `nextstrain/narratives github repo `__. You can obtain these by cloning that repo. - -You can then run ``auspice view --datasetDir data --narrativeDir `` to visualise all of the `nextstrain.org `__ datasets locally. diff --git a/docs/server/api.rst b/docs/server/api.rst index 9201a7fb8..36adf4afe 100644 --- a/docs/server/api.rst +++ b/docs/server/api.rst @@ -71,27 +71,12 @@ A ``204`` reponse will cause Auspice to show its splash page listing the availab **URL query arguments:** - ``prefix`` (required) - the pathname of the requesting page in Auspice. Use this to determine which narrative to return. -- ``type`` (optional) - the format of the data returned (see below for more information). Current valid values are "json" and "md". If no type is specified the server will use "json" as a default (for backwards compatibility reasons). Requests to this API from the Auspice client are made with ``type=md``. +- ``type`` (required) - this must be ``type=md`` for historical reasons **Response (on success):** -The response depends on the ``type`` specified in the query. +The narrative file is sent to the client (unmodified, to be parsed client-side). -If a markdown format is requested, then the narrative file is sent to the client unmodified to be parsed on the client. - -If a JSON is requested then the narrative file is parsed into JSON format by the server. For Auspice versions prior to v2.18 this was the only expected behavior. The transformation from markdown (i.e. the narrative file itself) to JSON is via the ``parseNarrativeFile()`` function (see below for how this is exported from Auspice for use in other servers). Here, roughly, is the code we use in the auspice server for this transformation: - -.. code:: js - - const fileContents = fs.readFileSync(pathName, 'utf8'); - if (type === "json") { - const blocks = parseNarrative(fileContents); - res.send(JSON.stringify(blocks).replace(/`__. - -``parseNarrativeFile`` -~~~~~~~~~~~~~~~~~~~~~~ - -.. warning:: - - This function is deprecated as of vXXX. You can now send the untransformed contents of the narrative file (markdown) for client-side parsing. See :ref:`above ` for more details. - -**Signature:** - -.. code:: js - - const blocks = parseNarrativeFile(fileContents); - -where ``fileContents`` is a string representation of the narrative Markdown file. - -**Returns:** - -An array of objects, each entry representing a different narrative "block" or "page". Each object has properties - -* ``__html`` -- the HTML to render in the sidebar to form the narrative -* ``dataset`` -- the dataset associated with this block -* ``query`` -- the query associated with this block diff --git a/docs/server/overview.rst b/docs/server/overview.rst index 401262945..464fa4083 100644 --- a/docs/server/overview.rst +++ b/docs/server/overview.rst @@ -73,6 +73,6 @@ It should be simple to deploy a custom version of auspice to any server, but we Deploying to Heroku is straightforward, but there are a few points to note: 1. You must set the config var ``HOST=0.0.0.0`` for the app. -2. You will need to either create a ``Procfile`` or a ``npm run start`` script which calls ``auspice view`` (or ``npx auspice view`` depending on how you implement auspice). -3. Make sure the datasets to be served are either (a) included in your git repo or (b) downloaded by the heroku build pipeline. `We use option (b) `_ by specifying a npm script called ``heroku-postbuild``. - +2. You will need to either create a ``Procfile`` or a ``npm run start`` script which calls ``auspice view`` (or some equivalent). +3. Datasets & narratives can be sourced by providing positional argument(s) to ``auspice view`` or by writing your own request handlers for the server. + If you need to download datasets at heroku-buildtime you can use the ``heroku-postbuild`` npm script. diff --git a/index.js b/index.js index fe9962920..be52de20b 100644 --- a/index.js +++ b/index.js @@ -1,16 +1,11 @@ /** This file exports the functions / objects available to node packages - * which import auspice (e.g. via `const auspice = require("auspice")`) + * which import auspice (e.g. via `import { convertFromV1 } from "auspice"`) * * These are intended to be "helper functions" for those wishing to write * their own auspice server, i.e. a server which handles the GET * requests originating from a auspice client. */ +import { convertFromV1 } from "./cli/server/convertJsonSchemas.js"; -const convertFromV1 = require("./cli/server/convertJsonSchemas").convertFromV1; -const parseNarrativeFile = require("./cli/server/getNarrative").parseNarrative; - -module.exports = { - convertFromV1, - parseNarrativeFile -}; +export { convertFromV1 }; diff --git a/package-lock.json b/package-lock.json index 119203516..1d3eadea3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,11 +115,10 @@ "@types/lodash": "^4.17.13", "@types/node": "^18.15.11", "@types/webpack-env": "^1.18.2", - "@typescript-eslint/eslint-plugin": "^5.57.0", - "@typescript-eslint/parser": "^5.57.0", - "chai": "^4.1.2", - "chai-http": "^4.0.0", - "eslint": "^8.39.0", + "@typescript-eslint/eslint-plugin": "^8.60.1", + "@typescript-eslint/parser": "^8.60.1", + "babel-plugin-transform-import-meta": "^2.3.3", + "eslint": "^8.57.1", "eslint-plugin-react": "^7.2.1", "eslint-plugin-react-hooks": "^4.0.1", "jest": "^29.5.0", @@ -130,8 +129,8 @@ "typescript": "~5.9" }, "engines": { - "node": "^20 || ^22 || ^24", - "npm": "^9 || ^10 || ^11" + "node": "^24 || ^26", + "npm": "^11" } }, "node_modules/@asamuzakjp/css-color": { @@ -2101,50 +2100,44 @@ "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, "funding": { "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", - "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -2163,13 +2156,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -2180,11 +2175,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2198,6 +2213,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -2206,10 +2222,11 @@ } }, "node_modules/@eslint/js": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", - "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -2229,13 +2246,15 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -2256,10 +2275,12 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -6533,18 +6554,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/chai": { - "version": "4.2.22", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz", - "integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==", - "dev": true - }, - "node_modules/@types/cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", - "dev": true - }, "node_modules/@types/d3-array": { "version": "2.12.7", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-2.12.7.tgz", @@ -7166,12 +7175,6 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, - "node_modules/@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", - "dev": true - }, "node_modules/@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -7184,16 +7187,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/superagent": { - "version": "3.8.7", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.7.tgz", - "integrity": "sha512-9KhCkyXv268A2nZ1Wvu7rQWM+BmdYUVkycFeNnYrUL5Zwu7o8wPQ3wBfW59dDP+wuoxw0ww8YKgTNv8j/cgscA==", - "dev": true, - "dependencies": { - "@types/cookiejar": "*", - "@types/node": "*" - } - }, "node_modules/@types/tapable": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", @@ -7283,130 +7276,149 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz", - "integrity": "sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", + "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/type-utils": "5.59.2", - "@typescript-eslint/utils": "5.59.2", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/type-utils": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.60.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "node_modules/@typescript-eslint/parser": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", + "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3" }, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.2.tgz", - "integrity": "sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", + "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/typescript-estree": "5.59.2", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.60.1", + "@typescript-eslint/types": "^8.60.1", + "debug": "^4.4.3" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", - "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", + "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/visitor-keys": "5.59.2" + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", + "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz", - "integrity": "sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", + "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.2", - "@typescript-eslint/utils": "5.59.2", - "debug": "^4.3.4", - "tsutils": "^3.21.0" + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", - "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", + "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -7414,65 +7426,76 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", - "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", + "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/visitor-keys": "5.59.2", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "@typescript-eslint/project-service": "8.60.1", + "@typescript-eslint/tsconfig-utils": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, + "license": "MIT", "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=10" + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", "dev": true, "license": "ISC", "bin": { @@ -7483,55 +7506,41 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", - "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", + "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/typescript-estree": "5.59.2", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", - "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", + "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.59.2", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "8.60.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -7539,17 +7548,25 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -7749,6 +7766,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -8036,26 +8054,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -8312,6 +8315,20 @@ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" }, + "node_modules/babel-plugin-transform-import-meta": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-import-meta/-/babel-plugin-transform-import-meta-2.3.3.tgz", + "integrity": "sha512-bbh30qz1m6ZU1ybJoNOhA2zaDvmeXMnGNBMVMDOJ1Fni4+wMBoy/j7MTRVmqAUCIcy54/rEnr9VEBsfcgbpm3Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/template": "^7.25.9", + "tslib": "^2.8.1" + }, + "peerDependencies": { + "@babel/core": "^7.10.0" + } + }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", @@ -8740,62 +8757,6 @@ "node": ">=0.8" } }, - "node_modules/chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", - "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chai-http": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.3.0.tgz", - "integrity": "sha512-zFTxlN7HLMv+7+SPXZdkd5wUlK+KxH6Q7bIEMiEx0FK3zuuMqL7cwICAQ0V1+yYRozBburYuxN1qZstgHpFZQg==", - "dev": true, - "dependencies": { - "@types/chai": "4", - "@types/superagent": "^3.8.3", - "cookiejar": "^2.1.1", - "is-ip": "^2.0.0", - "methods": "^1.1.2", - "qs": "^6.5.1", - "superagent": "^3.7.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chai-http/node_modules/ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/chai-http/node_modules/is-ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", - "integrity": "sha1-aO6gfooKCpTC0IDdZ0xzGrKkYas=", - "dev": true, - "dependencies": { - "ip-regex": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/chain-function": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.1.tgz", @@ -8823,15 +8784,6 @@ "node": ">=10" } }, - "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/check-types": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz", @@ -8992,18 +8944,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -9017,12 +8957,6 @@ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, - "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -9183,12 +9117,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true - }, "node_modules/copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -9600,9 +9528,10 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -9627,23 +9556,12 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, - "node_modules/deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", @@ -9686,15 +9604,6 @@ "node": ">=6" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -9742,18 +9651,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -10138,27 +10035,30 @@ } }, "node_modules/eslint": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", - "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.39.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -10166,22 +10066,19 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -10293,6 +10190,19 @@ "node": ">=8.0.0" } }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -10370,10 +10280,11 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -10385,23 +10296,12 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint/node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -10443,7 +10343,17 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/eslint/node_modules/is-path-inside": { @@ -10468,19 +10378,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/eslint/node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -10496,23 +10393,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -10543,15 +10423,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -10576,18 +10447,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -10601,27 +10460,16 @@ } }, "node_modules/espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "eslint-visitor-keys": "^3.4.1" }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -10883,45 +10731,11 @@ } ] }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -11112,55 +10926,6 @@ "is-callable": "^1.1.3" } }, - "node_modules/form-data": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.35", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/form-data/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/formidable": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", - "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", - "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", - "dev": true, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -11278,16 +11043,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -11469,11 +11224,12 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" }, "node_modules/gzip-size": { "version": "5.1.1", @@ -11801,10 +11557,11 @@ "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -14234,16 +13991,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -14423,6 +14170,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -14644,15 +14405,6 @@ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -14837,12 +14589,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -15157,6 +14903,24 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/outer-product": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/outer-product/-/outer-product-0.0.4.tgz", @@ -15349,15 +15113,6 @@ "node": ">=8" } }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -15551,6 +15306,16 @@ "node": ">=0.10.0" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/pretty-error": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", @@ -16908,37 +16673,6 @@ "stylis": "^3.5.0" } }, - "node_modules/superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "deprecated": "Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at .", - "dev": true, - "dependencies": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/superagent/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -17172,6 +16906,54 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tldts": { "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", @@ -17239,6 +17021,19 @@ "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -17297,31 +17092,24 @@ } }, "node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^1.8.1" + "prelude-ls": "^1.2.1" }, "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + "node": ">= 0.8.0" } }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -19530,37 +19318,29 @@ "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, "@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "requires": { - "eslint-visitor-keys": "^3.3.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", - "dev": true - } + "eslint-visitor-keys": "^3.4.3" } }, "@eslint-community/regexpp": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", - "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true }, "@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -19576,18 +19356,24 @@ "dev": true }, "globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" } }, - "js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true + }, + "js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -19602,9 +19388,9 @@ } }, "@eslint/js": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", - "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true }, "@hot-loader/react-dom": { @@ -19619,13 +19405,13 @@ } }, "@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", "minimatch": "^3.0.5" } }, @@ -19636,9 +19422,9 @@ "dev": true }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, "@istanbuljs/load-nyc-config": { @@ -21565,18 +21351,6 @@ "@babel/types": "^7.20.7" } }, - "@types/chai": { - "version": "4.2.22", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz", - "integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==", - "dev": true - }, - "@types/cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", - "dev": true - }, "@types/d3-array": { "version": "2.12.7", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-2.12.7.tgz", @@ -22098,12 +21872,6 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, - "@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", - "dev": true - }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -22115,16 +21883,6 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, - "@types/superagent": { - "version": "3.8.7", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.7.tgz", - "integrity": "sha512-9KhCkyXv268A2nZ1Wvu7rQWM+BmdYUVkycFeNnYrUL5Zwu7o8wPQ3wBfW59dDP+wuoxw0ww8YKgTNv8j/cgscA==", - "dev": true, - "requires": { - "@types/cookiejar": "*", - "@types/node": "*" - } - }, "@types/tapable": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", @@ -22210,156 +21968,166 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz", - "integrity": "sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", + "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", "dev": true, "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/type-utils": "5.59.2", - "@typescript-eslint/utils": "5.59.2", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "dependencies": { - "semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true - } + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/type-utils": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" } }, "@typescript-eslint/parser": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.2.tgz", - "integrity": "sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", + "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/typescript-estree": "5.59.2", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3" + } + }, + "@typescript-eslint/project-service": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", + "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "dev": true, + "requires": { + "@typescript-eslint/tsconfig-utils": "^8.60.1", + "@typescript-eslint/types": "^8.60.1", + "debug": "^4.4.3" } }, "@typescript-eslint/scope-manager": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", - "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", + "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/visitor-keys": "5.59.2" + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1" } }, + "@typescript-eslint/tsconfig-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", + "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "dev": true, + "requires": {} + }, "@typescript-eslint/type-utils": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz", - "integrity": "sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", + "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.59.2", - "@typescript-eslint/utils": "5.59.2", - "debug": "^4.3.4", - "tsutils": "^3.21.0" + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" } }, "@typescript-eslint/types": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", - "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", + "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", - "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", + "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/visitor-keys": "5.59.2", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "@typescript-eslint/project-service": "8.60.1", + "@typescript-eslint/tsconfig-utils": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "dependencies": { - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "requires": { + "balanced-match": "^4.0.2" + } + }, + "minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "brace-expansion": "^5.0.5" } }, "semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", "dev": true } } }, "@typescript-eslint/utils": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", - "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", + "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", "dev": true, "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/typescript-estree": "5.59.2", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true - } + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1" } }, "@typescript-eslint/visitor-keys": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", - "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", + "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.2", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "8.60.1", + "eslint-visitor-keys": "^5.0.0" }, "dependencies": { "eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true } } }, + "@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true + }, "@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -22724,23 +22492,11 @@ "is-string": "^1.0.7" } }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, "async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -22940,6 +22696,16 @@ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" }, + "babel-plugin-transform-import-meta": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-import-meta/-/babel-plugin-transform-import-meta-2.3.3.tgz", + "integrity": "sha512-bbh30qz1m6ZU1ybJoNOhA2zaDvmeXMnGNBMVMDOJ1Fni4+wMBoy/j7MTRVmqAUCIcy54/rEnr9VEBsfcgbpm3Q==", + "dev": true, + "requires": { + "@babel/template": "^7.25.9", + "tslib": "^2.8.1" + } + }, "babel-preset-current-node-syntax": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", @@ -23231,52 +22997,6 @@ } } }, - "chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - } - }, - "chai-http": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.3.0.tgz", - "integrity": "sha512-zFTxlN7HLMv+7+SPXZdkd5wUlK+KxH6Q7bIEMiEx0FK3zuuMqL7cwICAQ0V1+yYRozBburYuxN1qZstgHpFZQg==", - "dev": true, - "requires": { - "@types/chai": "4", - "@types/superagent": "^3.8.3", - "cookiejar": "^2.1.1", - "is-ip": "^2.0.0", - "methods": "^1.1.2", - "qs": "^6.5.1", - "superagent": "^3.7.0" - }, - "dependencies": { - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", - "dev": true - }, - "is-ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", - "integrity": "sha1-aO6gfooKCpTC0IDdZ0xzGrKkYas=", - "dev": true, - "requires": { - "ip-regex": "^2.0.0" - } - } - } - }, "chain-function": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.1.tgz", @@ -23298,12 +23018,6 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, "check-types": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz", @@ -23422,15 +23136,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, "commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -23441,12 +23146,6 @@ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, "compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -23550,12 +23249,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true - }, "copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -23904,9 +23597,9 @@ } }, "debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "requires": { "ms": "^2.1.3" } @@ -23923,15 +23616,6 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -23967,12 +23651,6 @@ "rimraf": "^2.6.3" } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -24001,15 +23679,6 @@ "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", "dev": true }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -24297,27 +23966,28 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", - "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.39.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -24325,22 +23995,19 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "dependencies": { @@ -24397,21 +24064,15 @@ "dev": true }, "eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, - "eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", - "dev": true - }, "estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -24443,6 +24104,12 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true + }, "is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -24458,16 +24125,6 @@ "argparse": "^2.0.1" } }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -24477,20 +24134,6 @@ "p-locate": "^5.0.0" } }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -24509,12 +24152,6 @@ "p-limit": "^3.0.2" } }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -24533,15 +24170,6 @@ "has-flag": "^4.0.0" } }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -24625,23 +24253,21 @@ "estraverse": "^4.1.1" } }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, "espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", - "dev": true - } + "eslint-visitor-keys": "^3.4.1" } }, "esprima": { @@ -24825,41 +24451,11 @@ "mime": "^1.3.4" } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -25009,34 +24605,6 @@ "is-callable": "^1.1.3" } }, - "form-data": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.35", - "safe-buffer": "^5.2.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "formidable": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", - "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", - "dev": true - }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -25115,12 +24683,6 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, - "get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true - }, "get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -25246,10 +24808,10 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "gzip-size": { @@ -25473,9 +25035,9 @@ "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" }, "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true }, "immer": { @@ -27247,12 +26809,6 @@ } } }, - "js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "dev": true - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -27377,6 +26933,16 @@ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -27553,12 +27119,6 @@ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -27695,12 +27255,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -27913,6 +27467,20 @@ "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==" }, + "optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + } + }, "outer-product": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/outer-product/-/outer-product-0.0.4.tgz", @@ -28054,12 +27622,6 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -28197,6 +27759,12 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, "pretty-error": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", @@ -29205,35 +28773,6 @@ "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==", "requires": {} }, - "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "dev": true, - "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -29384,6 +28923,31 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "requires": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "dependencies": { + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, + "picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true + } + } + }, "tldts": { "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", @@ -29438,6 +29002,13 @@ "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" }, + "ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "requires": {} + }, "ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -29471,25 +29042,17 @@ } }, "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "requires": { - "tslib": "^1.8.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } + "prelude-ls": "^1.2.1" } }, "type-detect": { diff --git a/package.json b/package.json index 82a5df6d4..896de20dc 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,16 @@ "author": "James Hadfield, Trevor Bedford and Richard Neher", "license": "AGPL-3.0-only", "repository": "github:nextstrain/auspice", + "type": "module", "homepage": "https://www.npmjs.com/package/auspice", "engines": { - "node": "^20 || ^22 || ^24", - "npm": "^9 || ^10 || ^11" + "node": "^24 || ^26", + "npm": "^11" }, "bin": { "auspice": "./auspice.js" }, - "main": "index.js", + "exports": "./index.js", "scripts": { "view": "node auspice.js view --verbose", "dev": "node auspice.js develop --verbose", @@ -23,14 +24,13 @@ "prepare": "npm run build", "lint": "eslint --max-warnings=0 .", "lint:fix": "eslint --fix .", - "type-check": "tsc", - "type-check:watch": "npm run type-check -- --watch", - "get-data": "env bash ./scripts/get-data.sh", + "type-check": "tsc && tsc --project cli/tsconfig.json", + "type-check:watch": "tsc --watch", "fetch-test-data": "./scripts/fetch-test-data", - "heroku-postbuild": "npm run build && npm run get-data", - "gzip-and-upload": "env bash ./scripts/gzip-and-upload.sh", "test": "jest test/*.js test/*.ts", "smoke-test": "NODE_ENV=test ENV=dev npx playwright test", + "profile": "node test/profiling/runProfiling.mjs", + "render-equiv": "node test/profiling/renderEquivalence.mjs", "diff-lang": "./scripts/diff-lang.js" }, "dependencies": { @@ -137,11 +137,10 @@ "@types/lodash": "^4.17.13", "@types/node": "^18.15.11", "@types/webpack-env": "^1.18.2", - "@typescript-eslint/eslint-plugin": "^5.57.0", - "@typescript-eslint/parser": "^5.57.0", - "chai": "^4.1.2", - "chai-http": "^4.0.0", - "eslint": "^8.39.0", + "@typescript-eslint/eslint-plugin": "^8.60.1", + "@typescript-eslint/parser": "^8.60.1", + "babel-plugin-transform-import-meta": "^2.3.3", + "eslint": "^8.57.1", "eslint-plugin-react": "^7.2.1", "eslint-plugin-react-hooks": "^4.0.1", "jest": "^29.5.0", diff --git a/playwright.config.ts b/playwright.config.ts index 60a705cd5..7d2905d8b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ }, ], webServer: { - command: 'npm run server', + command: 'npm run view -- test/data test/fetched-jsons', url: 'http://localhost:4000', reuseExistingServer: true, }, diff --git a/scripts/diff-lang.js b/scripts/diff-lang.js index a926d6965..4f7a1e3e1 100755 --- a/scripts/diff-lang.js +++ b/scripts/diff-lang.js @@ -1,7 +1,10 @@ #!/usr/bin/env node /* eslint-disable no-console */ -const fs = require('fs'); -const path = require('path'); +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); main(); diff --git a/scripts/extract-release-notes.js b/scripts/extract-release-notes.js index 52d00fe69..05d989229 100644 --- a/scripts/extract-release-notes.js +++ b/scripts/extract-release-notes.js @@ -3,7 +3,7 @@ * It is intended to be run as part of releaseNewVersion.sh */ -const fs = require('fs'); +import fs from 'fs'; function main() { const releaseNotes = []; diff --git a/scripts/fetch-test-data b/scripts/fetch-test-data index 2a74ac750..60bdba662 100755 --- a/scripts/fetch-test-data +++ b/scripts/fetch-test-data @@ -1,12 +1,54 @@ -#!/usr/bin/env bash +#!/usr/bin/env node -# Provision specific JSONs for tests +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; -set -x -e -o pipefail +const __dirname = path.dirname(fileURLToPath(import.meta.url)); -DEST="test/fetched-jsons" -CHARON="https://nextstrain.org/charon/getDataset?prefix=" -FETCH="curl --fail --compressed" +const DEST = path.join(__dirname, '..', 'test', 'fetched-jsons'); +const CHARON = 'https://nextstrain.org/charon/getDataset?prefix='; -${FETCH} ${CHARON}/ebola/all-outbreaks@2026-05-18 --output ${DEST}/ebola-all-outbreaks.json -${FETCH} ${CHARON}/ebola/ebov-2013@2025-10-14 --output ${DEST}/ebola-ebov-2013.json +const datasets = [ + { src: 'ebola/all-outbreaks@2026-05-18', dst: 'ebola-all-outbreaks' }, + { src: 'ebola/ebov-2013@2025-10-14', dst: 'ebola-ebov-2013' }, + { src: 'mumps/global@2026-05-30', dst: 'mumps', tipFrequencies: true }, + { src: 'zika@2026-06-05', dst: 'zika'}, +]; + +async function main() { + if (!fs.existsSync(DEST)) { + console.error(`Expected directory ${DEST} to exist`); + process.exit(1); + } + + const fetches = datasets.flatMap(({src, dst, tipFrequencies}) => { + const items = [ + {url: `${CHARON}/${src}`, output: `${dst}.json`}, + ]; + if (tipFrequencies) { + items.push({url: `${CHARON}/${src}&type=tip-frequencies`, output: `${dst}_tip-frequencies.json`}); + } + return items; + }); + + await Promise.all( + fetches.map(async ({url, output}) => { + console.log(`Fetching ${url}`); + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); + } + const outputPath = path.join(DEST, output); + fs.writeFileSync(outputPath, Buffer.from(await res.arrayBuffer())); + console.log(` -> ${outputPath}`); + }) + ); + + console.log(`Fetched ${fetches.length} files`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/get-data.sh b/scripts/get-data.sh deleted file mode 100755 index 7861efdc6..000000000 --- a/scripts/get-data.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/bash - -data_files=( - "dengue_all.json" "dengue_denv1.json" "dengue_denv2.json" "dengue_denv3.json" "dengue_denv4.json"\ - "ebola.json" "ebola_root-sequence.json" \ - "ebola_2019-09-14-no-epi-id_meta.json" "ebola_2019-09-14-no-epi-id_tree.json" \ - "lassa_s_tree.json" "lassa_s_meta.json" \ - "lassa_l_tree.json" "lassa_l_meta.json" \ - "measles.json" \ - "mers_tree.json" "mers_meta.json" \ - "mumps_global.json" "mumps_na.json" \ - "WNV_NA_tree.json" "WNV_NA_meta.json" \ - "zika.json" \ - "tb_global_meta.json" "tb_global_tree.json" \ - "enterovirus_d68_genome_meta.json" "enterovirus_d68_genome_tree.json" \ - "enterovirus_d68_vp1_meta.json" "enterovirus_d68_vp1_tree.json" \ - ############## AVIAN FLU ############## - "flu_avian_h7n9_ha.json" \ - "flu_avian_h7n9_mp.json" \ - "flu_avian_h7n9_na.json" \ - "flu_avian_h7n9_np.json" \ - "flu_avian_h7n9_ns.json" \ - "flu_avian_h7n9_pa.json" \ - "flu_avian_h7n9_pb1.json" \ - "flu_avian_h7n9_pb2.json" \ - ############## SEASONAL FLU ############## - "flu_seasonal_h3n2_ha_2y.json" "flu_seasonal_h3n2_ha_2y_tip-frequencies.json" \ - "flu_seasonal_h3n2_ha_3y.json" "flu_seasonal_h3n2_ha_3y_tip-frequencies.json" \ - "flu_seasonal_h3n2_ha_6y.json" "flu_seasonal_h3n2_ha_6y_tip-frequencies.json" \ - "flu_seasonal_h3n2_ha_12y.json" "flu_seasonal_h3n2_ha_12y_tip-frequencies.json" \ - "flu_seasonal_h3n2_na_2y.json" "flu_seasonal_h3n2_na_2y_tip-frequencies.json" \ - "flu_seasonal_h3n2_na_3y.json" "flu_seasonal_h3n2_na_3y_tip-frequencies.json" \ - "flu_seasonal_h3n2_na_6y.json" "flu_seasonal_h3n2_na_6y_tip-frequencies.json" \ - "flu_seasonal_h3n2_na_12y.json" "flu_seasonal_h3n2_na_12y_tip-frequencies.json" \ - "flu_seasonal_h1n1pdm_ha_2y.json" "flu_seasonal_h1n1pdm_ha_2y_tip-frequencies.json" \ - "flu_seasonal_h1n1pdm_ha_3y.json" "flu_seasonal_h1n1pdm_ha_3y_tip-frequencies.json" \ - "flu_seasonal_h1n1pdm_ha_6y.json" "flu_seasonal_h1n1pdm_ha_6y_tip-frequencies.json" \ - "flu_seasonal_h1n1pdm_ha_12y.json" "flu_seasonal_h1n1pdm_ha_12y_tip-frequencies.json" \ - "flu_seasonal_h1n1pdm_ha_pandemic_meta.json" "flu_seasonal_h1n1pdm_ha_pandemic_tree.json" "flu_seasonal_h1n1pdm_ha_pandemic_tip-frequencies.json" \ - "flu_seasonal_h1n1pdm_na_2y.json" "flu_seasonal_h1n1pdm_na_2y_tip-frequencies.json" \ - "flu_seasonal_h1n1pdm_na_3y.json" "flu_seasonal_h1n1pdm_na_3y_tip-frequencies.json" \ - "flu_seasonal_h1n1pdm_na_6y.json" "flu_seasonal_h1n1pdm_na_6y_tip-frequencies.json" \ - "flu_seasonal_h1n1pdm_na_12y.json" "flu_seasonal_h1n1pdm_na_12y_tip-frequencies.json" \ - "flu_seasonal_h1n1pdm_na_pandemic_tree.json" "flu_seasonal_h1n1pdm_na_pandemic_meta.json" "flu_seasonal_h1n1pdm_na_pandemic_tip-frequencies.json" \ - "flu_seasonal_vic_ha_2y.json" "flu_seasonal_vic_ha_2y_tip-frequencies.json" "flu_seasonal_vic_ha_2y_root-sequence.json" \ - "flu_seasonal_vic_ha_3y.json" "flu_seasonal_vic_ha_3y_tip-frequencies.json" "flu_seasonal_vic_ha_3y_root-sequence.json" \ - "flu_seasonal_vic_ha_6y.json" "flu_seasonal_vic_ha_6y_tip-frequencies.json" "flu_seasonal_vic_ha_6y_root-sequence.json" \ - "flu_seasonal_vic_ha_12y.json" "flu_seasonal_vic_ha_12y_tip-frequencies.json" "flu_seasonal_vic_ha_12y_root-sequence.json" \ - "flu_seasonal_vic_na_2y.json" "flu_seasonal_vic_na_2y_tip-frequencies.json" "flu_seasonal_vic_na_2y_root-sequence.json" \ - "flu_seasonal_vic_na_3y.json" "flu_seasonal_vic_na_3y_tip-frequencies.json" "flu_seasonal_vic_na_3y_root-sequence.json" \ - "flu_seasonal_vic_na_6y.json" "flu_seasonal_vic_na_6y_tip-frequencies.json" "flu_seasonal_vic_na_6y_root-sequence.json" \ - "flu_seasonal_vic_na_12y.json" "flu_seasonal_vic_na_12y_tip-frequencies.json" "flu_seasonal_vic_na_12y_root-sequence.json" \ - "flu_seasonal_yam_ha_2y.json" "flu_seasonal_yam_ha_2y_tip-frequencies.json" "flu_seasonal_yam_ha_2y_root-sequence.json" \ - "flu_seasonal_yam_ha_3y.json" "flu_seasonal_yam_ha_3y_tip-frequencies.json" "flu_seasonal_yam_ha_3y_root-sequence.json" \ - "flu_seasonal_yam_ha_6y.json" "flu_seasonal_yam_ha_6y_tip-frequencies.json" "flu_seasonal_yam_ha_6y_root-sequence.json" \ - "flu_seasonal_yam_ha_12y.json" "flu_seasonal_yam_ha_12y_tip-frequencies.json" "flu_seasonal_yam_ha_12y_root-sequence.json" \ - "flu_seasonal_yam_na_2y.json" "flu_seasonal_yam_na_2y_tip-frequencies.json" "flu_seasonal_yam_na_2y_root-sequence.json" \ - "flu_seasonal_yam_na_3y.json" "flu_seasonal_yam_na_3y_tip-frequencies.json" "flu_seasonal_yam_na_3y_root-sequence.json" \ - "flu_seasonal_yam_na_6y.json" "flu_seasonal_yam_na_6y_tip-frequencies.json" "flu_seasonal_yam_na_6y_root-sequence.json" \ - "flu_seasonal_yam_na_12y.json" "flu_seasonal_yam_na_12y_tip-frequencies.json" "flu_seasonal_yam_na_12y_root-sequence.json" \ - ############## LATEST CORE SARS-CoV-2 (COVID-19) BUILDS ############## - "ncov_gisaid_global.json" "ncov_gisaid_global_tip-frequencies.json" \ - "ncov_gisaid_africa.json" "ncov_gisaid_africa_tip-frequencies.json" \ - "ncov_gisaid_asia.json" "ncov_gisaid_asia_tip-frequencies.json" \ - "ncov_gisaid_europe.json" "ncov_gisaid_europe_tip-frequencies.json" \ - "ncov_gisaid_north-america.json" "ncov_gisaid_north-america_tip-frequencies.json" \ - "ncov_gisaid_oceania.json" "ncov_gisaid_oceania_tip-frequencies.json" \ - "ncov_gisaid_south-america.json" "ncov_gisaid_south-america_tip-frequencies.json" \ - ############## TIMESTAMPED SARS-CoV-2 BUILDS USED IN NARRATIVES ############# - "ncov_2020-01-23.json" "ncov_2020-01-25.json" "ncov_2020-01-26.json" "ncov_2020-01-30.json" \ - "ncov_2020-03-04.json" "ncov_2020-03-05.json" "ncov_2020-03-11.json" "ncov_2020-03-13.json" \ - "ncov_2020-03-20.json" "ncov_2020-03-27.json" "ncov_2020-04-03.json" \ - "ncov_global_2020-04-09.json" "ncov_north-america_2020-04-17.json" \ -) - -rm -rf data/ -mkdir -p data/ -for i in "${data_files[@]}" -do - curl http://data.nextstrain.org/"${i}" --compressed -o data/"${i}" -done - -echo "Copying the test datasets from test/data to data" -cp -r test/data/*.json data/ - -echo "The local data directory ./data now contains a selection of up-to-date datasets from http://data.nextstrain.org" diff --git a/src/actions/updateMetadata/updateMetadata.ts b/src/actions/updateMetadata/updateMetadata.ts index f9bb84a3b..f00942a4f 100644 --- a/src/actions/updateMetadata/updateMetadata.ts +++ b/src/actions/updateMetadata/updateMetadata.ts @@ -128,7 +128,7 @@ function _reduxControls( /* geographic resolutions */ if (newMetadata.geographic?.length && !state.panelsAvailable.includes("map")) { - updates.panelsAvailable = [...state.panelsAvailable, "map"], + updates.panelsAvailable = [...state.panelsAvailable, "map"]; updates.panelsToDisplay = [...updates.panelsAvailable]; updates.canTogglePanelLayout = hasMultipleGridPanels(updates.panelsAvailable); updates.geoResolution = newMetadata.geographic[0].key; diff --git a/src/components/controls/animation-controls.js b/src/components/controls/animation-controls.js index 203aabc74..0f782f42c 100644 --- a/src/components/controls/animation-controls.js +++ b/src/components/controls/animation-controls.js @@ -21,9 +21,8 @@ class AnimationControls extends React.Component { return ( { - this.props.animationPlayPauseButton === "Play" ? - this.props.dispatch({type: MAP_ANIMATION_PLAY_PAUSE_BUTTON, data: "Pause"}) : - this.props.dispatch({type: MAP_ANIMATION_PLAY_PAUSE_BUTTON, data: "Play"}); + const newState = this.props.animationPlayPauseButton === "Play" ? "Pause" : "Play"; + this.props.dispatch({ type: MAP_ANIMATION_PLAY_PAUSE_BUTTON, data: newState }); }} > diff --git a/src/components/controls/filter.js b/src/components/controls/filter.js index f27e949b3..3c3f9069a 100644 --- a/src/components/controls/filter.js +++ b/src/components/controls/filter.js @@ -82,7 +82,6 @@ class FilterData extends React.Component { ...(this.props.totalStateCountsSecondTree?.[traitName]?.keys() || []), ]); - this.props.totalStateCounts[traitName]; const traitTitle = this.getFilterTitle(traitName); const filterValuesCurrentlyActive = new Set((this.props.activeFilters[traitName] || []).filter((x) => x.active).map((x) => x.value)); for (const traitValue of Array.from(traitData).sort()) { diff --git a/src/components/controls/language.js b/src/components/controls/language.js index 452031ac8..dea408410 100644 --- a/src/components/controls/language.js +++ b/src/components/controls/language.js @@ -20,7 +20,7 @@ class Language extends React.Component { try { const res = await import(/* webpackMode: "lazy-once" */ `../../locales/${lang}/${ns}.json`); i18n.addResourceBundle(lang, ns, res.default); - } catch (err) { + } catch (_err) { console.error(`Language ${lang} not found!`); } } diff --git a/src/components/framework/fine-print.js b/src/components/framework/fine-print.js index 5943a1eec..98ecf5225 100644 --- a/src/components/framework/fine-print.js +++ b/src/components/framework/fine-print.js @@ -11,7 +11,7 @@ import { publications } from "../download/downloadModal"; import { hasExtension, getExtension } from "../../util/extensions"; import { canShowLinkOuts } from "../modal/LinkOutModalContents.jsx"; -const logoPNG = require("../../images/favicon.png"); +import logoPNG from "../../images/favicon.png"; const MarkdownDisplay = lazy(() => import("../markdownDisplay")); diff --git a/src/components/info/byline.js b/src/components/info/byline.js index 8da817e44..33da38f80 100644 --- a/src/components/info/byline.js +++ b/src/components/info/byline.js @@ -3,6 +3,7 @@ import { connect } from "react-redux"; import { withTranslation, Trans } from 'react-i18next'; import styled from 'styled-components'; import { headerFont } from "../../globalStyles"; +import gisaidLogo from "../../images/gisaid-logo.png"; /** * React component for the byline of the current dataset. @@ -138,7 +139,7 @@ function renderDataProvenance(t, metadata) { .map((source) => { if (source.name.toUpperCase() === "GISAID") { // SPECIAL CASE return ( - gisaid-logo + gisaid-logo ); } const url = parseUrl(source.url); @@ -171,7 +172,7 @@ function parseUrl(potentialUrl) { try { const urlObj = new URL(potentialUrl); return urlObj.href; - } catch (err) { + } catch (_err) { return false; } } diff --git a/src/components/map/mapHelpersLatLong.js b/src/components/map/mapHelpersLatLong.js index 09abe4b51..b51162674 100644 --- a/src/components/map/mapHelpersLatLong.js +++ b/src/components/map/mapHelpersLatLong.js @@ -221,13 +221,13 @@ const maybeConstructTransmissionEvent = ( try { latOrig = demeToLatLongs[nodeLocation].latitude; longOrig = demeToLatLongs[nodeLocation].longitude; - } catch (e) { + } catch (_err) { demesMissingLatLongs.add(nodeLocation); } try { latDest = demeToLatLongs[childLocation].latitude; longDest = demeToLatLongs[childLocation].longitude; - } catch (e) { + } catch (_err) { demesMissingLatLongs.add(childLocation); } @@ -503,7 +503,7 @@ const updateTransmissionDataColAndVis = (transmissionData, transmissionIndices, transmissionDataCopy[index].color = col; transmissionDataCopy[index].visible = vis; }); - } catch (err) { + } catch (_err) { console.warn(`Error trying to access ${id} in transmissionIndices. Map transmissions may be wrong.`); } }); diff --git a/src/components/navBar/content.js b/src/components/navBar/content.js index dfc1b8136..b96ce4d98 100644 --- a/src/components/navBar/content.js +++ b/src/components/navBar/content.js @@ -1,7 +1,6 @@ import React from "react"; import styled from 'styled-components'; - -const logoPNG = require("../../images/logo-light.svg"); +import logoPNG from "../../images/logo-light.svg"; const AuspiceNavBarContainer = styled.div` display: flex; diff --git a/src/components/splash/splash.js b/src/components/splash/splash.js index deb35e9c4..5c89de5f3 100644 --- a/src/components/splash/splash.js +++ b/src/components/splash/splash.js @@ -4,6 +4,7 @@ import NavBar from "../navBar"; import Flex from "../../components/framework/flex"; import { CenterContent } from "./centerContent"; import { FinePrintStyles, getCitation, getCustomFinePrint } from "../../components/framework/fine-print"; +import logoLightSvg from "../../images/logo-light.svg"; const getNumColumns = (width) => width > 1000 ? 3 : width > 750 ? 2 : 1; @@ -32,9 +33,7 @@ const SplashContent = ({available, browserDimensions, errorMessage}) => { logo diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index c478e5f92..3a8769163 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -141,6 +141,7 @@ export const modifySVG = function modifySVG( transitionTime: number, extras: Extras, ): void { + timerStart("modifySVG"); let updateCall: UpdateCall; const classesToPotentiallyUpdate: TreeElement[] = [".tip", ".vaccineDottedLine", ".vaccineCross", ".branch"]; /* order is respected */ /* treat stem / branch specially, but use these to replace a normal .branch call if that's also to be applied */ @@ -222,6 +223,7 @@ export const modifySVG = function modifySVG( } else { this.removeMeasurementsColoringCrosshair(); } + timerEnd("modifySVG"); }; /* instead of modifying the SVG the "normal" way, this is sometimes too janky (e.g. when we need to move everything) diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index 7b8e21169..a1537c670 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -558,6 +558,7 @@ export const mapToScreen = function mapToScreen(this: PhyloTreeType): void { if (this.params.showStreamTrees) { this.mapStreamsToScreen() } + timerEnd("mapToScreen"); }; /** diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index c615c2111..bd6fb68d6 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -138,7 +138,10 @@ export class TreeComponent extends React.Component {mainTree ? this.domRefs.mainTree = c : this.domRefs.secondTree = c;}} + ref={(c): void => { + if (mainTree) { this.domRefs.mainTree = c; } + else { this.domRefs.secondTree = c; } + }} /> diff --git a/src/root.js b/src/root.js index d774ef4e8..28bd662ed 100644 --- a/src/root.js +++ b/src/root.js @@ -16,7 +16,7 @@ const DebugNarrative = lazy(() => import("./components/narrativeEditor/narrative * This triggers a window resize which in turn triggers a general * rerender. A bit ham-fisted but gets the job done for the time being * */ -if (module.hot) { +if (import.meta.webpackHot) { setTimeout(() => window.dispatchEvent(new Event('resize')), 500); } diff --git a/src/store.ts b/src/store.ts index 69c87306b..5a6ff4247 100644 --- a/src/store.ts +++ b/src/store.ts @@ -41,10 +41,9 @@ const store = configureStore({ devTools: process.env.NODE_ENV !== 'production', }) -if (process.env.NODE_ENV !== 'production' && module.hot) { - // console.log("hot reducer reload"); - module.hot.accept('./reducers', () => { - const nextRootReducer = require('./reducers/index'); +if (process.env.NODE_ENV !== 'production' && import.meta.webpackHot) { + import.meta.webpackHot.accept('./reducers', async () => { + const nextRootReducer = (await import('./reducers/index')).default; store.replaceReducer(nextRootReducer); }); } diff --git a/src/util/entropy.js b/src/util/entropy.js index 076771e6d..b9aa76edc 100644 --- a/src/util/entropy.js +++ b/src/util/entropy.js @@ -29,6 +29,7 @@ const calcMutationCounts = (nodes, visibility, selectedCds) => { mutations?.[cdsName]?.forEach((m) => { const {from, to, pos} = parseMutation(m); if (valid(from, to, isAA)) { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions sparse[pos] ? sparse[pos]++ : sparse[pos] = 1; } }); @@ -39,7 +40,7 @@ const calcMutationCounts = (nodes, visibility, selectedCds) => { for (let i = 0; i < sparse.length; i++) { if (!sparse[i]) {continue;} if (sparse[i] > m) {m = sparse[i];} - counts[j] = isAA ? + counts[j] = isAA ? {codon: parseInt(i, 10), y: sparse[i]} : {x: i, y: sparse[i]}; /* TODO reset y scale in D3 or compute entropy */ j++; @@ -118,7 +119,7 @@ const calcEntropy = (nodes, visibility, selectedCds) => { counts[position][anc_state[position]] = nUnobserved; } } - + let s = 0; /* shannon entropy */ for (const count of Object.values(counts[position])) { const a = count / visibleTips; @@ -177,10 +178,10 @@ export function getCdsByName(genomeMap, name) { * codon). * If _either_ of the rangeGenome positions are _beyond_ the CDS then we return * the entire rangeLocal of the cds + set the flag `valid` to false. - * + * * For wrapping genes, the UI forces the rangeGenome (i.e. the zoomCoordinates) * to be on either side of the origin, and we maintain thus assumption here. - * + * * Returns [rangeLocalInView:rangeLocal, valid:bool] */ export function getCdsRangeLocalFromRangeGenome(cds, rangeGenome) { @@ -203,12 +204,12 @@ export function getCdsRangeLocalFromRangeGenome(cds, rangeGenome) { * The general approach is to visit the segments in the order they appear, * i.e. in the context of the strand it's always 5' -> 3' but for -ve strand * CDSs this appears 3' -> 5' in the context of the +ve strand. Once we find - * an intersection we can work out the appropriate local coordinate. + * an intersection we can work out the appropriate local coordinate. * Remember that zoomStart/End are reversed if the CDS is wrapping! */ let prevSeg; for (const seg of segments) { - /* If the zoom start (5') is inside the segment, then we know one of the local bounds */ + /* If the zoom start (5') is inside the segment, then we know one of the local bounds */ if (seg.rangeGenome[0]<=zoomStart && seg.rangeGenome[1]>=zoomStart) { if (positive) { const delta = zoomStart - seg.rangeGenome[0]; @@ -217,8 +218,8 @@ export function getCdsRangeLocalFromRangeGenome(cds, rangeGenome) { const delta = zoomStart - seg.rangeGenome[0]; cdsLocalEnd = seg.rangeLocal[1] - delta; } - } - /* If the zoom end (3') is inside the segment, then we know one of the local bounds */ + } + /* If the zoom end (3') is inside the segment, then we know one of the local bounds */ if (seg.rangeGenome[0]<=zoomEnd && seg.rangeGenome[1]>=zoomEnd) { if (positive) { const delta = zoomEnd - seg.rangeGenome[0]; @@ -349,4 +350,4 @@ export function nucleotideToAaPosition(genomeMap, nucPos) { } }) return matches; -} \ No newline at end of file +} diff --git a/src/util/extensions.ts b/src/util/extensions.ts index 1f7c5caa1..6e4e7de19 100644 --- a/src/util/extensions.ts +++ b/src/util/extensions.ts @@ -6,7 +6,6 @@ type Extensions = { const registry: Extensions = ((): Extensions => { if (!process.env.EXTENSION_DATA) { - // console.log("no EXTENSION_DATA found"); return {}; } @@ -15,12 +14,10 @@ const registry: Extensions = ((): Extensions => { Object.keys(extensions).forEach((key: string) => { if (key.endsWith("Component")) { - // console.log("loading component", key); /* "@extensions" is a webpack alias */ extensions[key] = require(`@extensions/${extensions[key]}`).default; } }); - // console.log("extensions", extensions); return extensions; })(); diff --git a/src/util/parseNarrative.js b/src/util/parseNarrative.js index 5ab213539..378ee9185 100644 --- a/src/util/parseNarrative.js +++ b/src/util/parseNarrative.js @@ -1,10 +1,10 @@ /* eslint no-param-reassign: off */ -const { safeLoadFront } = require('yaml-front-matter'); -const queryString = require("query-string"); +import { safeLoadFront } from 'yaml-front-matter'; +import queryString from "query-string"; -const parseMarkdownNarrativeFile = (fileContents, markdownParser) => { +export const parseMarkdownNarrativeFile = (fileContents, markdownParser) => { const frontMatter = safeLoadFront(fileContents); if (Object.keys(frontMatter).length === 1) { @@ -22,7 +22,7 @@ const parseMarkdownNarrativeFile = (fileContents, markdownParser) => { }; -function createTitleSlideFromFrontmatter(frontMatter, markdownParser) { +export function createTitleSlideFromFrontmatter(frontMatter, markdownParser) { let markdown = ""; // A markdown interpretation of the YAML for display markdown += parseTitleSlideTitle(frontMatter); @@ -45,7 +45,7 @@ function createTitleSlideFromFrontmatter(frontMatter, markdownParser) { }; } -function parseTitleSlideTitle(frontMatter) { +export function parseTitleSlideTitle(frontMatter) { if (!frontMatter.title) throw new Error("Narrative YAML frontmatter must define a title!"); return `## ${frontMatter.title}\n`; } @@ -54,7 +54,7 @@ function parseUrl(urlString, fallbackDataset=false) { let url, dataset; try { url = new URL(urlString); - } catch (err) { + } catch (_err) { if (fallbackDataset) { console.error(`Couldn't parse the URL "${urlString}". Falling back to "${fallbackDataset}".`); return {dataset: fallbackDataset, query: ""}; @@ -139,7 +139,7 @@ function* parseNarrativeBody(markdown, fallbackDataset, markdownParser) { } } -function parseNarrativeAuthors(frontMatter) { +export function parseNarrativeAuthors(frontMatter) { let authorMd = ""; const authors = parseAttributions(frontMatter, "authors", "authorLinks"); if (authors) { @@ -152,7 +152,7 @@ function parseNarrativeAuthors(frontMatter) { return authorMd+"\n"; } -function parseNarrativeTranslators(frontMatter) { +export function parseNarrativeTranslators(frontMatter) { const translators = parseAttributions(frontMatter, "translators", "translatorLinks"); if (translators) return `### Translators: ${translators}\n`; return ""; @@ -212,13 +212,3 @@ function attributionLink(attribution, attributionLinkValue) { } return attribution; } - - -module.exports = { - parseMarkdownNarrativeFile, - /* following functions exported for unit testing */ - createTitleSlideFromFrontmatter, - parseTitleSlideTitle, - parseNarrativeAuthors, - parseNarrativeTranslators -}; diff --git a/src/util/treeJsonProcessing.ts b/src/util/treeJsonProcessing.ts index 6ff2de6b1..9bb1b4984 100644 --- a/src/util/treeJsonProcessing.ts +++ b/src/util/treeJsonProcessing.ts @@ -164,6 +164,7 @@ const collectObservedMutations = (nodesArray: ReduxNode[]): Mutations => { if (!n.branch_attrs || !n.branch_attrs.mutations) return; Object.entries(n.branch_attrs.mutations).forEach(([gene, muts]) => { muts.forEach((mut) => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions mutations[`${gene}:${mut}`] ? mutations[`${gene}:${mut}`]++ : (mutations[`${gene}:${mut}`] = 1); }); }); diff --git a/src/util/treeMiscHelpers.js b/src/util/treeMiscHelpers.js index 6960bd8c9..117c6ab25 100644 --- a/src/util/treeMiscHelpers.js +++ b/src/util/treeMiscHelpers.js @@ -103,7 +103,7 @@ function validateUrl(url) { if (url.startsWith("https_")) url = url.replace("https_", "https:"); const urlObj = new URL(url); return urlObj.href; - } catch (err) { + } catch (_err) { console.warn(`Dataset provided the invalid URL ${url}`); return undefined; } diff --git a/src/util/treeVisibilityHelpers.js b/src/util/treeVisibilityHelpers.js index 20987d265..101447021 100644 --- a/src/util/treeVisibilityHelpers.js +++ b/src/util/treeVisibilityHelpers.js @@ -25,7 +25,7 @@ export const strainNameToIdx = (nodes, name) => { /** * Find the node with the given label name & value * NOTE: if there are multiple nodes with the same label then `null` is returned - * + * * @param {Array} nodes tree nodes (flat) * @param {string} labelName label name * @param {string} labelValue label value @@ -157,7 +157,7 @@ const getInView = (tree) => { let inView; try { inView = tree.nodes.map((d) => d.shell.inView); - } catch (e) { + } catch (_err) { /* edge case: this fn may be called before the shell structure of the nodes * has been created (i.e. phyloTree's not run yet). In this case, it's * safe to assume that everything's in view */ diff --git a/src/version.js b/src/version.js index fcc0c5a6a..72dd2e177 100644 --- a/src/version.js +++ b/src/version.js @@ -1,5 +1 @@ -const version = "2.72.0"; - -module.exports = { - version -}; +export const version = "2.72.0"; diff --git a/test/colorScales.test.js b/test/colorScales.test.js index fb61a75a1..e0debdcc3 100644 --- a/test/colorScales.test.js +++ b/test/colorScales.test.js @@ -1,6 +1,6 @@ import { unknownColor, createNonContinuousScaleFromProvidedScaleMap } from "../src/util/colorScale"; -const crypto = require("crypto"); +import crypto from "crypto" /* ---------------------------------------------------------------------- */ diff --git a/test/profiling/.gitignore b/test/profiling/.gitignore new file mode 100644 index 000000000..382345845 --- /dev/null +++ b/test/profiling/.gitignore @@ -0,0 +1 @@ +.server.log diff --git a/test/profiling/README.md b/test/profiling/README.md new file mode 100644 index 000000000..e093b489d --- /dev/null +++ b/test/profiling/README.md @@ -0,0 +1,99 @@ +# Auspice profiling harness + +A headless, reproducible harness that measures Auspice's built-in `src/util/perf.js` +timers (`timerStart`/`timerEnd`) across representative datasets and produces a ranked +baseline of where time is spent. **Measure-only** — it changes no application code. + +## Render-equivalence suite (`npm run render-equiv`) + +A regression suite (`renderEquivalence.mjs` + `domSnapshot.mjs`) that guards the +incremental tree-update path. For each operation (colorBy, layout, distance, filter, +zoom, confidence, …) and for sequences of operations, it drives the **incremental** +update in-app — via `history.pushState` + a `popstate` event, which the app's own +listener turns into an incremental `phylotree.change()` (zero source changes) — and +asserts the settled SVG DOM is **identical** to a from-scratch **full render** of the +same end state (a fresh page at the app's resulting URL). Any mismatch is a stale-DOM +regression. + +```bash +npm run render-equiv # reuse current dist/ build +npm run render-equiv -- --build # force a fresh build first +npm run render-equiv -- --only ebola-zoom,ebola-filter-date-colorby +``` + +Run it after any change to the tree render/update code. It surfaced (and we then +fixed) the branch stem-offset divergence in the selective-redraw optimization. + +## Quick start + +```bash +# 1. build (--includeTiming, production) + serve data/ + run all scenarios +npm run profile -- --build + +# subsequent runs can skip the rebuild if dist/ already has timers: +npm run profile + +# compare a run against a previous baseline (before/after): +npm run profile -- --baseline test/profiling/baselines/baseline-latest.json + +# emulate a slower device (also lifts small-tree spans above perf.js's 20ms floor): +npm run profile -- --throttle 4 + +# run a subset: +npm run profile -- --only zika-load,spike-animation +``` + +Outputs land in `test/profiling/baselines/`: +- `baseline-.json` and `baseline-latest.json` — machine-readable (per + scenario × marker × span: median/p95/count/total + raw samples). +- `report-.md` — ranked hotspot report (also printed to stdout). + +## How it works (and why) + +- **Timers must be compiled in.** Normal builds strip `timerStart`/`timerEnd` + (`babel.config.cjs`). `cli/build.ts --includeTiming` keeps them in a **production, + minified** bundle. The harness runs that build and serves it with `auspice view` + — production-representative numbers, *not* `auspice develop` (dev mode distorts + timings). It asserts the served bundle actually contains timer output or fails loudly. +- **Serving `data/`.** The stock `playwright.config.ts` webServer only serves + `test/data test/fetched-jsons`; this harness serves the top-level `data/` dir too + (so `/spike-sm` etc. resolve). It reuses an already-running :4000 server if present. +- **Capturing timers.** `page.on('console')` accepts both `log` and `warning` + (perf.js uses `console.warn` for calls >20ms). It records the per-call `took` + value and ignores perf.js's cumulative `Average`. +- **Isolation.** A fresh browser context per trial resets perf.js's accumulator, so + samples never bleed across scenarios. Each sample is tagged with the interaction + marker in flight. +- **Driving interactions.** Most spans are measured via URL-driven loads (a load with + `?l=radial` runs the same layout/mapToScreen code as an in-app change). The + incremental `phylotree.change()` cascade is captured by the URL-driven `animate` + scenario. DOM-click steps (colorby/layout) are best-effort extras that skip + gracefully if a control can't be driven. +- **Render-complete signal.** Waits for the `phyloTree render()` timer line (not + `networkidle`, which is meaningless for a 35k-tip D3 render), with a + `svg#MainTree circle.tip` DOM fallback. + +## Datasets + +Uses whatever is in the repo's `data/` dir. The stress target is +`data/spike-sm.json` (~35k tips, all four panels + tip-frequencies sidecar), which +exercises every instrumented span. `zika`/`ebola` give fast small/medium baselines. + +## Known gotchas + +- The 55MB spike load is slow; scenarios use generous (180s) timeouts. +- `updateFrequencyData` is debounced (~500ms) — spike scenarios wait it out. +- Animation and large loads emit many console lines; that's expected. +- If a run reports no timers, the served `dist/` is stale/stripped — rerun with `--build`. + +## Files + +| File | Role | +|---|---| +| `runProfiling.mjs` | entrypoint / flags / orchestration / guard | +| `buildAndServe.mjs` | `--includeTiming` build guard + `auspice view` server | +| `scenarios.mjs` | declarative scenario matrix | +| `driveScenario.mjs` | per-trial page driving + console capture | +| `consoleTimers.mjs` | timer-line parser + marker-tagging collector | +| `aggregate.mjs` | samples → median/p95/total per (scenario × marker × span) | +| `report.mjs` | baseline JSON + ranked markdown (+ Δ vs baseline) | diff --git a/test/profiling/aggregate.mjs b/test/profiling/aggregate.mjs new file mode 100644 index 000000000..4c804837d --- /dev/null +++ b/test/profiling/aggregate.mjs @@ -0,0 +1,57 @@ +/** + * Aggregate flat timer samples into per-(scenario x marker x span) statistics. + * Uses the per-call `took` values; reports median/p95 (robust to outliers) + * rather than mean. + */ + +function median(sorted) { + const n = sorted.length; + if (n === 0) return 0; + return n % 2 ? sorted[(n - 1) / 2] : (sorted[n / 2 - 1] + sorted[n / 2]) / 2; +} + +function percentile(sorted, p) { + const n = sorted.length; + if (n === 0) return 0; + const idx = Math.min(n - 1, Math.max(0, Math.ceil(p * n) - 1)); + return sorted[idx]; +} + +function stats(values) { + const sorted = [...values].sort((a, b) => a - b); + return { + count: sorted.length, + median: median(sorted), + p95: percentile(sorted, 0.95), + min: sorted[0] ?? 0, + max: sorted[sorted.length - 1] ?? 0, + total: values.reduce((a, b) => a + b, 0), + }; +} + +// Field delimiter for group keys. Span names contain spaces (e.g. +// "phyloTree render()"), so we use the NUL char, which cannot appear in a field. +// Built via fromCharCode to keep the source file pure ASCII (no raw NUL byte). +const SEP = String.fromCharCode(0); + +/** @returns {Array<{scenario,marker,name,count,median,p95,min,max,total,samples}>} */ +export function aggregate(samples) { + const real = samples.filter((s) => typeof s.took === "number" && !s.name.startsWith("__")); + const groups = new Map(); + for (const s of real) { + const key = `${s.scenario}${SEP}${s.marker}${SEP}${s.name}`; + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(s.took); + } + const rows = []; + for (const [key, vals] of groups) { + const [scenario, marker, name] = key.split(SEP); + rows.push({ scenario, marker, name, ...stats(vals), samples: vals }); + } + rows.sort((a, b) => b.median - a.median); + return rows; +} + +export function rowKey(r) { + return `${r.scenario}${SEP}${r.marker}${SEP}${r.name}`; +} diff --git a/test/profiling/baselines/.gitkeep b/test/profiling/baselines/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/test/profiling/buildAndServe.mjs b/test/profiling/buildAndServe.mjs new file mode 100644 index 000000000..57c4df789 --- /dev/null +++ b/test/profiling/buildAndServe.mjs @@ -0,0 +1,113 @@ +/** + * Orchestrate a timers-enabled production build and an `auspice view` server. + * + * - ensureTimingBuild(): guarantees dist/ holds a `--includeTiming` production + * bundle (rebuilds if missing/stripped), asserting timers survived. + * - startServer()/stopServer(): serve the top-level `data/` dir (which the stock + * playwright.config.ts webServer does NOT do) on :4000, reusing an already + * running server if present. + */ + +import { spawn } from "node:child_process"; +import { readdirSync, readFileSync, existsSync, openSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +export const ROOT = path.resolve(HERE, "..", ".."); +const PORT = 4000; +const BASE_URL = `http://localhost:${PORT}`; +const AVAILABLE_URL = `${BASE_URL}/charon/getAvailable`; + +// A string literal from perf.js's timerEnd() output — present in the bundle only +// when timer calls were NOT stripped (i.e. built with --includeTiming). +const TIMER_MARKER = "ms. Average:"; + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +function distHasTimers() { + const distDir = path.join(ROOT, "dist"); + if (!existsSync(distDir)) return false; + const jsFiles = readdirSync(distDir).filter((f) => f.endsWith(".js")); + return jsFiles.some((f) => + readFileSync(path.join(distDir, f), "utf8").includes(TIMER_MARKER) + ); +} + +function runBuild() { + return new Promise((resolve, reject) => { + console.log("[build] node auspice.js build --includeTiming (production, timers on) …"); + const proc = spawn("node", ["auspice.js", "build", "--includeTiming"], { + cwd: ROOT, + stdio: "inherit", + }); + proc.on("exit", (code) => + code === 0 ? resolve() : reject(new Error(`build exited with code ${code}`)) + ); + proc.on("error", reject); + }); +} + +export async function ensureTimingBuild({ force = false } = {}) { + if (!force && distHasTimers()) { + console.log("[build] reusing existing timers-enabled dist/ build"); + return; + } + await runBuild(); + if (!distHasTimers()) { + throw new Error( + "[build] FATAL: production build did not retain timers — check babel.config.cjs strip logic / --includeTiming" + ); + } + console.log("[build] timers-enabled build confirmed in dist/"); +} + +async function isServerUp() { + try { + const r = await fetch(AVAILABLE_URL); + return r.ok; + } catch { + return false; + } +} + +let serverProc = null; + +export async function startServer() { + if (await isServerUp()) { + console.log(`[serve] reusing server already listening on ${BASE_URL}`); + return { spawned: false }; + } + const logPath = path.join(HERE, ".server.log"); + const out = openSync(logPath, "a"); + console.log(`[serve] starting: auspice view data test/data test/fetched-jsons (log: ${logPath})`); + serverProc = spawn( + "node", + ["auspice.js", "view", "data", "test/data", "test/fetched-jsons"], + { cwd: ROOT, stdio: ["ignore", out, out] } + ); + serverProc.on("error", (e) => console.error("[serve] spawn error:", e)); + + const deadline = Date.now() + 60000; + while (Date.now() < deadline) { + if (await isServerUp()) { + console.log(`[serve] up at ${BASE_URL}`); + return { spawned: true }; + } + if (serverProc.exitCode !== null) { + throw new Error(`[serve] server exited early (code ${serverProc.exitCode}); see ${logPath}`); + } + await sleep(500); + } + throw new Error(`[serve] server did not become ready within 60s; see ${logPath}`); +} + +export async function stopServer() { + if (serverProc && serverProc.exitCode === null) { + serverProc.kill("SIGTERM"); + console.log("[serve] stopped spawned server"); + } + serverProc = null; +} + +export { BASE_URL }; diff --git a/test/profiling/consoleTimers.mjs b/test/profiling/consoleTimers.mjs new file mode 100644 index 000000000..a6b9e7059 --- /dev/null +++ b/test/profiling/consoleTimers.mjs @@ -0,0 +1,84 @@ +/** + * Parse and collect Auspice's `src/util/perf.js` console timer output. + * + * perf.js prints one line per timerEnd(): + * `Timer {name} (#{n}) took {X}ms. Average: {Y}ms.` + * via console.warn when the call took >20ms, else console.log. We must therefore + * accept BOTH Playwright console message types 'log' and 'warning' — dropping + * 'warning' would silently discard every slow (interesting) span. + * + * We record the per-call `took` value (per-interaction). The reported `Average` + * is a cumulative session mean held in perf.js's module-scoped `dbsingle`, so it + * is NOT per-interaction and is deliberately ignored. + */ + +export const TIMER_RE = /^Timer (.+?) \(#(\d+)\) took (\d+)ms\. Average: (\d+)ms\.$/; + +export function parseTimerLine(text) { + const m = TIMER_RE.exec(text); + if (!m) return null; + return { name: m[1], callIndex: Number(m[2]), took: Number(m[3]), avg: Number(m[4]) }; +} + +export class TimerCollector { + constructor() { + this.samples = []; // { marker, name, took, callIndex } + this.marker = null; // label of the interaction currently in flight + this._waiters = []; // one-shot resolvers keyed by span name + } + + attach(page) { + page.on("console", (msg) => { + const type = msg.type(); + if (type !== "log" && type !== "warning") return; + const parsed = parseTimerLine(msg.text()); + if (!parsed) return; + this.samples.push({ + marker: this.marker, + name: parsed.name, + took: parsed.took, + callIndex: parsed.callIndex, + }); + this._notify(parsed); + }); + } + + setMarker(marker) { + this.marker = marker; + } + + hadAnyTimer() { + return this.samples.length > 0; + } + + /** All collected samples (marker-tagged). */ + all() { + return this.samples; + } + + _notify(parsed) { + if (this._waiters.length === 0) return; + const remaining = []; + for (const w of this._waiters) { + if (w.name === parsed.name) w.resolve(true); + else remaining.push(w); + } + this._waiters = remaining; + } + + /** + * Resolve when the NEXT console line for `name` arrives (after this call), + * or `false` on timeout. Used as an interaction-settled signal, e.g. wait for + * the "phyloTree render()" or "phylotree.change()" line that ends a render. + */ + waitForSpan(name, timeoutMs) { + return new Promise((resolve) => { + const waiter = { name, resolve }; + this._waiters.push(waiter); + setTimeout(() => { + this._waiters = this._waiters.filter((w) => w !== waiter); + resolve(false); + }, timeoutMs); + }); + } +} diff --git a/test/profiling/domSnapshot.mjs b/test/profiling/domSnapshot.mjs new file mode 100644 index 000000000..2f0737064 --- /dev/null +++ b/test/profiling/domSnapshot.mjs @@ -0,0 +1,153 @@ +/** + * Capture and compare the rendered phyloTree SVG DOM. + * + * Used by renderEquivalence.mjs to assert that an incrementally-updated tree + * (phylotree.change()) is identical to a from-scratch full render of the same + * state. Captures the geometry/colour-critical, stable-id elements (tips, branch + * stems/tees, confidence lines) plus label groups, reading BOTH inline style and + * attribute (style wins, per CSS precedence) and normalizing values so cosmetic + * draw-vs-update differences (e.g. stroke-width "2" vs "2px") don't register. + */ + +/** Runs in the browser: returns a structured snapshot of the tree SVG. */ +export function snapshot(page) { + return page.evaluate(() => { + const round = (v) => { + const f = parseFloat(String(v).replace("px", "").trim()); + return Number.isNaN(f) ? String(v) : String(Math.round(f * 10) / 10); + }; + // read a property from style first (wins), else attribute + const raw = (el, name) => { + const s = el.style.getPropertyValue(name); + return s !== "" && s != null ? s : el.getAttribute(name) || ""; + }; + const prop = (el, name, isNum) => (isNum ? round(raw(el, name)) : raw(el, name)); + + const byId = (sel, spec) => { + const out = {}; + document.querySelectorAll(sel).forEach((el) => { + if (!el.id) return; + const rec = {}; + for (const [name, isNum] of spec) rec[name] = prop(el, name, isNum); + out[el.id] = rec; + }); + return out; + }; + + // label groups have no per-element id -> capture as a list keyed by content+pos + const textGroup = (sel) => + [...document.querySelectorAll(sel)].map((el) => ({ + text: el.textContent || "", + x: round(raw(el, "x")), + y: round(raw(el, "y")), + visibility: raw(el, "visibility"), + "font-size": round(raw(el, "font-size")), + })); + + return { + tips: byId("circle.tip", [ + ["cx", true], ["cy", true], ["r", true], + ["fill", false], ["stroke", false], ["visibility", false], + ]), + branchS: byId("path.branch.S", [ + ["d", false], ["stroke", false], ["stroke-width", true], ["visibility", false], + ]), + branchT: byId("path.branch.T", [ + ["d", false], ["stroke", false], ["stroke-width", true], ["visibility", false], + ]), + conf: byId("path.conf", [ + ["d", false], ["stroke", false], ["stroke-width", true], + ]), + tipLabels: textGroup("#tipLabels text.tipLabel"), + branchLabels: textGroup("#branchLabels text.branchLabel"), + }; + }); +} + +/** Known draw-vs-update inconsistencies to ignore (documented, not regressions). */ +const ALLOWLIST = { + // updateBranchLabels omits text-anchor/fill/font-family and hardcodes x=xTip-5 + // (drawBranchLabels is orientation-aware) — a real draw/update divergence tracked + // separately, not something an incremental *tree* op regresses. + branchLabels: new Set(["x"]), +}; + +const KEYED = ["tips", "branchS", "branchT", "conf"]; + +/** Compare two id-keyed element maps; returns mismatch records. */ +function diffKeyed(cls, ref, incr) { + const mismatches = []; + const only = { ref: 0, incr: 0 }; + const allow = ALLOWLIST[cls] || new Set(); + const ids = new Set([...Object.keys(ref), ...Object.keys(incr)]); + for (const id of ids) { + const a = ref[id], b = incr[id]; + if (!a) { only.incr++; continue; } + if (!b) { only.ref++; continue; } + for (const k of Object.keys(a)) { + if (allow.has(k)) continue; + if (a[k] !== b[k]) mismatches.push({ cls, id, prop: k, ref: a[k], incr: b[k] }); + } + } + return { mismatches, only }; +} + +/** Compare label lists (order-independent) after allowlisting props. */ +function diffLabels(cls, ref, incr) { + const allow = ALLOWLIST[cls] || new Set(); + const key = (r) => JSON.stringify(Object.fromEntries(Object.entries(r).filter(([k]) => !allow.has(k)))); + const rc = ref.map(key).sort(), ic = incr.map(key).sort(); + const mismatches = []; + if (rc.length !== ic.length) mismatches.push({ cls, id: "(count)", prop: "length", ref: rc.length, incr: ic.length }); + for (let i = 0; i < Math.max(rc.length, ic.length); i++) { + if (rc[i] !== ic[i]) { mismatches.push({ cls, id: `[${i}]`, prop: "record", ref: rc[i], incr: ic[i] }); break; } + } + return { mismatches, only: { ref: 0, incr: 0 } }; +} + +/** Full comparison of two snapshots. Returns {total, byClass, examples}. */ +export function compare(ref, incr) { + const byClass = {}; + const examples = []; + let total = 0; + for (const cls of KEYED) { + const { mismatches, only } = diffKeyed(cls, ref[cls], incr[cls]); + byClass[cls] = { diffs: mismatches.length, onlyRef: only.ref, onlyIncr: only.incr }; + total += mismatches.length + only.ref + only.incr; + for (const m of mismatches.slice(0, 4)) examples.push(m); + } + for (const cls of ["tipLabels", "branchLabels"]) { + const { mismatches } = diffLabels(cls, ref[cls], incr[cls]); + byClass[cls] = { diffs: mismatches.length }; + total += mismatches.length; + for (const m of mismatches.slice(0, 2)) examples.push(m); + } + return { total, byClass, examples: examples.slice(0, 8) }; +} + +/** Count how many element records changed between two snapshots (for the "op did something" guard). */ +export function countChanged(a, b) { + let n = 0; + for (const cls of KEYED) { + const ids = new Set([...Object.keys(a[cls]), ...Object.keys(b[cls])]); + for (const id of ids) { + const x = a[cls][id], y = b[cls][id]; + if (!x || !y) { n++; continue; } + if (Object.keys(x).some((k) => x[k] !== y[k])) n++; + } + } + return n; +} + +/** A cheap signature for settle-detection (do positions/paths still change?). */ +export function signature(page) { + return page.evaluate(() => { + const t = [...document.querySelectorAll("circle.tip")].slice(0, 50) + .map((e) => (e.getAttribute("cx") || "") + (e.style.visibility || "")).join(","); + const b = [...document.querySelectorAll("path.branch.S")].slice(0, 25) + .map((e) => e.getAttribute("d") || "").join("|"); + const counts = ["circle.tip", "path.branch.S", "path.branch.T", "path.conf"] + .map((s) => document.querySelectorAll(s).length).join("/"); + return counts + "##" + t + "##" + b; + }); +} diff --git a/test/profiling/driveScenario.mjs b/test/profiling/driveScenario.mjs new file mode 100644 index 000000000..42fc0c043 --- /dev/null +++ b/test/profiling/driveScenario.mjs @@ -0,0 +1,88 @@ +/** + * Run a single trial of one scenario in an isolated browser context. + * + * Isolation: a fresh BrowserContext per trial resets perf.js's module-scoped + * `dbsingle` accumulator, so timer samples never bleed across scenarios/trials. + * Every captured line is tagged with the active interaction marker. + */ + +import { scenarioUrl } from "./scenarios.mjs"; +import { TimerCollector } from "./consoleTimers.mjs"; +import { BASE_URL } from "./buildAndServe.mjs"; + +const RENDER_SPAN = "phyloTree render()"; +const CHANGE_SPAN = "phylotree.change()"; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +export async function runScenarioTrial(browser, scenario, { throttle = 1, trial = 0 } = {}) { + const context = await browser.newContext(); + const page = await context.newPage(); + const collector = new TimerCollector(); + collector.attach(page); + page.on("pageerror", (e) => console.error(` [pageerror ${scenario.id}] ${e.message}`)); + + if (throttle && throttle !== 1) { + const cdp = await context.newCDPSession(page); + await cdp.send("Emulation.setCPUThrottlingRate", { rate: throttle }); + } + + const url = BASE_URL + scenarioUrl(scenario); + const loadTimeout = scenario.loadTimeoutMs || 60000; + const settle = scenario.settleAfterMs ?? 800; + + // --- initial load: wait for the render-complete timer line, not networkidle --- + collector.setMarker("load"); + const rendered = collector.waitForSpan(RENDER_SPAN, loadTimeout); + await page.goto(url, { waitUntil: "commit", timeout: loadTimeout }); + const renderOk = await rendered; + if (!renderOk) { + try { + await page.locator("svg#MainTree circle.tip").first().waitFor({ timeout: 15000 }); + } catch { + console.warn(` [warn ${scenario.id}] no '${RENDER_SPAN}' line and no tips within timeout`); + } + } + await sleep(settle); + + // --- animation window (URL-driven auto-start): capture ticks + cascade --- + if (scenario.animateMs) { + collector.setMarker("animation"); + await sleep(scenario.animateMs); + } + + // --- best-effort DOM interaction steps --- + for (const step of scenario.steps || []) { + collector.setMarker(step.label); + try { + await runStep(page, collector, step); + } catch (e) { + console.warn(` [skip ${scenario.id}/${step.label}] ${e.message || e}`); + collector.samples.push({ marker: step.label, name: "__step_skipped__", took: 0 }); + } + await sleep(settle); + } + + collector.setMarker(null); + const hadTimers = collector.hadAnyTimer(); + const samples = collector.all().map((s) => ({ ...s, scenario: scenario.id, trial })); + await context.close(); + return { samples, hadTimers, renderOk }; +} + +async function runStep(page, collector, step) { + if (step.kind === "wait") { + await sleep(step.waitMs || 500); + return; + } + const changed = collector.waitForSpan(CHANGE_SPAN, 30000); + if (step.kind === "layout") { + await page.getByRole("button", { name: step.value, exact: true }).click({ timeout: 8000 }); + } else if (step.kind === "colorby") { + await page.locator("#selectColorBy").click({ timeout: 8000 }); + await page.keyboard.type(step.value, { delay: 20 }); + await page.keyboard.press("Enter"); + } else { + throw new Error(`unknown step kind '${step.kind}'`); + } + await changed; +} diff --git a/test/profiling/renderEquivalence.mjs b/test/profiling/renderEquivalence.mjs new file mode 100644 index 000000000..69a3d4c4f --- /dev/null +++ b/test/profiling/renderEquivalence.mjs @@ -0,0 +1,178 @@ +/** + * Render-equivalence regression suite. + * + * For each operation (colorBy, layout, distance, filter, zoom, …) and for + * sequences of operations, this drives the INCREMENTAL update path in-app and + * asserts the settled SVG DOM is identical to a from-scratch FULL render of the + * same end state. A mismatch = a stale-DOM regression in the incremental path. + * + * Driving: history.pushState(newURL) + a popstate event triggers the app's own + * listener (monitor.js) which takes the incremental branch (navigation.js Case 1, + * createStateFromQueryOrJSONs) — the same phylotree.change() path a control uses, + * with zero source changes. Reference: a fresh page at the app's resulting URL. + * + * npm run render-equiv (reuses the current dist/ build) + * npm run render-equiv -- --build (force a fresh build first) + * npm run render-equiv -- --only ebola-colorby-categorical,ebola-zoom + */ + +import { chromium } from "playwright"; +import { ensureTimingBuild, startServer, stopServer, BASE_URL } from "./buildAndServe.mjs"; +import { snapshot, compare, countChanged, signature } from "./domSnapshot.mjs"; + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +/** Build a query string, supporting valueless params (value === true -> `key`). */ +function buildQuery(params) { + const parts = []; + for (const [k, v] of Object.entries(params)) { + if (v === undefined || v === null || v === false) continue; + parts.push(v === true ? encodeURIComponent(k) : `${encodeURIComponent(k)}=${encodeURIComponent(v)}`); + } + return parts.join("&"); +} + +async function settle(page, { min = 700, max = 30000, interval = 250 } = {}) { + await sleep(min); + let prev = await signature(page); + let stable = 0; + const deadline = Date.now() + max; + while (Date.now() < deadline) { + await sleep(interval); + const cur = await signature(page); + if (cur === prev) { if (++stable >= 2) return; } else stable = 0; + prev = cur; + } +} + +async function loadFull(page, url, loadTimeout) { + await page.goto(url, { waitUntil: "commit", timeout: loadTimeout }); + await page.locator("circle.tip").first().waitFor({ state: "attached", timeout: loadTimeout }); + await settle(page, { max: loadTimeout }); +} + +async function driveIncremental(page, path, loadTimeout) { + await page.evaluate((p) => { + history.pushState({}, "", p); + window.dispatchEvent(new PopStateEvent("popstate")); + }, path); + await settle(page, { max: loadTimeout }); +} + +// --- Scenarios ------------------------------------------------------------- +// dataset: URL path segment. base: initial query params. steps: each merges +// `set` into the running params and drives one incremental op. +const SCENARIOS = [ + // ---- ebola: single operations ---- + { id: "ebola-colorby-categorical", dataset: "ebola", base: { d: "tree", c: "country" }, steps: [{ set: { c: "division" } }] }, + { id: "ebola-colorby-continuous", dataset: "ebola", base: { d: "tree", c: "country" }, steps: [{ set: { c: "num_date" } }] }, + { id: "ebola-layout-radial", dataset: "ebola", base: { d: "tree", l: "rect" }, steps: [{ set: { l: "radial" } }] }, + { id: "ebola-layout-unrooted", dataset: "ebola", base: { d: "tree", l: "rect" }, steps: [{ set: { l: "unrooted" } }] }, + { id: "ebola-layout-clock", dataset: "ebola", base: { d: "tree", l: "rect" }, steps: [{ set: { l: "clock" } }] }, + { id: "ebola-layout-scatter", dataset: "ebola", base: { d: "tree", l: "rect" }, steps: [{ set: { l: "scatter" } }] }, + { id: "ebola-distance", dataset: "ebola", base: { d: "tree", m: "num_date" }, steps: [{ set: { m: "div" } }] }, + { id: "ebola-datefilter", dataset: "ebola", base: { d: "tree" }, steps: [{ set: { dmin: "2014-08-01", dmax: "2015-03-01" } }] }, + { id: "ebola-traitfilter", dataset: "ebola", base: { d: "tree" }, steps: [{ set: { f_country: "Sierra Leone" } }] }, + { id: "ebola-zoom", dataset: "ebola", base: { d: "tree" }, steps: [{ set: { f_country: "Sierra Leone", treeZoom: "selected" } }] }, + { id: "ebola-confidence", dataset: "ebola", base: { d: "tree", m: "num_date" }, steps: [{ set: { ci: true } }] }, + + // ---- ebola: combinations (sequenced incremental ops) ---- + { id: "ebola-filter-then-colorby", dataset: "ebola", base: { d: "tree", c: "country" }, steps: [{ set: { f_country: "Sierra Leone" } }, { set: { c: "division" } }] }, + { id: "ebola-colorby-then-zoom", dataset: "ebola", base: { d: "tree", c: "country" }, steps: [{ set: { c: "division" } }, { set: { f_country: "Sierra Leone", treeZoom: "selected" } }] }, + { id: "ebola-date-then-layout", dataset: "ebola", base: { d: "tree" }, steps: [{ set: { dmin: "2014-08-01", dmax: "2015-03-01" } }, { set: { l: "radial" } }] }, + { id: "ebola-filter-date-colorby", dataset: "ebola", base: { d: "tree", c: "country" }, steps: [{ set: { f_country: "Sierra Leone" } }, { set: { dmin: "2014-08-01", dmax: "2015-03-01" } }, { set: { c: "division" } }] }, + + // ---- zika: variety ---- + { id: "zika-colorby", dataset: "zika", base: { d: "tree", c: "country" }, steps: [{ set: { c: "region" } }] }, + { id: "zika-layout-unrooted", dataset: "zika", base: { d: "tree", l: "rect" }, steps: [{ set: { l: "unrooted" } }] }, + + // ---- spike-sm: scale + streamtrees (slow) ---- + { id: "spike-colorby", dataset: "spike-sm", base: { d: "tree", c: "clade_membership" }, steps: [{ set: { c: "region" } }], slow: true }, + { id: "spike-datefilter", dataset: "spike-sm", base: { d: "tree" }, steps: [{ set: { dmin: "2021-01-01", dmax: "2021-09-01" } }], slow: true }, +]; + +async function runScenario(browser, scn) { + const loadTimeout = scn.slow ? 180000 : 45000; + const ctxIncr = await browser.newContext(); + const incrPage = await ctxIncr.newPage(); + const errors = []; + incrPage.on("pageerror", (e) => errors.push(e.message)); + + let params = { ...scn.base }; + await loadFull(incrPage, `${BASE_URL}/${scn.dataset}?${buildQuery(params)}`, loadTimeout); + const base = await snapshot(incrPage); + + for (const step of scn.steps) { + params = { ...params, ...step.set }; + await driveIncremental(incrPage, `/${scn.dataset}?${buildQuery(params)}`, loadTimeout); + } + const incr = await snapshot(incrPage); + const synced = new URL(incrPage.url()).pathname + new URL(incrPage.url()).search; + await ctxIncr.close(); + + const ctxRef = await browser.newContext(); + const refPage = await ctxRef.newPage(); + refPage.on("pageerror", (e) => errors.push(e.message)); + await loadFull(refPage, `${BASE_URL}${synced}`, loadTimeout); + const ref = await snapshot(refPage); + await ctxRef.close(); + + const result = compare(ref, incr); + const changed = countChanged(base, incr); + const pass = result.total === 0; + return { pass, changed, result, synced, errors }; +} + +function fmt(scn, r) { + const status = r.pass ? "PASS" : "FAIL"; + const counts = Object.entries(r.result.byClass) + .filter(([, v]) => v.diffs || v.onlyRef || v.onlyIncr) + .map(([k, v]) => `${k}:${v.diffs}${v.onlyRef || v.onlyIncr ? `(+/-${v.onlyRef}/${v.onlyIncr})` : ""}`) + .join(" "); + let line = ` [${status}] ${scn.id.padEnd(30)} mismatch=${r.result.total} changed=${r.changed}${counts ? " " + counts : ""}`; + if (r.changed === 0) line += " ⚠ op produced no DOM change (drive may have no-oped)"; + if (r.errors.length) line += ` ⚠ ${r.errors.length} pageerror`; + return line; +} + +async function main() { + const argv = process.argv.slice(2); + const build = argv.includes("--build"); + const onlyIdx = argv.indexOf("--only"); + const only = onlyIdx >= 0 ? argv[onlyIdx + 1].split(",") : null; + const scenarios = only ? SCENARIOS.filter((s) => only.includes(s.id)) : SCENARIOS; + + await ensureTimingBuild({ force: build }); + await startServer(); + const browser = await chromium.launch({ headless: true }); + const failures = []; + try { + for (const scn of scenarios) { + let r; + try { + r = await runScenario(browser, scn); + } catch (e) { + console.log(` [ERROR] ${scn.id}: ${e.message}`); + failures.push(scn.id); + continue; + } + console.log(fmt(scn, r)); + if (!r.pass) { + failures.push(scn.id); + for (const ex of r.result.examples) { + console.log(` ${ex.cls} ${ex.id} ${ex.prop}: ref=${JSON.stringify(ex.ref)} incr=${JSON.stringify(ex.incr)}`); + } + } + } + } finally { + await browser.close(); + await stopServer(); + } + console.log(`\n${scenarios.length - failures.length}/${scenarios.length} scenarios passed.`); + if (failures.length) { + console.log(`FAILED: ${failures.join(", ")}`); + process.exitCode = 1; + } +} + +main().catch((e) => { console.error("[FATAL]", e); stopServer().finally(() => process.exit(1)); }); diff --git a/test/profiling/report.mjs b/test/profiling/report.mjs new file mode 100644 index 000000000..fcffc34e3 --- /dev/null +++ b/test/profiling/report.mjs @@ -0,0 +1,115 @@ +/** + * Write the machine-readable baseline JSON and the human-readable ranked report. + * Both support before/after diffing against a prior baseline (measure-only — + * no code change is required to compare two runs). + */ + +import { writeFileSync, mkdirSync, readFileSync, existsSync } from "node:fs"; +import path from "node:path"; +import { rowKey } from "./aggregate.mjs"; + +export function writeBaselineJson(rows, meta, outDir) { + mkdirSync(outDir, { recursive: true }); + const payload = { meta, rows }; + const shaName = `baseline-${meta.gitSha || "unknown"}.json`; + const shaPath = path.join(outDir, shaName); + const latestPath = path.join(outDir, "baseline-latest.json"); + const json = JSON.stringify(payload, null, 2); + writeFileSync(shaPath, json); + writeFileSync(latestPath, json); + return { shaPath, latestPath }; +} + +export function loadBaseline(file) { + if (!file || !existsSync(file)) return null; + try { + const parsed = JSON.parse(readFileSync(file, "utf8")); + const map = new Map(); + for (const r of parsed.rows || []) map.set(rowKey(r), r); + return { meta: parsed.meta, map }; + } catch { + return null; + } +} + +const pad = (s, n) => String(s).padEnd(n); +const padL = (s, n) => String(s).padStart(n); +const ms = (v) => `${Math.round(v)}`; + +function deltaCell(cur, base) { + if (!base) return "—"; + const d = cur - base.median; + const pct = base.median ? (d / base.median) * 100 : 0; + const sign = d > 0 ? "+" : ""; + return `${sign}${ms(d)} (${sign}${pct.toFixed(0)}%)`; +} + +function table(rows, baseline) { + const cols = [ + ["span", 26], + ["scenario", 22], + ["marker", 18], + ["median", 8], + ["p95", 7], + ["max", 7], + ["n", 4], + ]; + if (baseline) cols.push(["Δ median", 16]); + const header = cols.map(([h, w]) => (h === "median" || h === "p95" || h === "max" || h === "n" ? padL(h, w) : pad(h, w))).join(" "); + const lines = [header, "-".repeat(header.length)]; + for (const r of rows) { + const base = baseline ? baseline.map.get(rowKey(r)) : null; + const cells = [ + pad(r.name, 26), + pad(r.scenario, 22), + pad(r.marker, 18), + padL(ms(r.median), 8), + padL(ms(r.p95), 7), + padL(ms(r.max), 7), + padL(r.count, 4), + ]; + if (baseline) cells.push(pad(deltaCell(r.median, base), 16)); + lines.push(cells.join(" ")); + } + return lines.join("\n"); +} + +export function buildRankedReport(rows, meta, baseline) { + const byMedian = [...rows].sort((a, b) => b.median - a.median); + const byTotal = [...rows].sort((a, b) => b.total - a.total); + + const out = []; + out.push(`# Auspice profiling baseline`); + out.push(""); + out.push(`- date: ${meta.date}`); + out.push(`- git: ${meta.gitSha} (${meta.gitBranch})`); + out.push(`- node: ${meta.node} cpuThrottle: ${meta.cpuThrottle}x`); + out.push(`- scenarios: ${meta.scenarioIds.join(", ")}`); + if (baseline) out.push(`- Δ vs baseline: ${baseline.meta?.gitSha} @ ${baseline.meta?.date}`); + out.push(""); + out.push(`## Top 15 hotspots by per-call median (ms)`); + out.push(""); + out.push("```"); + out.push(table(byMedian.slice(0, 15), baseline)); + out.push("```"); + out.push(""); + out.push(`## Top 15 by total time across all calls (ms)`); + out.push(""); + out.push("```"); + out.push(table(byTotal.slice(0, 15), baseline)); + out.push("```"); + out.push(""); + out.push(`## All spans (by median)`); + out.push(""); + out.push("```"); + out.push(table(byMedian, baseline)); + out.push("```"); + out.push(""); + return out.join("\n"); +} + +export function writeRankedMarkdown(rows, meta, outPath, baseline) { + const md = buildRankedReport(rows, meta, baseline); + writeFileSync(outPath, md); + return md; +} diff --git a/test/profiling/runProfiling.mjs b/test/profiling/runProfiling.mjs new file mode 100644 index 000000000..a1075bd23 --- /dev/null +++ b/test/profiling/runProfiling.mjs @@ -0,0 +1,116 @@ +/** + * Auspice profiling harness — entrypoint (MEASURE-ONLY). + * + * Ensures a --includeTiming production build, serves the local data/ dir, drives + * a set of scenarios headless while capturing perf.js console timers, and writes + * a baseline JSON + a ranked markdown report. + * + * node test/profiling/runProfiling.mjs [flags] + * --build force a fresh --includeTiming production build + * --throttle N CPU throttle rate (default 1 = off; e.g. 4 = 4x slower) + * --only a,b,c run only these scenario ids + * --baseline diff results against a prior baseline JSON + * --out output dir (default test/profiling/baselines) + */ + +import { chromium } from "playwright"; +import { execSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { ensureTimingBuild, startServer, stopServer, ROOT } from "./buildAndServe.mjs"; +import { scenarios as ALL_SCENARIOS } from "./scenarios.mjs"; +import { runScenarioTrial } from "./driveScenario.mjs"; +import { aggregate } from "./aggregate.mjs"; +import { writeBaselineJson, writeRankedMarkdown, loadBaseline } from "./report.mjs"; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); + +function parseFlags(argv) { + const flags = { build: false, throttle: 1, only: null, baseline: null, out: path.join(HERE, "baselines") }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--build") flags.build = true; + else if (a === "--throttle") flags.throttle = Number(argv[++i]); + else if (a === "--only") flags.only = argv[++i].split(",").map((s) => s.trim()); + else if (a === "--baseline") flags.baseline = argv[++i]; + else if (a === "--out") flags.out = path.resolve(argv[++i]); + } + return flags; +} + +function gitInfo() { + const run = (cmd) => { + try { return execSync(cmd, { cwd: ROOT }).toString().trim(); } catch { return "unknown"; } + }; + return { gitSha: run("git rev-parse --short HEAD"), gitBranch: run("git rev-parse --abbrev-ref HEAD") }; +} + +async function main() { + const flags = parseFlags(process.argv.slice(2)); + const scenarioList = flags.only + ? ALL_SCENARIOS.filter((s) => flags.only.includes(s.id)) + : ALL_SCENARIOS; + if (scenarioList.length === 0) { + throw new Error(`no scenarios matched --only ${flags.only}`); + } + + await ensureTimingBuild({ force: flags.build }); + await startServer(); + + const browser = await chromium.launch({ headless: true }); + const allSamples = []; + let sawAnyTimer = false; + + try { + for (const scenario of scenarioList) { + const total = (scenario.warmup || 0) + (scenario.repeat || 1); + console.log(`\n[scenario] ${scenario.id} (${scenario.warmup || 0} warmup + ${scenario.repeat || 1} measured)`); + for (let t = 0; t < total; t++) { + const isWarmup = t < (scenario.warmup || 0); + const { samples, hadTimers, renderOk } = await runScenarioTrial(browser, scenario, { + throttle: flags.throttle, + trial: t, + }); + sawAnyTimer = sawAnyTimer || hadTimers; + const tag = isWarmup ? "warmup" : "measured"; + console.log(` trial ${t} (${tag}): ${samples.filter((s) => !s.name.startsWith("__")).length} timer samples, renderOk=${renderOk}`); + if (!isWarmup) allSamples.push(...samples); + } + + // Global guard: after the first (fast) scenario, fail loudly if no timers + // were seen — means the served build has timers stripped. + if (scenario === scenarioList[0] && !sawAnyTimer) { + throw new Error( + "No 'Timer …' console lines captured on the first scenario. The served dist/ build likely has timers stripped — rerun with --build." + ); + } + } + } finally { + await browser.close(); + await stopServer(); + } + + const rows = aggregate(allSamples); + const meta = { + date: new Date().toISOString(), + node: process.version, + cpuThrottle: flags.throttle, + scenarioIds: scenarioList.map((s) => s.id), + ...gitInfo(), + }; + + const baseline = loadBaseline(flags.baseline); + const { shaPath, latestPath } = writeBaselineJson(rows, meta, flags.out); + const mdPath = path.join(flags.out, `report-${meta.gitSha}.md`); + const md = writeRankedMarkdown(rows, meta, mdPath, baseline); + + console.log("\n" + md); + console.log(`\n[done] baseline: ${shaPath}`); + console.log(`[done] latest: ${latestPath}`); + console.log(`[done] report: ${mdPath}`); +} + +main().catch((e) => { + console.error("\n[FATAL]", e); + stopServer().finally(() => process.exit(1)); +}); diff --git a/test/profiling/scenarios.mjs b/test/profiling/scenarios.mjs new file mode 100644 index 000000000..a9d58dc04 --- /dev/null +++ b/test/profiling/scenarios.mjs @@ -0,0 +1,126 @@ +/** + * Declarative profiling scenarios. + * + * Design note: most instrumented spans are best measured via URL-driven page + * loads — a load with `?l=radial` runs exactly the same setLayout/mapToScreen + * code as an in-app layout change, and a load with `?c=region` runs the same + * changeColorBy calculation. The one path that only exists in-app (the + * incremental `phylotree.change()` cascade) is captured robustly by the + * `animate` scenario, whose every tick dispatches a date-filter change. DOM-click + * steps (colorby/layout) are included as best-effort extras for true transition + * numbers and degrade gracefully if a selector can't be driven. + * + * Scenario schema: + * { id, dataset, params, steps, repeat, warmup, loadTimeoutMs, settleAfterMs, animateMs } + * Step schema: + * { label, kind: 'colorby'|'layout'|'wait', value?, waitMs? } + */ + +const SPIKE = "/spike-sm"; + +export const scenarios = [ + // --- small/medium loads: fast sanity + the timers-present guard (zika is first) --- + { + id: "zika-load", + dataset: "/zika", + params: {}, + steps: [], + repeat: 5, + warmup: 1, + loadTimeoutMs: 30000, + settleAfterMs: 800, + }, + { + id: "ebola-load", + dataset: "/ebola", + params: { d: "tree,map,entropy" }, + steps: [], + repeat: 5, + warmup: 1, + loadTimeoutMs: 45000, + settleAfterMs: 1000, + }, + + // --- STRESS: 35k-tip spike, all panels — every load-time span --- + { + id: "spike-load-all-panels", + dataset: SPIKE, + params: { d: "tree,map,entropy,frequencies" }, + steps: [], + repeat: 2, + warmup: 1, + loadTimeoutMs: 180000, + settleAfterMs: 2500, // catch debounced updateFrequencyData + map + entropy + }, + + // --- per-layout compute cost on the big tree (tree-only to isolate + cut load) --- + { + id: "spike-load-radial", + dataset: SPIKE, + params: { d: "tree", l: "radial" }, + steps: [], + repeat: 2, + warmup: 1, + loadTimeoutMs: 180000, + settleAfterMs: 1500, + }, + { + id: "spike-load-unrooted", + dataset: SPIKE, + params: { d: "tree", l: "unrooted" }, + steps: [], + repeat: 2, + warmup: 1, + loadTimeoutMs: 180000, + settleAfterMs: 1500, + }, + + // --- incremental cascade + per-tick cost (URL-driven, robust) --- + // animate = start,end,loop,cumulative,speedMs + { + id: "spike-animation", + dataset: SPIKE, + params: { d: "tree", animate: "2020-03-01,2022-06-01,0,0,3000" }, + steps: [], + repeat: 2, + warmup: 0, + loadTimeoutMs: 180000, + settleAfterMs: 500, + animateMs: 10000, // capture animation ticks for this wall-clock window + }, + + // --- best-effort DOM incremental transitions (skipped gracefully on failure) --- + { + id: "spike-colorby", + dataset: SPIKE, + params: { d: "tree,entropy" }, + steps: [ + { label: "colorby-region", kind: "colorby", value: "region" }, + { label: "colorby-country", kind: "colorby", value: "country" }, + ], + repeat: 2, + warmup: 0, + loadTimeoutMs: 180000, + settleAfterMs: 1200, + }, + { + id: "spike-layout", + dataset: SPIKE, + params: { d: "tree" }, + steps: [ + { label: "layout-radial", kind: "layout", value: "radial" }, + { label: "layout-unrooted", kind: "layout", value: "unrooted" }, + { label: "layout-rect", kind: "layout", value: "rectangular" }, + ], + repeat: 2, + warmup: 0, + loadTimeoutMs: 180000, + settleAfterMs: 1200, + }, +]; + +/** Build a dataset URL path + query string from a scenario. */ +export function scenarioUrl(scenario) { + const qs = new URLSearchParams(scenario.params || {}).toString(); + return qs ? `${scenario.dataset}?${qs}` : scenario.dataset; +} diff --git a/test/request_urls.js b/test/request_urls.js deleted file mode 100644 index 8b96b1d0f..000000000 --- a/test/request_urls.js +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint no-unused-expressions: off */ - -/* HOW TO RUN THESE TESTS? -npx mocha -*/ - -const chai = require('chai'); -chai.use(require('chai-http')); - -const expect = chai.expect; - -const isValidURLCallback = (url) => { - return (done) => { - chai.request(url) - .get('') - .end((err, res) => { - expect(err).to.be.null; - expect(res).to.have.status(200); - done(); // Call done to signal callback end - }); - }; -}; - - -it('loads the spash page as expected', isValidURLCallback('http://localhost:4000')); -it('loads /zika', isValidURLCallback('http://localhost:4000/zika')); diff --git a/test/server-fetch.test.js b/test/server-fetch.test.js new file mode 100644 index 000000000..da163d6c3 --- /dev/null +++ b/test/server-fetch.test.js @@ -0,0 +1,89 @@ +/** + * Exercises the CLI's default `/charon/getDataset` handler against real on-disk + * JSONs (provisioned into `test/fetched-jsons/` via `npm run fetch-test-data`). + * + * We stand up a minimal express server using the exact handler factory that + * `auspice view` / `auspice develop` use, listen on an ephemeral port, and make + * real HTTP requests so the streaming response path is exercised end-to-end. + * + * The `mumps` fixtures consist of a v2 dataset (`mumps.json`) plus its + * tip-frequencies sidecar (`mumps_tip-frequencies.json`). The client requests + * sidecars via the same getDataset route with a `type` query param, e.g. + * `/charon/getDataset?prefix=mumps&type=tip-frequencies` + * (see src/actions/loadData.js). + */ + +import express from "express"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { setUpGetDatasetHandler } from "../cli/server/getDataset.ts"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const DATA_DIR = path.resolve(__dirname, "fetched-jsons"); +const MAIN_FIXTURE = path.join(DATA_DIR, "mumps.json"); +const SIDECAR_FIXTURE = path.join(DATA_DIR, "mumps_tip-frequencies.json"); + +const fixturesAvailable = fs.existsSync(MAIN_FIXTURE) && fs.existsSync(SIDECAR_FIXTURE); + +/* The fetched JSONs are gitignored; skip (with a clear message) rather than fail + * if they haven't been provisioned via `npm run fetch-test-data`. */ +const describeOrSkip = fixturesAvailable ? describe : describe.skip; +if (!fixturesAvailable) { + // eslint-disable-next-line no-console + console.error( + `[server-fetch.test] Skipping: fixtures missing in ${DATA_DIR}. ` + + `Provision them with \`npm run fetch-test-data\`.` + ); + process.exit(1) +} + +describeOrSkip("CLI default getDataset handler", () => { + let server; + let baseUrl; + + beforeAll((done) => { + const app = express(); + /* `dataPaths` mirrors the structure produced by `processPathArguments`: + * an object mapping an absolute path to the set of data types to serve from it. */ + const dataPaths = { [DATA_DIR]: new Set(["datasets"]) }; + app.get("/charon/getDataset", setUpGetDatasetHandler(dataPaths)); + server = app.listen(0, () => { + const { port } = server.address(); + baseUrl = `http://127.0.0.1:${port}/charon/getDataset`; + done(); + }); + }); + + afterAll((done) => { + server.close(done); + }); + + it("serves the main (v2) dataset JSON (sanity check)", async () => { + const res = await fetch(`${baseUrl}?prefix=mumps`); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.version).toBe("v2"); + expect(json).toHaveProperty("tree"); + }); + + it("serves the tip-frequencies sidecar JSON", async () => { + const res = await fetch(`${baseUrl}?prefix=mumps&type=tip-frequencies`); + expect(res.status).toBe(200); + const json = await res.json(); + const expected = JSON.parse(fs.readFileSync(SIDECAR_FIXTURE, "utf8")); + expect(json.pivots).toEqual(expected.pivots); + }); + + it("a valid prefix but unknown type is a 400 (Bad Request)", async () => { + const res = await fetch(`${baseUrl}?prefix=mumps&type=foo`); + expect(res.status).toBe(400); + }); + + it("dataset doesn't exist is a 404 (Not Found)", async () => { + const res = await fetch(`${baseUrl}?prefix=foo`); + expect(res.status).toBe(404); + }); + +}); diff --git a/test/smoke-test/urls.test.js b/test/smoke-test/urls.test.js index d2a06abe0..3036ee7ef 100644 --- a/test/smoke-test/urls.test.js +++ b/test/smoke-test/urls.test.js @@ -1,5 +1,5 @@ -const {readFileSync} = require('fs'); -const {expect, test} = require('@playwright/test'); +import {readFileSync} from 'fs'; +import {expect, test} from '@playwright/test'; test.describe.configure({ mode: 'parallel' }); @@ -7,7 +7,7 @@ const testCases = []; // Load a suite of smoke tests from a simple text file. File is of the format: // [path], [comma separated text assertions] -const text = readFileSync(require.resolve('./urls.txt'), 'utf8').trim(); +const text = readFileSync(new URL('./urls.txt', import.meta.url), 'utf8').trim(); for (const line of text.split(/\r?\n/)) { if (line.length === 0) continue; // allow for blank lines. if (line.match(/^#/)) continue; // allow for comments. diff --git a/test/smoke-test/urls.txt b/test/smoke-test/urls.txt index b23726da0..dea68580b 100644 --- a/test/smoke-test/urls.txt +++ b/test/smoke-test/urls.txt @@ -2,9 +2,6 @@ # comma separated list of text assertions (text we expect to find in the # rendered page): -# Following smoke test passed locally, but failed stochastically on GitHub Actions -#/ncov/global, novel coronavirus, Phylogeny, Diversity, Filter by Location - /zika, Zika virus evolution, rectangular /zika?c=region&l=clock&legend=open&m=div&r=region, Zika virus evolution, rectangular diff --git a/tsconfig.json b/tsconfig.json index 23bb52501..7723a7abe 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ Visit https://aka.ms/tsconfig.json for a detailed list of options. /* Modules */ "module": "esnext", /* Specify what module code is generated. Required for dynamic `import()`. */ "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "allowImportingTsExtensions": true, /* Allow .ts extensions in imports (required as cli .ts files are pulled in transitively). */ "types": ["node", "webpack-env", "jest"], /* Specify type package names to be included without being referenced in a source file. */ "resolveJsonModule": true, /* Allow importing .json files. */ @@ -53,5 +54,8 @@ Visit https://aka.ms/tsconfig.json for a detailed list of options. "include": [ "src/**/*", "test/**/*" + ], + "exclude": [ + "cli/**/*" ] } diff --git a/webpack.config.js b/webpack.config.cjs similarity index 98% rename from webpack.config.js rename to webpack.config.cjs index f6a4168c3..3b1329f19 100644 --- a/webpack.config.js +++ b/webpack.config.cjs @@ -3,7 +3,7 @@ const path = require("path"); const webpack = require("webpack"); const CompressionPlugin = require('compression-webpack-plugin'); const fs = require('fs'); -const utils = require('./cli/utils'); +const utils = require('./cli/utils.ts'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const LodashModuleReplacementPlugin = require('lodash-webpack-plugin'); @@ -212,6 +212,7 @@ const generateConfig = ({extensionPath, devMode=false, customOutputPath, analyze resolve: { alias: aliasesToResolve, extensions: ['.ts', '.tsx', '...'], + fullySpecified: false, fallback: { buffer: require.resolve("buffer/"), fs: false @@ -269,6 +270,11 @@ const generateConfig = ({extensionPath, devMode=false, customOutputPath, analyze }, module: { rules: [ + { + test: /\.(ts|js)x?$/, + type: "javascript/auto", + resolve: { fullySpecified: false } + }, { test: /\.(ts|js)x?$/, loader: 'babel-loader',