From 0ec12631e9f841a733de2e6c1c36c308a8ecc451 Mon Sep 17 00:00:00 2001 From: Sem Bauke Date: Wed, 17 Sep 2025 21:11:50 +0200 Subject: [PATCH] test(test): migrate from Mocha to Vitest (#62085) Co-authored-by: Oliver Eyton-Williams --- .gitignore | 1 + curriculum/build-curriculum.js | 34 +- curriculum/file-handler.js | 13 +- curriculum/package.json | 9 +- curriculum/test/daily-challenges.test.js | 69 ++ curriculum/test/test-challenges.js | 658 +++++++----------- .../test/utils/generate-block-tests.mjs | 49 ++ curriculum/test/vitest-global-setup.mjs | 53 ++ curriculum/test/vitest-setup.mjs | 13 + curriculum/test/vitest.config.mjs | 13 + curriculum/utils.js | 10 +- curriculum/vitest.config.mjs | 7 + eslint.config.mjs | 3 +- package.json | 4 +- pnpm-lock.yaml | 370 ++-------- 15 files changed, 565 insertions(+), 741 deletions(-) create mode 100644 curriculum/test/daily-challenges.test.js create mode 100644 curriculum/test/utils/generate-block-tests.mjs create mode 100644 curriculum/test/vitest-global-setup.mjs create mode 100644 curriculum/test/vitest-setup.mjs create mode 100644 curriculum/test/vitest.config.mjs create mode 100644 curriculum/vitest.config.mjs diff --git a/.gitignore b/.gitignore index b600541d6a3..761af5be87d 100644 --- a/.gitignore +++ b/.gitignore @@ -199,6 +199,7 @@ curriculum/curricula.json ### Additional Folders ### curriculum/dist curriculum/build +curriculum/test/blocks-generated ### Playwright ### diff --git a/curriculum/build-curriculum.js b/curriculum/build-curriculum.js index 15dcd7e0e4b..d177803c207 100644 --- a/curriculum/build-curriculum.js +++ b/curriculum/build-curriculum.js @@ -18,7 +18,8 @@ const { getCurriculumStructure, getBlockStructure, getSuperblockStructure, - getBlockStructurePath + getBlockStructurePath, + getBlockStructureDir } = require('./file-handler'); /** @@ -294,14 +295,9 @@ function validateBlocks(superblocks, blockStructureDir) { } } -async function buildCurriculum(lang, filters) { - const contentDir = getContentDir(lang); - const blockStructureDir = getLanguageConfig(lang).blockStructureDir; - const builder = new SuperblockCreator({ - blockCreator: getBlockCreator(lang, !isEmpty(filters)) - }); - +async function parseCurriculumStructure(filters) { const curriculum = getCurriculumStructure(); + const blockStructureDir = getBlockStructureDir(); if (isEmpty(curriculum.superblocks)) throw Error('No superblocks found in curriculum.json'); if (isEmpty(curriculum.certifications)) @@ -314,8 +310,22 @@ async function buildCurriculum(lang, filters) { const superblockList = addBlockStructure( addSuperblockStructure(curriculum.superblocks) ); + return { + fullSuperblockList: applyFilters(superblockList, filters), + certifications: curriculum.certifications + }; +} + +async function buildCurriculum(lang, filters) { + const contentDir = getContentDir(lang); + + const builder = new SuperblockCreator({ + blockCreator: getBlockCreator(lang, !isEmpty(filters)) + }); + + const { fullSuperblockList, certifications } = + await parseCurriculumStructure(filters); - const fullSuperblockList = applyFilters(superblockList, filters); const fullCurriculum = { certifications: { blocks: {} } }; for (const superblock of fullSuperblockList) { @@ -323,7 +333,7 @@ async function buildCurriculum(lang, filters) { await builder.processSuperblock(superblock); } - for (const cert of curriculum.certifications) { + for (const cert of certifications) { const certPath = path.resolve(contentDir, 'certifications', `${cert}.yml`); if (!fs.existsSync(certPath)) { throw Error(`Certification file not found: ${certPath}`); @@ -344,5 +354,7 @@ module.exports = { getSuperblockStructure, createCommentMap, superBlockToFilename, - getSuperblocks + getSuperblocks, + addSuperblockStructure, + parseCurriculumStructure }; diff --git a/curriculum/file-handler.js b/curriculum/file-handler.js index 290adf144aa..1bfbf3f5368 100644 --- a/curriculum/file-handler.js +++ b/curriculum/file-handler.js @@ -94,6 +94,10 @@ function getBlockStructurePath(block) { return path.resolve(BLOCK_STRUCTURE_DIR, `${block}.json`); } +function getBlockStructureDir() { + return BLOCK_STRUCTURE_DIR; +} + function getBlockStructure(block) { return JSON.parse(fs.readFileSync(getBlockStructurePath(block), 'utf8')); } @@ -148,17 +152,15 @@ function getSuperblockStructurePath(superblockFilename) { */ function getLanguageConfig( lang, - { baseDir, i18nBaseDir, structureDir } = { + { baseDir, i18nBaseDir } = { baseDir: CURRICULUM_DIR, - i18nBaseDir: I18N_CURRICULUM_DIR, - structureDir: STRUCTURE_DIR + i18nBaseDir: I18N_CURRICULUM_DIR } ) { const contentDir = path.resolve(baseDir, 'challenges', 'english'); const i18nContentDir = path.resolve(i18nBaseDir, 'challenges', lang); const blockContentDir = path.resolve(contentDir, 'blocks'); const i18nBlockContentDir = path.resolve(i18nContentDir, 'blocks'); - const blockStructureDir = path.resolve(structureDir, 'blocks'); const dictionariesDir = path.resolve(baseDir, 'dictionaries'); const i18nDictionariesDir = path.resolve(i18nBaseDir, 'dictionaries'); @@ -179,7 +181,6 @@ function getLanguageConfig( debug(`Using content directory: ${contentDir}`); debug(`Using i18n content directory: ${i18nContentDir}`); - debug(`Using block content directory: ${blockContentDir}`); debug(`Using i18n block content directory: ${i18nBlockContentDir}`); debug(`Using dictionaries directory: ${dictionariesDir}`); debug(`Using i18n dictionaries directory: ${i18nDictionariesDir}`); @@ -189,7 +190,6 @@ function getLanguageConfig( i18nContentDir, blockContentDir, i18nBlockContentDir, - blockStructureDir, dictionariesDir, i18nDictionariesDir }; @@ -197,6 +197,7 @@ function getLanguageConfig( exports.getContentConfig = getContentConfig; exports.getContentDir = getContentDir; +exports.getBlockStructureDir = getBlockStructureDir; exports.getBlockStructure = getBlockStructure; exports.getBlockStructurePath = getBlockStructurePath; exports.getSuperblockStructure = getSuperblockStructure; diff --git a/curriculum/package.json b/curriculum/package.json index 1b06a27d944..96607e28dd0 100644 --- a/curriculum/package.json +++ b/curriculum/package.json @@ -35,13 +35,14 @@ "reorder-tasks": "CALLING_DIR=$INIT_CWD tsx --tsconfig ../tsconfig.json ../tools/challenge-helper-scripts/reorder-tasks", "update-challenge-order": "CALLING_DIR=$INIT_CWD tsx --tsconfig ../tsconfig.json ../tools/challenge-helper-scripts/update-challenge-order", "update-step-titles": "CALLING_DIR=$INIT_CWD tsx --tsconfig ../tsconfig.json ../tools/challenge-helper-scripts/update-step-titles", - "test": "NODE_OPTIONS='--max-old-space-size=7168' tsx ./node_modules/mocha/bin/mocha.js --delay --exit --reporter progress --bail", - "test:full-output": "NODE_OPTIONS='--max-old-space-size=7168' FULL_OUTPUT=true tsx ./node_modules/mocha/bin/mocha.js --delay --reporter progress" + "test-gen": "node ./test/utils/generate-block-tests.mjs", + "test": "NODE_OPTIONS='--max-old-space-size=7168' pnpm -s test-gen && vitest -c test/vitest.config.mjs", + "test:full-output": "NODE_OPTIONS='--max-old-space-size=7168' pnpm -s test-gen && FULL_OUTPUT=true vitest -c test/vitest.config.mjs --reporter=default" }, "devDependencies": { "@babel/core": "7.23.7", "@babel/register": "7.23.7", - "@compodoc/live-server": "^1.2.3", + "@types/polka": "^0.5.7", "@vitest/ui": "^3.2.4", "chai": "4.4.1", "glob": "8.1.0", @@ -54,8 +55,10 @@ "mocha": "10.3.0", "mock-require": "3.0.3", "ora": "5.4.1", + "polka": "^0.5.2", "puppeteer": "22.12.1", "readdirp": "3.6.0", + "sirv": "^3.0.2", "string-similarity": "4.0.4", "vitest": "^3.2.4" } diff --git a/curriculum/test/daily-challenges.test.js b/curriculum/test/daily-challenges.test.js new file mode 100644 index 00000000000..29468c13b7f --- /dev/null +++ b/curriculum/test/daily-challenges.test.js @@ -0,0 +1,69 @@ +import { assert, describe, it } from 'vitest'; +import { testedLang } from '../utils'; +import { getChallenges } from './test-challenges'; + +// Daily coding challenges are upcoming changes, so this test does nothing +// unless SHOW_UPCOMING_CHANGES is true. +describe('Daily Coding Challenges', async () => { + const lang = testedLang(); + const challenges = await getChallenges(lang, { + superBlock: 'dev-playground' + }); + + const jsDailyChallenges = challenges.filter( + c => c.block === 'daily-coding-challenges-javascript' + ); + + const pyDailyChallenges = challenges.filter( + c => c.block === 'daily-coding-challenges-python' + ); + + it('should have matching number of JavaScript and Python challenges', function () { + assert.equal( + jsDailyChallenges.length, + pyDailyChallenges.length, + `JavaScript challenges: ${jsDailyChallenges.length}, Python challenges: ${pyDailyChallenges.length}` + ); + }); + + for (let i = 0; i < jsDailyChallenges.length; i++) { + describe(`Challenge ${i + 1} Parity`, function () { + const jsChallenge = jsDailyChallenges[i]; + const pyChallenge = pyDailyChallenges[i]; + + it("should have matching ID's", function () { + assert.equal( + jsChallenge.id, + pyChallenge.id, + `Challenge ${i + 1} ID mismatch - JS: ${jsChallenge.id}, Python: ${pyChallenge.id}` + ); + }); + + it(`should have matching titles`, function () { + assert.equal( + jsChallenge.title, + pyChallenge.title, + `Challenge ${i + 1} title mismatch - JS: ${jsChallenge.title}, Python: ${pyChallenge.title}` + ); + }); + + it('should have matching descriptions', function () { + assert.equal( + jsChallenge.description, + pyChallenge.description, + `Challenge ${i + 1} description mismatch` + ); + }); + + it('should have the same number of tests', function () { + const jsTestCount = jsChallenge.tests.length; + const pyTestCount = pyChallenge.tests.length; + assert.equal( + jsTestCount, + pyTestCount, + `Challenge ${i + 1} test count mismatch - JS: ${jsTestCount}, Python: ${pyTestCount}` + ); + }); + }); + } +}); diff --git a/curriculum/test/test-challenges.js b/curriculum/test/test-challenges.js index bafbfdb75d3..8b54a69b5ef 100644 --- a/curriculum/test/test-challenges.js +++ b/curriculum/test/test-challenges.js @@ -1,42 +1,26 @@ -const path = require('path'); -const { assert, AssertionError } = require('chai'); -const jsdom = require('jsdom'); -const liveServer = require('@compodoc/live-server'); -const lodash = require('lodash'); -const Mocha = require('mocha'); -const mockRequire = require('mock-require'); -const spinner = require('ora')(); -const puppeteer = require('puppeteer'); +import { createRequire } from 'node:module'; -// lodash-es can't easily be used in node environments, so we just mock it out -// for the original lodash in testing. -mockRequire('lodash-es', lodash); +import { describe, it, beforeAll } from 'vitest'; +import { assert, AssertionError } from 'chai'; +import jsdom from 'jsdom'; +import lodash from 'lodash'; -const clientPath = path.resolve(__dirname, '../../client'); -require('@babel/register')({ - root: clientPath, - babelrc: false, - presets: ['@babel/preset-env', '@babel/typescript'], - plugins: ['dynamic-import-node'], - ignore: [/node_modules/], - only: [clientPath] -}); -const { +import { buildChallenge, runnerTypes -} = require('../../client/src/templates/Challenges/utils/build'); -const { +} from '../../client/src/templates/Challenges/utils/build'; +import { challengeTypes, hasNoSolution -} = require('../../shared/config/challenge-types'); -const { getLines } = require('../../shared/utils/get-lines'); +} from '../../shared/config/challenge-types'; +import { getLines } from '../../shared/utils/get-lines'; +import { prefixDoctype } from '../../client/src/templates/Challenges/utils/frame'; + +const require = createRequire(import.meta.url); + const { getChallengesForLang } = require('../get-challenges'); const { challengeSchemaValidator } = require('../schema/challenge-schema'); const { testedLang } = require('../utils'); -const { - prefixDoctype, - helperVersion -} = require('../../client/src/templates/Challenges/utils/frame'); const { curriculumSchemaValidator } = require('../schema/curriculum-schema'); const { validateMetaSchema } = require('../schema/meta-schema'); @@ -49,183 +33,79 @@ const { sortChallenges } = require('./utils/sort-challenges'); const { flatten, isEmpty, cloneDeep } = lodash; -const testFilter = { - block: process.env.FCC_BLOCK ? process.env.FCC_BLOCK.trim() : undefined, - challengeId: process.env.FCC_CHALLENGE_ID - ? process.env.FCC_CHALLENGE_ID.trim() - : undefined, - superBlock: process.env.FCC_SUPERBLOCK - ? process.env.FCC_SUPERBLOCK.trim() - : undefined -}; - -// rethrow unhandled rejections to make sure the tests exit with non-zero code -process.on('unhandledRejection', err => handleRejection(err)); -// If an uncaught exception gets here, then mocha is in an unexpected state. All -// we can do is log the exception and exit with a non-zero code. -process.on('uncaughtException', err => { - console.error('Uncaught exception:'); - console.error(err); - process.exit(1); -}); - -// some errors *may* not be reported, since cleanup is triggered by the first -// error and that starts shutting down the browser and the server. -const handleRejection = err => { - console.error('Unhandled rejection:'); - // setting the error code because node does not (yet) exit with a non-zero - // code on unhandled exceptions. - process.exitCode = 1; - cleanup(); - console.error(err); - if (process.env.FULL_OUTPUT !== 'true') process.exit(); -}; - const dom = new jsdom.JSDOM(''); global.document = dom.window.document; global.DOMParser = dom.window.DOMParser; -const oldRunnerFail = Mocha.Runner.prototype.fail; -Mocha.Runner.prototype.fail = function (test, err) { - if (err instanceof AssertionError) { - const errMessage = String(err.message || ''); - const assertIndex = errMessage.indexOf(': expected'); - if (assertIndex !== -1) { - err.message = errMessage.slice(0, assertIndex); - } - // Don't show stacktrace for assertion errors. - if (err.stack) { - delete err.stack; - } - } - return oldRunnerFail.call(this, test, err); -}; +// The puppeteer page is global so that we can recreate it during tests, if +// needed. +let page; -async function newPageContext(browser) { - const page = await browser.newPage(); - // it's needed for workers as context. - await page.goto('http://127.0.0.1:8080/index.html'); +const poolId = process.env.VITEST_POOL_ID; + +async function createAndVisitNewPage() { + const page = await globalThis.puppeteerBrowserContext[poolId].newPage(); + await page.goto(`http://127.0.0.1:8080/index.html`); return page; } -spinner.start(); -spinner.text = 'Populate tests.'; +async function newPageContext() { + // Reuse a single page per worker/pool to avoid the overhead of creating a + // new page for every block file. + globalThis.__fccPuppeteerPages ??= {}; + globalThis.__fccPuppeteerPages[poolId] ??= globalThis.__fccPuppeteerPages[ + poolId + ] = createAndVisitNewPage(); -let browser; -let page; - -setup() - .then(runTests) - .catch(err => handleRejection(err)); - -async function setup() { - // liveServer starts synchronously - liveServer.start({ - host: '127.0.0.1', - port: '8080', - root: path.resolve(__dirname, 'stubs'), - mount: [ - [ - '/dist', - path.join(clientPath, `static/js/test-runner/${helperVersion}`) - ], - ['/js', path.join(clientPath, 'static/js')] - ], - open: false, - logLevel: 0 - }); - browser = await puppeteer.launch({ - args: [ - // Required for Docker version of Puppeteer - '--no-sandbox', - '--disable-setuid-sandbox', - // This will write shared memory files into /tmp instead of /dev/shm, - // because Docker’s default for /dev/shm is 64MB - '--disable-dev-shm-usage' - // dumpio: true - ], - headless: 'new' - }); - global.Worker = createPseudoWorker(await newPageContext(browser)); - - page = await newPageContext(browser); - await page.setViewport({ width: 300, height: 150 }); + return globalThis.__fccPuppeteerPages[poolId]; +} +export async function defineTestsForBlock({ block }) { const lang = testedLang(); - const challenges = await getChallenges(lang, testFilter); + const challenges = await getChallenges(lang, { block }); const nonCertificationChallenges = challenges.filter( ({ challengeType }) => challengeType !== 7 ); - if (isEmpty(nonCertificationChallenges)) { - throw Error( - `No challenges to test when using filter ${JSON.stringify(testFilter)} -If the challenge file exists, try running 'build:curriculum' for more information. - ` - ); + console.warn(`No non-certification challenges to test for block ${block}.`); + describe('Check challenges', () => { + it('No non-certification challenges to test', () => {}); + }); + return; } - - // the next few statements create a list of all blocks and superblocks - // as they appear in the list of challenges - const superBlocks = challenges.map(({ superBlock }) => superBlock); - const targetSuperBlockStrings = [ - ...new Set(superBlocks.filter(el => Boolean(el))) - ]; - const meta = {}; for (const challenge of challenges) { const dashedBlockName = challenge.block; - // certifications do not have dashedBlockName's and don't have metas so - // we can skip them. - // TODO: omit certifications from the list of challenges if (dashedBlockName && !meta[dashedBlockName]) { meta[dashedBlockName] = getBlockStructure(dashedBlockName); const result = validateMetaSchema(meta[dashedBlockName]); - - if (result.error) { - throw new AssertionError(result.error); - } + if (result.error) throw new AssertionError(result.error); } } - return { - meta, - challenges, - lang, - superBlocks: targetSuperBlockStrings - }; -} -// cleanup calls some async functions, but it's the last thing that happens, so -// no need to await anything. -function cleanup() { - if (browser) { - browser.close(); - } - liveServer.shutdown(); - spinner.stop(); -} + const challengeData = { meta, challenges, lang }; -function runTests(challengeData) { - describe('Check challenges', function () { - after(function () { - cleanup(); + describe('Check challenges', () => { + beforeAll(async () => { + page = await newPageContext(); + global.Worker = createPseudoWorker(page); }); - populateTestsForLang(challengeData); + + populateTestsForLang(challengeData, () => page); }); - spinner.text = 'Testing'; - run(); } -async function getChallenges(lang, filters) { +export async function getChallenges(lang, filters) { const challenges = await getChallengesForLang(lang, filters).then( curriculum => { - const result = curriculumSchemaValidator(curriculum); // If there are filters, we're testing a single challenge or block, so we // can skip the validation. - if (result.error && isEmpty(filters)) { - throw new Error( - `Curriculum validation failed: ${result.error.message}` - ); + if (isEmpty(filters)) { + const result = curriculumSchemaValidator(curriculum); + if (result.error) + throw new Error( + `Curriculum validation failed: ${result.error.message}` + ); } return Object.keys(curriculum) .map(key => curriculum[key].blocks) @@ -242,213 +122,143 @@ async function getChallenges(lang, filters) { return sortChallenges(challenges); } -function populateTestsForLang({ lang, challenges, meta, superBlocks }) { +function populateTestsForLang({ lang, challenges, meta }) { const validateChallenge = challengeSchemaValidator(); - superBlocks.forEach(superBlock => { - describe(`Language: ${lang}`, function () { - describe(`SuperBlock: ${superBlock}`, function () { - this.timeout(5000); - const superBlockChallenges = challenges.filter( - c => c.superBlock === superBlock - ); + describe(`Language: ${lang}`, function () { + const challengeTitles = new ChallengeTitles(); + const mongoIds = new MongoIds(); - // daily challenge tests - if (superBlock === 'dev-playground') { - describe('Daily Coding Challenges', function () { - const jsDailyChallenges = superBlockChallenges.filter( - c => c.block === 'daily-coding-challenges-javascript' - ); + challenges.forEach((challenge, id) => { + // When testing single challenge, in project based curriculum, + // challenge to test (current challenge) might not have solution. + // Instead seed from next challenge is tested against tests from + // current challenge. Next challenge is skipped from testing. + if (process.env.FCC_CHALLENGE_ID && id > 0) return; - const pyDailyChallenges = superBlockChallenges.filter( - c => c.block === 'daily-coding-challenges-python' - ); + const dashedBlockName = challenge.block; + // TODO: once certifications are not included in the list of challenges, + // stop returning early here. + if (typeof dashedBlockName === 'undefined') return; + describe(`Block: ${challenge.block}`, function () { + describe(`Title: ${challenge.title}`, function () { + describe(`ID: ${challenge.id}`, function () { + // Note: the title in meta.json are purely for human readability and + // do not include translations, so we do not validate against them. + it('Matches an ID in meta.json', function () { + const index = meta[dashedBlockName]?.challengeOrder?.findIndex( + ({ id }) => id === challenge.id + ); - it('should have matching number of JavaScript and Python challenges', function () { - assert.equal( - jsDailyChallenges.length, - pyDailyChallenges.length, - `JavaScript challenges: ${jsDailyChallenges.length}, Python challenges: ${pyDailyChallenges.length}` + if (index < 0) { + throw new AssertionError( + `Cannot find ID "${challenge.id}" in meta.json file for block "${dashedBlockName}"` + ); + } + }); + + it('Common checks', function () { + const result = validateChallenge(challenge); + + if (result.error) { + throw new AssertionError(result.error); + } + const { id, block, dashedName } = challenge; + assert.exists( + dashedName, + `Missing dashedName for challenge ${id} in ${block}.` + ); + const pathAndTitle = `${block}/${dashedName}`; + const idVerificationMessage = mongoIds.check(id, block); + assert.isNull(idVerificationMessage, idVerificationMessage); + const dupeTitleCheck = challengeTitles.check(dashedName, block); + assert.isTrue( + dupeTitleCheck, + `All challenges within a block must have a unique dashed name. ${dashedName} (at ${pathAndTitle}) is already assigned` ); }); - for (let i = 0; i < jsDailyChallenges.length; i++) { - describe(`Challenge ${i + 1} Parity`, function () { - const jsChallenge = jsDailyChallenges[i]; - const pyChallenge = pyDailyChallenges[i]; + const { challengeType } = challenge; - it("should have matching ID's", function () { - assert.equal( - jsChallenge.id, - pyChallenge.id, - `Challenge ${i + 1} ID mismatch - JS: ${jsChallenge.id}, Python: ${pyChallenge.id}` - ); - }); + if (hasNoSolution(challengeType)) return; - it(`should have matching titles`, function () { - assert.equal( - jsChallenge.title, - pyChallenge.title, - `Challenge ${i + 1} title mismatch - JS: ${jsChallenge.title}, Python: ${pyChallenge.title}` - ); - }); - - it('should have matching descriptions', function () { - assert.equal( - jsChallenge.description, - pyChallenge.description, - `Challenge ${i + 1} description mismatch` - ); - }); - - it('should have the same number of tests', function () { - const jsTestCount = jsChallenge.tests.length; - const pyTestCount = pyChallenge.tests.length; - assert.equal( - jsTestCount, - pyTestCount, - `Challenge ${i + 1} test count mismatch - JS: ${jsTestCount}, Python: ${pyTestCount}` - ); - }); - }); + let { tests = [] } = challenge; + tests = tests.filter(test => !!test.testString); + if (tests.length === 0) { + it('Check tests. No tests.', () => {}); + return; } - }); - } - const challengeTitles = new ChallengeTitles(); - const mongoIds = new MongoIds(); + if (challengeType === challengeTypes.backend) { + it('Check tests is not implemented.', () => {}); + return; + } - superBlockChallenges.forEach((challenge, id) => { - // When testing single challenge, in project based curriculum, - // challenge to test (current challenge) might not have solution. - // Instead seed from next challenge is tested against tests from - // current challenge. Next challenge is skipped from testing. - if (process.env.FCC_CHALLENGE_ID && id > 0) return; - - const dashedBlockName = challenge.block; - // TODO: once certifications are not included in the list of challenges, - // stop returning early here. - if (typeof dashedBlockName === 'undefined') return; - describe(`Block: ${challenge.block}`, function () { - describe(`Title: ${challenge.title}`, function () { - describe(`ID: ${challenge.id}`, function () { - // Note: the title in meta.json are purely for human readability and - // do not include translations, so we do not validate against them. - it('Matches an ID in meta.json', function () { - const index = meta[ - dashedBlockName - ]?.challengeOrder?.findIndex(({ id }) => id === challenge.id); - - if (index < 0) { - throw new AssertionError( - `Cannot find ID "${challenge.id}" in meta.json file for block "${dashedBlockName}"` - ); - } - }); - - it('Common checks', function () { - const result = validateChallenge(challenge); - - if (result.error) { - throw new AssertionError(result.error); - } - const { id, block, dashedName } = challenge; - assert.exists( - dashedName, - `Missing dashedName for challenge ${id} in ${block}.` + // The python tests are (currently) slow, so we give them more time. + const timePerTest = + challengeType === challengeTypes.python ? 10000 : 5000; + it( + 'Test suite must fail on the initial contents', + async function () { + // suppress errors in the console. + const oldConsoleError = console.error; + console.error = () => {}; + let fails = false; + let testRunner; + // TODO: if this times out, try wrapping this whole test in a + // new describe block and create the runner in a beforeAll (i.e. + // same as "Check tests against solutions") + try { + testRunner = await createTestRunner( + challenge, + challenge.challengeFiles, + buildChallenge ); - const pathAndTitle = `${block}/${dashedName}`; - const idVerificationMessage = mongoIds.check(id, block); - assert.isNull(idVerificationMessage, idVerificationMessage); - const dupeTitleCheck = challengeTitles.check( - dashedName, - block + } catch (e) { + console.error( + `Error creating test runner for initial contents` ); - assert.isTrue( - dupeTitleCheck, - `All challenges within a block must have a unique dashed name. ${dashedName} (at ${pathAndTitle}) is already assigned` - ); - }); - - const { challengeType } = challenge; - - if (hasNoSolution(challengeType)) return; - - let { tests = [] } = challenge; - tests = tests.filter(test => !!test.testString); - if (tests.length === 0) { - it('Check tests. No tests.'); - return; + console.error(e); + fails = true; } - - if (challengeType === challengeTypes.backend) { - it('Check tests is not implemented.'); - return; - } - - // The python tests are (currently) slow, so we give them more time. - const timePerTest = - challengeType === challengeTypes.python ? 10000 : 5000; - it('Test suite must fail on the initial contents', async function () { - // TODO: some tests take a surprisingly long time to setup the - // test runner, so this timeout is large while we investigate. - this.timeout(timePerTest * tests.length + 20000); - // suppress errors in the console. - const oldConsoleError = console.error; - console.error = () => {}; - let fails = false; - let testRunner; + if (!fails) { try { - testRunner = await createTestRunner( - challenge, - challenge.challengeFiles, - buildChallenge - ); - } catch (e) { - console.error( - `Error creating test runner for initial contents` - ); - console.error(e); + await testRunner(tests); + } catch { fails = true; } - if (!fails) { - try { - await testRunner(tests); - } catch { - fails = true; - } - } - console.error = oldConsoleError; - assert( - fails, - 'Test suite does not fail on the initial contents' - ); - }); + } + console.error = oldConsoleError; + assert( + fails, + 'Test suite does not fail on the initial contents' + ); + }, + timePerTest * tests.length + 20000 + ); - let { solutions = [] } = challenge; + let { solutions = [] } = challenge; - // if there's an empty string as solution, this is likely a mistake - // TODO: what does this look like now? (this being detection of empty - // lines in solutions - rather than entirely missing solutions) + // if there's an empty string as solution, this is likely a mistake + // TODO: what does this look like now? (this being detection of empty + // lines in solutions - rather than entirely missing solutions) - // We need to track where the solution came from to give better - // feedback if the solution is failing. - let solutionFromNext = false; + // We need to track where the solution came from to give better + // feedback if the solution is failing. + let solutionFromNext = false; - if (isEmpty(solutions)) { - // if there are no solutions in the challenge, it's assumed the next - // challenge's seed will be a solution to the current challenge. - // This is expected to happen in the project based curriculum. + if (isEmpty(solutions)) { + // if there are no solutions in the challenge, it's assumed the next + // challenge's seed will be a solution to the current challenge. + // This is expected to happen in the project based curriculum. - const nextChallenge = superBlockChallenges[id + 1]; + const nextChallenge = challenges[id + 1]; - if (nextChallenge) { - const solutionFiles = cloneDeep( - nextChallenge.challengeFiles - ); - if (!solutionFiles) { - throw Error( - `No solution found. + if (nextChallenge) { + const solutionFiles = cloneDeep(nextChallenge.challengeFiles); + if (!solutionFiles) { + throw Error( + `No solution found. Check the next challenge (${nextChallenge.title}): it should have a seed which solves the current challenge. For example: @@ -460,61 +270,65 @@ For example: seed goes here \`\`\` ` - ); - } - const solutionFilesWithEditableContents = solutionFiles.map( - file => ({ - ...file, - editableContents: getLines( - file.contents, - file.editableRegionBoundaries - ) - }) - ); - // Since there is only one seed, there can only be one solution, - // but the tests assume solutions is an array. - solutions = [solutionFilesWithEditableContents]; - solutionFromNext = true; - } else { - throw Error( - `solution omitted for ${challenge.superBlock} ${challenge.block} ${challenge.title}` - ); - } + ); } - - // TODO: the no-solution filtering is a little convoluted: - const noSolution = new RegExp('// solution required'); - - const filteredSolutions = solutions.filter(solution => { - return !isEmpty( - solution.filter( - challengeFile => !noSolution.test(challengeFile.contents) + const solutionFilesWithEditableContents = solutionFiles.map( + file => ({ + ...file, + editableContents: getLines( + file.contents, + file.editableRegionBoundaries ) + }) + ); + // Since there is only one seed, there can only be one solution, + // but the tests assume solutions is an array. + solutions = [solutionFilesWithEditableContents]; + solutionFromNext = true; + } else { + throw Error( + `solution omitted for ${challenge.superBlock} ${challenge.block} ${challenge.title}` + ); + } + } + + // TODO: the no-solution filtering is a little convoluted: + const noSolution = new RegExp('// solution required'); + + const filteredSolutions = solutions.filter(solution => { + return !isEmpty( + solution.filter( + challengeFile => !noSolution.test(challengeFile.contents) + ) + ); + }); + + if (isEmpty(filteredSolutions)) { + it('Check tests. No solutions', () => {}); + return; + } + + describe('Check tests against solutions', function () { + solutions.forEach((solution, index) => { + let testRunner; + // Creating the test runner can be slow, so we do it in + // beforeAll rather than the test itself. + beforeAll(async () => { + testRunner = await createTestRunner( + challenge, + solution, + buildChallenge, + solutionFromNext ); }); - if (isEmpty(filteredSolutions)) { - it('Check tests. No solutions'); - return; - } - - describe('Check tests against solutions', function () { - solutions.forEach((solution, index) => { - it(`Solution ${ - index + 1 - } must pass the tests`, async function () { - this.timeout(timePerTest * tests.length + 2000); - const testRunner = await createTestRunner( - challenge, - solution, - buildChallenge, - solutionFromNext - ); - - await testRunner(tests); - }); - }); - }); + it( + `Solution ${index + 1} must pass the tests`, + async function () { + await testRunner(tests); + }, + timePerTest * tests.length + 2000 + ); }); }); }); @@ -543,6 +357,8 @@ async function createTestRunner( { usesTestRunner: true } ); + if (!page) throw new Error('Browser page is not ready yet'); + const evaluator = await getContextEvaluator({ // passing in challengeId so it's easier to debug timeouts challengeId: challenge.id, @@ -617,7 +433,7 @@ async function getContextEvaluator(config) { }; } -async function initializeTestRunner({ +async function _initializeTestRunner({ build, sources, type, @@ -625,7 +441,6 @@ async function initializeTestRunner({ loadEnzyme }) { const source = type === 'dom' ? prefixDoctype({ build, sources }) : build; - await page.evaluate( async (sources, source, type, hooks, loadEnzyme) => { await window.FCCTestRunner.createTestRunner({ @@ -643,3 +458,32 @@ async function initializeTestRunner({ loadEnzyme ); } + +async function initializeTestRunner({ + build, + sources, + type, + hooks, + loadEnzyme +}) { + // Ensure FCCTestRunner is available before creating it + await page.waitForFunction( + 'window.FCCTestRunner && window.FCCTestRunner.createTestRunner', + { timeout: 5000 } + ); + + try { + await _initializeTestRunner({ build, sources, type, hooks, loadEnzyme }); + } catch (e) { + // It's not clear why, but sometimes the iframe load times out. It seems to + // be an issue with Puppeteer, so we give it one more try to reduce test + // flakiness. + if (e.message.includes('Timed out waiting for the test frame to load')) { + console.warn('Test frame load timed out. Retrying...'); + page = await createAndVisitNewPage(); + await _initializeTestRunner({ build, sources, type, hooks, loadEnzyme }); + } else { + throw e; + } + } +} diff --git a/curriculum/test/utils/generate-block-tests.mjs b/curriculum/test/utils/generate-block-tests.mjs new file mode 100644 index 00000000000..c92bfd451ab --- /dev/null +++ b/curriculum/test/utils/generate-block-tests.mjs @@ -0,0 +1,49 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import _ from 'lodash'; + +import { parseCurriculumStructure } from '../../build-curriculum.js'; + +const __dirname = import.meta.dirname; + +const testFilter = { + block: process.env.FCC_BLOCK ? process.env.FCC_BLOCK.trim() : undefined, + challengeId: process.env.FCC_CHALLENGE_ID + ? process.env.FCC_CHALLENGE_ID.trim() + : undefined, + superBlock: process.env.FCC_SUPERBLOCK + ? process.env.FCC_SUPERBLOCK.trim() + : undefined +}; + +const GENERATED_DIR = path.resolve(__dirname, '../blocks-generated'); + +async function main() { + // clean and recreate directory + await fs.promises.rm(GENERATED_DIR, { force: true, recursive: true }); + await fs.promises.mkdir(GENERATED_DIR, { recursive: true }); + + const { fullSuperblockList } = await parseCurriculumStructure(testFilter); + + const blocks = _.uniq( + fullSuperblockList.flatMap(({ blocks }) => blocks).map(b => b.dashedName) + ); + + for (const block of blocks) { + const filePath = path.join(GENERATED_DIR, `${block}.test.js`); + const contents = generateSingleBlockFile({ block }); + await fs.promises.writeFile(filePath, contents, 'utf8'); + } + + console.log(`Generated ${blocks.length} block test file(s).`); +} + +function generateSingleBlockFile({ block }) { + return `import { defineTestsForBlock } from '../test-challenges.js'; + +await defineTestsForBlock({ block: ${JSON.stringify(block)} }); +`; +} + +main(); diff --git a/curriculum/test/vitest-global-setup.mjs b/curriculum/test/vitest-global-setup.mjs new file mode 100644 index 00000000000..c461f9ffac9 --- /dev/null +++ b/curriculum/test/vitest-global-setup.mjs @@ -0,0 +1,53 @@ +import path from 'node:path'; + +import sirv from 'sirv'; +import polka from 'polka'; +import puppeteer from 'puppeteer'; + +import { helperVersion } from '../../client/src/templates/Challenges/utils/frame'; + +const clientPath = path.resolve(__dirname, '../../client'); + +async function createBrowser() { + return puppeteer.launch({ + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage' + ], + headless: 'new' + }); +} + +let browser, server; + +async function startServer() { + const host = '127.0.0.1'; + const port = 8080; + + const app = polka(); + + // Mount static files used by the tests + app.use( + '/dist', + sirv(path.join(clientPath, `static/js/test-runner/${helperVersion}`)) + ); + app.use('/js', sirv(path.join(clientPath, 'static/js'))); + app.use('/', sirv(path.resolve(__dirname, 'stubs'))); + app.listen(port, host); + return app.server; +} + +export async function setup() { + server = await startServer(); + browser = await createBrowser(); + // Sharing the Websocket endpoint so that setup files can connect. This allows + // us to do as much work as possible once in the global setup while allowing + // each test pool to maintain its own connection. + process.env.PUPPETEER_WS_ENDPOINT = browser.wsEndpoint(); +} + +export async function teardown() { + await browser.close(); + await server.close(); +} diff --git a/curriculum/test/vitest-setup.mjs b/curriculum/test/vitest-setup.mjs new file mode 100644 index 00000000000..cb97fd27c09 --- /dev/null +++ b/curriculum/test/vitest-setup.mjs @@ -0,0 +1,13 @@ +// connect to the puppeteer browser instance and create a new page context +// each VITEST_POOL_ID will have its own context + +import puppeteer from 'puppeteer'; + +if (!globalThis.puppeteerBrowserContext?.[process.env.VITEST_POOL_ID]) { + globalThis.puppeteerBrowserContext ??= {}; + const browser = await puppeteer.connect({ + browserWSEndpoint: process.env.PUPPETEER_WS_ENDPOINT + }); + globalThis.puppeteerBrowserContext[process.env.VITEST_POOL_ID] = + await browser.createBrowserContext(); +} diff --git a/curriculum/test/vitest.config.mjs b/curriculum/test/vitest.config.mjs new file mode 100644 index 00000000000..f689fdd2130 --- /dev/null +++ b/curriculum/test/vitest.config.mjs @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['test/blocks-generated/**/*.test.js'], + environment: 'node', + hookTimeout: 60000, + testTimeout: 30000, + isolate: false, + globalSetup: 'test/vitest-global-setup.mjs', + setupFiles: 'test/vitest-setup.mjs' + } +}); diff --git a/curriculum/utils.js b/curriculum/utils.js index 2926102ccfd..0e8fabe1de9 100644 --- a/curriculum/utils.js +++ b/curriculum/utils.js @@ -58,23 +58,25 @@ function getSuperOrder(superblock) { } /** - * Filters the superblocks array to only include blocks with the specified dashedName (block). + * Filters the superblocks array to include, at most, a single superblock with the specified block. * If no block is provided, returns the original superblocks array. * * @param {Array} superblocks - Array of superblock objects, each containing a blocks array. * @param {Object} [options] - Options object * @param {string} [options.block] - The dashedName of the block to filter for (in kebab case). - * @returns {Array} Filtered array of superblocks containing only the specified block, or the original array if block is not provided. + * @returns {Array} Array with one superblock containing the specified block, or the original array if block is not provided. */ function filterByBlock(superblocks, { block } = {}) { if (!block) return superblocks; - return superblocks + const superblock = superblocks .map(superblock => ({ ...superblock, blocks: superblock.blocks.filter(({ dashedName }) => dashedName === block) })) - .filter(superblock => superblock.blocks.length > 0); + .find(superblock => superblock.blocks.length > 0); + + return superblock ? [superblock] : []; } /** diff --git a/curriculum/vitest.config.mjs b/curriculum/vitest.config.mjs new file mode 100644 index 00000000000..b270042c180 --- /dev/null +++ b/curriculum/vitest.config.mjs @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + exclude: ['test/blocks-generated/**/*.test.js'] + } +}); diff --git a/eslint.config.mjs b/eslint.config.mjs index f865418afc5..51863ab63b9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -67,7 +67,8 @@ export default tseslint.config( $: true, ga: true, jQuery: true, - router: true + router: true, + globalThis: true }, parser: babelParser, diff --git a/package.json b/package.json index 0059dfd1133..4a03191426c 100644 --- a/package.json +++ b/package.json @@ -75,9 +75,9 @@ "test:tools:challenge-helper-scripts": "cd ./tools/challenge-helper-scripts && pnpm test run", "test:tools:scripts-build": "cd ./tools/scripts/build && pnpm test run", "test:tools:challenge-parser": "cd ./tools/challenge-parser && pnpm test run", - "test:curriculum:content": "cd ./curriculum && pnpm test", + "test:curriculum:content": "cd ./curriculum && pnpm test run", "test:curriculum:tooling": "cd ./curriculum && pnpm vitest run", - "test-curriculum-full-output": "cd ./curriculum && pnpm run test:full-output", + "test-curriculum-full-output": "cd ./curriculum && pnpm run test:full-output run", "test:client": "cd ./client && pnpm test run", "test-config": "jest config", "test-tools": "jest tools", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 980ff2ab234..f98b84bf62c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -715,9 +715,9 @@ importers: '@babel/register': specifier: 7.23.7 version: 7.23.7(@babel/core@7.23.7) - '@compodoc/live-server': - specifier: ^1.2.3 - version: 1.2.3 + '@types/polka': + specifier: ^0.5.7 + version: 0.5.7 '@vitest/ui': specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) @@ -754,12 +754,18 @@ importers: ora: specifier: 5.4.1 version: 5.4.1 + polka: + specifier: ^0.5.2 + version: 0.5.2 puppeteer: specifier: 22.12.1 version: 22.12.1(typescript@5.8.2) readdirp: specifier: 3.6.0 version: 3.6.0 + sirv: + specifier: ^3.0.2 + version: 3.0.2 string-similarity: specifier: 4.0.4 version: 4.0.4 @@ -1171,6 +1177,10 @@ packages: resolution: {integrity: sha512-vyrkEHG1jrukmzTPtyWB4NLPauUw5bQeg4uhn8f+1SSynmrOcyvlb1GKQjjgoBzElLdfXCRYX8UnBlhklOHYRQ==} engines: {node: '>=8'} + '@arr/every@1.0.1': + resolution: {integrity: sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==} + engines: {node: '>=4'} + '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -2280,11 +2290,6 @@ packages: '@bundled-es-modules/tough-cookie@0.1.6': resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} - '@compodoc/live-server@1.2.3': - resolution: {integrity: sha512-hDmntVCyjjaxuJzPzBx68orNZ7TW4BtHWMnXlIVn5dqhK7vuFF/11hspO1cMmc+2QTYgqde1TBcb3127S7Zrow==} - engines: {node: '>=0.10.0'} - hasBin: true - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -3640,8 +3645,8 @@ packages: webpack-plugin-serve: optional: true - '@polka/url@1.0.0-next.23': - resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==} + '@polka/url@0.5.0': + resolution: {integrity: sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw==} '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -4705,6 +4710,9 @@ packages: resolution: {integrity: sha512-wKoab31pknvILkxAF8ss+v9iNyhw5Iu/0jLtRkUD74cNfOOLJNnqfFKAv0r7wVaTQxRZtWrMpGfShwwBjOcgcg==} deprecated: This is a stub types definition. pino provides its own type definitions, so you do not need this installed. + '@types/polka@0.5.7': + resolution: {integrity: sha512-TH8CDXM8zoskPCNmWabtK7ziGv9Q21s4hMZLVYK5HFEfqmGXBqq/Wgi7jNELWXftZK/1J/9CezYa06x1RKeQ+g==} + '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} @@ -4824,6 +4832,9 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trouter@3.1.4': + resolution: {integrity: sha512-4YIL/2AvvZqKBWenjvEpxpblT2KGO6793ipr5QS7/6DpQ3O3SwZGgNGWezxf3pzeYZc24a2pJIrR/+Jxh/wYNQ==} + '@types/unist@2.0.8': resolution: {integrity: sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw==} @@ -5495,14 +5506,6 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - apache-crypt@1.2.6: - resolution: {integrity: sha512-072WetlM4blL8PREJVeY+WHiUh1R5VNt2HfceGS8aKqttPHcmqE5pkKuXPz/ULmJOFkc8Hw3kfKl6vy7Qka6DA==} - engines: {node: '>=8'} - - apache-md5@1.1.8: - resolution: {integrity: sha512-FCAJojipPn0bXjuEpjOOOMN8FZDkxfWWp4JGN9mifU2IhxvKyXZYqpzPHdnTSUpmPDy+tsslB6Z1g+Vg6nVbYA==} - engines: {node: '>=8'} - append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} @@ -5917,12 +5920,6 @@ packages: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} - batch@0.6.1: - resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} - - bcryptjs@2.4.3: - resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} - better-opn@2.1.1: resolution: {integrity: sha512-kIPXZS5qwyKiX/HcRvDYfmBQUa8XP17I0mYZZ0y4UhpYOSvtsLHDYqmomS+Mj20aDvD3knEiQ0ecQy2nhio3yA==} engines: {node: '>8.0.0'} @@ -6406,10 +6403,6 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - colors@1.4.0: - resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} - engines: {node: '>=0.1.90'} - combine-source-map@0.8.0: resolution: {integrity: sha512-UlxQ9Vw0b/Bt/KYwCFqdEwsQ1eL8d1gibiFb7lxQJFdvTgc2hIZi6ugsg+kyhzhPV+QEpUiEIwInIAIrgoEkrg==} @@ -6478,10 +6471,6 @@ packages: confusing-browser-globals@1.0.11: resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} - connect@3.7.0: - resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} - engines: {node: '>= 0.10.0'} - console-browserify@1.2.0: resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} @@ -6916,10 +6905,6 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -7190,10 +7175,6 @@ packages: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -7668,9 +7649,6 @@ packages: event-source-polyfill@1.0.31: resolution: {integrity: sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==} - event-stream@4.0.1: - resolution: {integrity: sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==} - event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -7851,10 +7829,6 @@ packages: fault@1.0.4: resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} - faye-websocket@0.11.4: - resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} - engines: {node: '>=0.8.0'} - fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -7921,10 +7895,6 @@ packages: resolution: {integrity: sha512-TL48Pi1oNHeMOHrKv1bCJUrWZDcD3DIG6AGYVNOnyZPr7Bd/pStN0pL+lfzF5BNoj/FclaoiaLenk4XUIFVYng==} engines: {node: '>=8'} - finalhandler@1.1.2: - resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} - engines: {node: '>= 0.8'} - finalhandler@1.2.0: resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} engines: {node: '>= 0.8'} @@ -8068,13 +8038,6 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - - from@0.1.7: - resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} - fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -8715,21 +8678,9 @@ packages: htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - http-auth-connect@1.0.6: - resolution: {integrity: sha512-yaO0QSCPqGCjPrl3qEEHjJP+lwZ6gMpXLuCBE06eWwcXomkI5TARtu0kxf9teFuBj6iaV3Ybr15jaWUvbzNzHw==} - engines: {node: '>=8'} - - http-auth@4.1.9: - resolution: {integrity: sha512-kvPYxNGc9EKGTXvOMnTBQw2RZfuiSihK/mLw/a4pbtRueTE45S55Lw/3k5CktIf7Ak0veMKEIteDj4YkNmCzmQ==} - engines: {node: '>=8'} - http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} - http-errors@1.6.3: - resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} - engines: {node: '>= 0.6'} - http-errors@1.8.0: resolution: {integrity: sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==} engines: {node: '>= 0.6'} @@ -8738,9 +8689,6 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} - http-parser-js@0.5.8: - resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} - http-proxy-agent@4.0.1: resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} engines: {node: '>= 6'} @@ -10076,9 +10024,6 @@ packages: resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} engines: {node: '>=0.10.0'} - map-stream@0.0.7: - resolution: {integrity: sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==} - map-visit@1.0.0: resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} engines: {node: '>=0.10.0'} @@ -10101,6 +10046,10 @@ packages: resolution: {integrity: sha512-4lbtT14A3m0LPX1WS/3d1m7Blg+ZwiLq36WvjQqFGsX3Gik99NV+VXp/PW3n+Q62xyPdbvGOCfjPqjW+/SKMig==} engines: {node: '>=18'} + matchit@1.1.0: + resolution: {integrity: sha512-+nGYoOlfHmxe5BW5tE0EMJppXEwdSf8uBA1GTZC7Q77kbT35+VKLYJMzVNWCHSsga1ps1tPYFtFyvxvKzWVmMA==} + engines: {node: '>=6'} + matchmediaquery@0.3.1: resolution: {integrity: sha512-Hlk20WQHRIm9EE9luN1kjRjYXAQToHOIAHPJn9buxBwuhfTHoKUcX+lXBbxc85DVQfXYbEQ4HcwQdd128E3qHQ==} @@ -10483,10 +10432,6 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} - engines: {node: '>= 0.6'} - mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -10647,10 +10592,6 @@ packages: socks: optional: true - morgan@1.10.0: - resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} - engines: {node: '>= 0.8.0'} - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -10951,10 +10892,6 @@ packages: on-exit-leak-free@2.1.0: resolution: {integrity: sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==} - on-finished@2.3.0: - resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} - engines: {node: '>= 0.8'} - on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -10978,10 +10915,6 @@ packages: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} - open@8.4.0: - resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} - engines: {node: '>=12'} - openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} @@ -11231,9 +11164,6 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} - pause-stream@0.0.11: - resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} - pbkdf2@3.1.2: resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} engines: {node: '>=0.12'} @@ -11340,6 +11270,9 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + polka@0.5.2: + resolution: {integrity: sha512-FVg3vDmCqP80tOrs+OeNlgXYmFppTXdjD5E7I4ET1NjvtNmQrb1/mJibybKkb/d4NA7YWAr1ojxuhpL3FHqdlw==} + portfinder@1.0.37: resolution: {integrity: sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==} engines: {node: '>= 10.12'} @@ -11725,10 +11658,6 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - proxy-middleware@0.15.0: - resolution: {integrity: sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q==} - engines: {node: '>=0.8.0'} - pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} @@ -12552,10 +12481,6 @@ packages: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} - engines: {node: '>= 18'} - serialize-javascript@5.0.1: resolution: {integrity: sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==} @@ -12568,10 +12493,6 @@ packages: serve-handler@6.1.3: resolution: {integrity: sha512-FosMqFBNrLyeiIDvP1zgO6YoTzFYHxLDEIavhlmQ+knB2Z7l1t+kGLHkZIDN7UVWqQAmKI3D20A6F6jo3nDd4w==} - serve-index@1.9.1: - resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} - engines: {node: '>= 0.8.0'} - serve-static@1.15.0: resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} engines: {node: '>= 0.8.0'} @@ -12610,9 +12531,6 @@ packages: resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} engines: {node: '>=0.10.0'} - setprototypeof@1.1.0: - resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -12720,8 +12638,8 @@ packages: resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} engines: {node: '>= 10'} - sirv@3.0.1: - resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} sister@3.0.2: @@ -12873,9 +12791,6 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - split@1.0.1: - resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} - sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -12940,9 +12855,6 @@ packages: stream-combiner2@1.1.1: resolution: {integrity: sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==} - stream-combiner@0.2.2: - resolution: {integrity: sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==} - stream-http@3.2.0: resolution: {integrity: sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==} @@ -13449,6 +13361,10 @@ packages: trough@1.0.5: resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==} + trouter@2.0.1: + resolution: {integrity: sha512-kr8SKKw94OI+xTGOkfsvwZQ8mWoikZDd2n8XZHjJVZUARZT+4/VV6cacRS6CLsH9bNm+HFIPU1Zx4CnNnb4qlQ==} + engines: {node: '>=6'} + true-case-path@2.2.1: resolution: {integrity: sha512-0z3j8R7MCjy10kc/g+qg7Ln3alJTodw9aDuVWZa3uiWqfuBMKeAeP2ocWcxoyM3D73yz3Jt/Pu4qPr4wHSdB/Q==} @@ -13813,9 +13729,6 @@ packages: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} - unix-crypt-td-js@1.1.4: - resolution: {integrity: sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==} - unixify@1.0.0: resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} engines: {node: '>=0.10.0'} @@ -14202,14 +14115,6 @@ packages: webpack-cli: optional: true - websocket-driver@0.7.4: - resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} - engines: {node: '>=0.8.0'} - - websocket-extensions@0.1.4: - resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} - engines: {node: '>=0.8.0'} - whatwg-encoding@1.0.5: resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} @@ -14642,6 +14547,8 @@ snapshots: dependencies: tslib: 2.0.3 + '@arr/every@1.0.1': {} + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) @@ -17029,25 +16936,6 @@ snapshots: '@types/tough-cookie': 4.0.5 tough-cookie: 4.1.4 - '@compodoc/live-server@1.2.3': - dependencies: - chokidar: 3.6.0 - colors: 1.4.0 - connect: 3.7.0 - cors: 2.8.5 - event-stream: 4.0.1 - faye-websocket: 0.11.4 - http-auth: 4.1.9 - http-auth-connect: 1.0.6 - morgan: 1.10.0 - object-assign: 4.1.1 - open: 8.4.0 - proxy-middleware: 0.15.0 - send: 1.2.0 - serve-index: 1.9.1 - transitivePeerDependencies: - - supports-color - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -18492,7 +18380,7 @@ snapshots: source-map: 0.7.4 webpack: 5.90.3(webpack-cli@4.10.0) - '@polka/url@1.0.0-next.23': {} + '@polka/url@0.5.0': {} '@polka/url@1.0.0-next.29': {} @@ -19832,6 +19720,13 @@ snapshots: dependencies: pino: 9.7.0 + '@types/polka@0.5.7': + dependencies: + '@types/express': 4.17.21 + '@types/express-serve-static-core': 4.17.37 + '@types/node': 20.12.8 + '@types/trouter': 3.1.4 + '@types/prismjs@1.26.5': {} '@types/prop-types@15.7.8': {} @@ -19970,6 +19865,8 @@ snapshots: '@types/tough-cookie@4.0.5': {} + '@types/trouter@3.1.4': {} + '@types/unist@2.0.8': {} '@types/unist@3.0.0': {} @@ -20493,7 +20390,7 @@ snapshots: fflate: 0.8.2 flatted: 3.3.3 pathe: 2.0.3 - sirv: 3.0.1 + sirv: 3.0.2 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 vitest: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) @@ -20783,12 +20680,6 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - apache-crypt@1.2.6: - dependencies: - unix-crypt-td-js: 1.1.4 - - apache-md5@1.1.8: {} - append-field@1.0.0: {} application-config-path@0.1.1: {} @@ -21369,10 +21260,6 @@ snapshots: basic-ftp@5.0.5: {} - batch@0.6.1: {} - - bcryptjs@2.4.3: {} - better-opn@2.1.1: dependencies: open: 7.4.2 @@ -22017,8 +21904,6 @@ snapshots: colorette@2.0.20: {} - colors@1.4.0: {} - combine-source-map@0.8.0: dependencies: convert-source-map: 1.1.3 @@ -22098,15 +21983,6 @@ snapshots: confusing-browser-globals@1.0.11: {} - connect@3.7.0: - dependencies: - debug: 2.6.9 - finalhandler: 1.1.2 - parseurl: 1.3.3 - utils-merge: 1.0.1 - transitivePeerDependencies: - - supports-color - console-browserify@1.2.0: {} constants-browserify@1.0.0: {} @@ -22620,8 +22496,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.0.1 - define-lazy-prop@2.0.0: {} - define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -22915,8 +22789,6 @@ snapshots: encodeurl@1.0.2: {} - encodeurl@2.0.0: {} - end-of-stream@1.4.4: dependencies: once: 1.4.0 @@ -23756,16 +23628,6 @@ snapshots: event-source-polyfill@1.0.31: {} - event-stream@4.0.1: - dependencies: - duplexer: 0.1.2 - from: 0.1.7 - map-stream: 0.0.7 - pause-stream: 0.0.11 - split: 1.0.1 - stream-combiner: 0.2.2 - through: 2.3.8 - event-target-shim@5.0.1: {} eventemitter3@3.1.2: {} @@ -24038,10 +23900,6 @@ snapshots: dependencies: format: 0.2.2 - faye-websocket@0.11.4: - dependencies: - websocket-driver: 0.7.4 - fb-watchman@2.0.2: dependencies: bser: 2.1.1 @@ -24105,18 +23963,6 @@ snapshots: dependencies: '@babel/runtime': 7.23.9 - finalhandler@1.1.2: - dependencies: - debug: 2.6.9 - encodeurl: 1.0.2 - escape-html: 1.0.3 - on-finished: 2.3.0 - parseurl: 1.3.3 - statuses: 1.5.0 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - finalhandler@1.2.0: dependencies: debug: 2.6.9 @@ -24280,10 +24126,6 @@ snapshots: fresh@0.5.2: {} - fresh@2.0.0: {} - - from@0.1.7: {} - fs-constants@1.0.0: {} fs-exists-cached@1.0.0: {} @@ -25431,24 +25273,8 @@ snapshots: domutils: 3.1.0 entities: 4.5.0 - http-auth-connect@1.0.6: {} - - http-auth@4.1.9: - dependencies: - apache-crypt: 1.2.6 - apache-md5: 1.1.8 - bcryptjs: 2.4.3 - uuid: 8.3.2 - http-cache-semantics@4.1.1: {} - http-errors@1.6.3: - dependencies: - depd: 1.1.2 - inherits: 2.0.3 - setprototypeof: 1.1.0 - statuses: 1.5.0 - http-errors@1.8.0: dependencies: depd: 1.1.2 @@ -25465,8 +25291,6 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 - http-parser-js@0.5.8: {} - http-proxy-agent@4.0.1: dependencies: '@tootallnate/once': 1.1.2 @@ -27148,8 +26972,6 @@ snapshots: map-cache@0.2.2: {} - map-stream@0.0.7: {} - map-visit@1.0.0: dependencies: object-visit: 1.0.1 @@ -27176,6 +26998,10 @@ snapshots: markdown-it: 14.0.0 markdownlint-micromark: 0.1.8 + matchit@1.1.0: + dependencies: + '@arr/every': 1.0.1 + matchmediaquery@0.3.1: dependencies: css-mediaquery: 0.1.2 @@ -27902,10 +27728,6 @@ snapshots: dependencies: mime-db: 1.52.0 - mime-types@3.0.1: - dependencies: - mime-db: 1.54.0 - mime@1.6.0: {} mime@2.6.0: {} @@ -28052,16 +27874,6 @@ snapshots: '@aws-sdk/credential-providers': 3.521.0 socks: 2.8.3 - morgan@1.10.0: - dependencies: - basic-auth: 2.0.1 - debug: 2.6.9 - depd: 2.0.0 - on-finished: 2.3.0 - on-headers: 1.0.2 - transitivePeerDependencies: - - supports-color - mri@1.2.0: {} mrmime@1.0.1: {} @@ -28426,10 +28238,6 @@ snapshots: on-exit-leak-free@2.1.0: {} - on-finished@2.3.0: - dependencies: - ee-first: 1.1.1 - on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -28453,12 +28261,6 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 - open@8.4.0: - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - openapi-types@12.1.3: {} opener@1.5.2: {} @@ -28732,10 +28534,6 @@ snapshots: pathval@2.0.1: {} - pause-stream@0.0.11: - dependencies: - through: 2.3.8 - pbkdf2@3.1.2: dependencies: create-hash: 1.2.0 @@ -28846,6 +28644,11 @@ snapshots: pluralize@8.0.0: {} + polka@0.5.2: + dependencies: + '@polka/url': 0.5.0 + trouter: 2.0.1 + portfinder@1.0.37: dependencies: async: 3.2.6 @@ -29238,8 +29041,6 @@ snapshots: proxy-from-env@1.1.0: {} - proxy-middleware@0.15.0: {} - pseudomap@1.0.2: {} psl@1.9.0: {} @@ -30299,22 +30100,6 @@ snapshots: transitivePeerDependencies: - supports-color - send@1.2.0: - dependencies: - debug: 4.4.1 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.0 - mime-types: 3.0.1 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - serialize-javascript@5.0.1: dependencies: randombytes: 2.1.0 @@ -30338,18 +30123,6 @@ snapshots: path-to-regexp: 2.2.1 range-parser: 1.2.0 - serve-index@1.9.1: - dependencies: - accepts: 1.3.8 - batch: 0.6.1 - debug: 2.6.9 - escape-html: 1.0.3 - http-errors: 1.6.3 - mime-types: 2.1.35 - parseurl: 1.3.3 - transitivePeerDependencies: - - supports-color - serve-static@1.15.0: dependencies: encodeurl: 1.0.2 @@ -30421,8 +30194,6 @@ snapshots: is-plain-object: 2.0.4 split-string: 3.1.0 - setprototypeof@1.1.0: {} - setprototypeof@1.2.0: {} sha.js@2.4.11: @@ -30549,11 +30320,11 @@ snapshots: sirv@2.0.3: dependencies: - '@polka/url': 1.0.0-next.23 + '@polka/url': 1.0.0-next.29 mrmime: 1.0.1 totalist: 3.0.1 - sirv@3.0.1: + sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 mrmime: 2.0.1 @@ -30744,10 +30515,6 @@ snapshots: split2@4.2.0: {} - split@1.0.1: - dependencies: - through: 2.3.8 - sprintf-js@1.0.3: {} sprintf-js@1.1.3: {} @@ -30811,11 +30578,6 @@ snapshots: duplexer2: 0.1.4 readable-stream: 2.3.8 - stream-combiner@0.2.2: - dependencies: - duplexer: 0.1.2 - through: 2.3.8 - stream-http@3.2.0: dependencies: builtin-status-codes: 3.0.0 @@ -31409,7 +31171,7 @@ snapshots: tough-cookie@4.1.4: dependencies: psl: 1.9.0 - punycode: 2.3.0 + punycode: 2.3.1 universalify: 0.2.0 url-parse: 1.5.10 @@ -31443,6 +31205,10 @@ snapshots: trough@1.0.5: {} + trouter@2.0.1: + dependencies: + matchit: 1.1.0 + true-case-path@2.2.1: {} ts-api-utils@2.0.1(typescript@5.7.3): @@ -31890,8 +31656,6 @@ snapshots: universalify@2.0.0: {} - unix-crypt-td-js@1.1.4: {} - unixify@1.0.0: dependencies: normalize-path: 2.1.1 @@ -32493,14 +32257,6 @@ snapshots: - esbuild - uglify-js - websocket-driver@0.7.4: - dependencies: - http-parser-js: 0.5.8 - safe-buffer: 5.2.1 - websocket-extensions: 0.1.4 - - websocket-extensions@0.1.4: {} - whatwg-encoding@1.0.5: dependencies: iconv-lite: 0.4.24