diff --git a/.gitignore b/.gitignore index c6bba59..afb3085 100644 --- a/.gitignore +++ b/.gitignore @@ -123,8 +123,13 @@ dist .vscode-test # yarn v2 +.yarn/releases .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# Ignore automation scripts +*.ps1 +*.sh diff --git a/README.md b/README.md index 47dba0a..7ff3335 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,125 @@ -# ba-web-framework -A framework for web apps built atop Node, Express and other libraries and utilties that makes creating and maintaining Bridgeman Accessible web apps easier. +# Bridgeman Accessible Web Framework +A framework for web apps built atop [Node](https://nodejs.org/en), [Express.js](https://expressjs.com/) and other libraries and utilities that make creating and maintaining web apps easier. + +Admittedly, this is not a mature, feature complete, etc... framework compared to others that already exist (ex. [NEXT.JS](https://nextjs.org/), [Nuxt](https://nuxt.com/), etc...). The intention of this framework is largely to simplify existing Express apps in a way that they become more readable and learnable quickly for people that come from other frameworks and languages. + +## Requirements +- ~~Has a `base.ejs` file~~ (or use the `BaseTemplateCreator` to create one - [**new**] which the `Renderer` will do automatically) +- Uses Typescript 5.4.5 or greater (because decorators don't compile properly otherwise) + +## Decorators +This package relies heavily on decorators as it's primary mechanism for code creation/insertion. + +The decorators included in this framework include: +- [`@Controller`](#controllers-controller) +- [`@GET`](#http-get-routes-get) +- [`@POST`](#http-post-routes-post) +- [`@Page`](#easy-page-rendering-page) +- [`@ChildController`]() + +### Controllers (`@Controller`) +To understand what the `@Controller` does you need to understand the automated controller detection done within the `Router` class/object. More specifically, all controllers should be subclasses of the `BaseController` which has an abstract `setup` method that is called when a controller is detected to setup it's route callbacks for the app (that is, telling the app what to do when a particular endpoint/route is hit). + +However, controller/application developers don't need to worry about implementing this `setup` method because of this decorator which automatically generates the `setup` method which adds routes based on the class' use of the `@GET` and `@POST` decorators. + +### HTTP GET Routes (`@GET`) +As briefly explained in the [`@Controller` section](#controllers-controller) this decorator assists in the automated creation of the `setup` method for the controller. Which sets up the routes in the app. In particular, the automation within the `@Controller.setup` method translates any method with the `@GET` decorator into an `app.get` call (where `app` is the Express app) with the decorated method as the callback. + +Note, the singular parameter for this decorator is a string denoting the route where the decorated method will be used as the callback. + +In other words, the following two code snippets are equivalent: + +```typescript +@Controller() +export class HomeRoutes extends BaseController { + @GET('/') + page(req: Request, res: Response) { + ... + } +} +``` + +```typescript +export class HomeRoutes { + page(req: Request, res: Response) { + ... + } + + static setup(app: Express.Application) { + const homeRoutes = new HomeRoutes(); + app.get('/', homeRoutes.page.bind(homeRoutes)); + } +} +``` + +### HTTP POST Routes (`@POST`) +As briefly explained in the [`@Controller` section](#controllers-controller) this decorator assists in the automated creation of the `setup` method for the controller. Which sets up the routes in the app. In particular, the automation within the `@Controller.setup` method translates any method with the `@POST` decorator into an `app.post` call (on the Express app) with the decorated method as the callback. + +Unlike the `@GET` decorator This decorator takes anywhere from one to an unconstrained number of parameters. The first parameter is the string endpoint/route at which the method will be called. The parameters after that are middleware to apply to the request in addition to globally configured middleware. This is largely to compensate for needed request/body processing before the decorated method can handle the request. + +In other words, the following two code snippets are equivalent: + +```typescript +@Controller() +export class FormRoutes extends BaseController { + @POST('/form/submit', express.json()) + formSubmission(req: Request, res: Response) { + ... + } +} +``` + +```typescript +export class FormRoutes { + formSubmission(req: Request, res: Response) { + ... + } + + static setup(app: Express.Application) { + const formRoutes = new FormRoutes(); + app.post('/form/submit', express.json(), formRoutes.page.bind(formRoutes)); + } +} +``` + +### Easy Page Rendering (`@Page`) +A very common pattern particularly for GET requests but also POST requests from time to time is to render a page as a result/response of the request. This can be automated in small ways particularly if we make a few assumptions that seem to be true across numerous projects. + +The assumptions: +- Use EJS as the templating engine (in theory this is a pretty soft requirement but is what it works with currently) +- Have a `base.ejs` that contains a `<%- include(page) %>` snippet (also including stuff for `extraScripts`, `extraStyles` and `title` is highly recommended as these are common, in existing not in content, on most pages - see [`BaseTemplateCreator`](./src/BaseTemplateCreator.ts) as a guide) + +In other words, the following two code snippets are equivalent: + +```typescript +@Controller() +export class HomeRoutes extends BaseController { + @Page('Home', 'home.ejs', ['some-interactive-component'], ['extra-styles']) + @GET('/') + page(req: Request, res: Response) {} +} +``` + +```typescript +export class HomeRoutes { + page(req: Request, res: Response) { + res.render('base', { + page: 'home.ejs', + title: 'Home', + extraScripts: ['js/some-interactive-component.js'], + extraStyles: ['css/extra-styles.css'] + }); + } + + static setup(app: Express.Application) { + const homeRoutes = new HomeRoutes(); + app.get('/', homeRoutes.page.bind(homeRoutes)); + } +} +``` + +Granted, there are some intricacies around in the decorator and `base.ejs` we try to simplify the `extraScripts` and `extraStyles` so that you don't have to know the folder and file extensions etc... so that it's that little bit simpler but you get the idea. + +There is also a layer of complexity if this callback has a return. It should be either: +- a boolean - if to render or not +- an object - any additional render parameters for the template \ No newline at end of file diff --git a/docs/Automated Controller Loading with Child Controllers.md b/docs/Automated Controller Loading with Child Controllers.md new file mode 100644 index 0000000..254e06a --- /dev/null +++ b/docs/Automated Controller Loading with Child Controllers.md @@ -0,0 +1,32 @@ +# Automated Controller Loading with Child Controllers +While loading controllers automatically from a provided directory is an overall helpful feature in abbreviating the code for the end developer/user while maintaining a high level of flexibility. However, it does introduce a particular problem with child controllers. + +## The Issue +The issue with a naive approach of just calling `setup` on all controllers we find in the designated directory (and it's subdirectories) is this would produce duplicate callbacks for the same route. This is because we would both call `setup` as we find the controllers but ALSO inside the `Controller` decorator for child controllers. + +## The Solution Impact +However, to resolve this has meant multiple entire passes of all the found controllers which if there is a large amount of controllers could be pretty inefficient in a number of ways. + +For instance, this means that parallelization of loading is limited to the initial loading rather than calling of `setup` + +Another way, our particular solution to this issue, at least currently, precludes using any kind of greedy methodology which if we could use could make the loading significantly more efficient. + +This is because we can't know that the parent controller (with the `ChildController` decorator) would be processed before any child controller is. + +## The Solution +The following is the isolated code that addresses this particular issue + +```typescript +// Get all controllers that have the child controller decorator +const controllersWithChildControllers = loadedControllers.filter(controller => Reflect.getMetadata('ChildController', controller) !== undefined); + +// Get all those controllers that are designated as children controllers by parent controller +const childrenControllers = controllersWithChildControllers.map(controller => Reflect.getMetadata('ChildController', controller)); + +// Get the exclusive set of controllers that don't fit in the group of controllers with child controllers OR a child controller themselves +const controllersWithoutChildren = loadedControllers.filter(controller => !controllersWithChildControllers.includes(controller) && !childrenControllers.includes(controller)); + +// Take the list of controllers with child controllers and the list without children (that excludes children themselves) +// And forma a list that has all child controllers removed +const topLevelControllers = [...controllersWithChildControllers, ...controllersWithoutChildren]; +``` \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..259f6a2 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "@BridgemanAccessible/ba-web-framework", + "version": "1.0.0", + "description": "A framework for web apps built atop Node, Express and other libraries and utilties that makes creating and maintaining Bridgeman Accessible web apps easier.", + "author": "Bridgeman Accessible (initializer?: T, callback?: (app: Application) => void | Promise) { + // Do the initial setup of the app + let app: Application; + if(typeof initializer !== 'undefined') { + app = await initializer.init(); + } + else { + app = await (new Initializer()).init(); + } + + // Start the server + const port = process.env.PORT || this.DEFAULT_PORT; + app.listen(port, async () => { + console.log(`Server is running on port ${port}`); + + // Run the callback if one is provided + if(typeof callback !== 'undefined') { + await callback(app); + } + }); + } +} \ No newline at end of file diff --git a/src/BaseTemplateCreator.ts b/src/BaseTemplateCreator.ts new file mode 100644 index 0000000..5090a1b --- /dev/null +++ b/src/BaseTemplateCreator.ts @@ -0,0 +1,514 @@ +import path from 'path'; +import fse from 'fs-extra'; +import { JSDOM } from 'jsdom'; + +/** The inputs for setting up the meta tags */ +type SetupMetaTagInputs = { + /** Description for the Meta description tag */ + description?: string, + /** Keywords for the Meta keywords tag */ + keywords?: string[], + /** author for the Meta author tag */ + author?: string, +} + +/** The inputs for the `setupHead` method */ +type SetupHeadInputs = SetupMetaTagInputs & { + /** if to include Stripe script(s) */ + includeStripe?: boolean, + /** If to include Foundation Framework script(s) */ + includeFoundationFramework?: boolean, + /** The Font Awesome kit to include (if applicable) */ + fontAwesomeKit?: string +} + +/** The inputs for the base template */ +export type BaseTemplateInputs = SetupHeadInputs; + +/** + * A class to create a base template for an app + */ +export class BaseTemplateCreator { + /** The output of the template */ + private output: string; + + /** The DOM of the template */ + private dom: JSDOM; + + /** The document of the template */ + private document: Document; + + /** + * The constructor for the BaseTemplateCreator class + * + * @param language The human language of the document (default: English/`en`) + */ + constructor(language: string = 'en') { + this.output = ''; + + // Create the base document + const { dom, document } = this.createBaseDocument(language); + this.dom = dom; + this.document = document; + } + + /** + * Create a base document + * + * @param language The human language of the document (default: English/`en`) + * @returns The base document's DOM and document + */ + private createBaseDocument(language = 'en') { + // Create a basic HTML document (as a string) + const docTypeTag = ''; + const emptyHeadTags = ''; + const emptyBodyTags = ''; + const htmlLanguageProperty = `lang="${language}"`; + const basicHtmlTags = `${emptyHeadTags}${emptyBodyTags}`; + const domStr = `${docTypeTag}${basicHtmlTags}`; + + // Create a DOM and document from the string + const dom = new JSDOM(domStr); + const document = dom.window.document; + + return { dom, document }; + } + + /** + * Create the meta tag section of the head tag + * + * @param inputs The inputs to use for setting up the meta tag section of the head tag + * @returns The string representation of the meta tag section of the head tag + */ + private createHeadMetaTagSection(inputs?: SetupMetaTagInputs) { + let output = ''; + + // Create the meta charset tag + const metaCharsetTag = this.document.createElement('meta'); + metaCharsetTag.setAttribute('charset', 'utf-8'); + + // Create the meta description tag + // Note, if no description is given then we specify a template string that allows specifying it later + const metaDescTag = this.document.createElement('meta'); + metaDescTag.name = 'description'; + metaDescTag.content = typeof inputs !== 'undefined' && typeof inputs.description !== 'undefined' ? inputs.description : '<% if(typeof description !== \'undefined\') { %><%= description %><% } %>'; + + // Create the meta keywords tag + // Note, if no keywords are given then we specify a template string that allows specifying it later + const metaKeywordsTag = this.document.createElement('meta'); + metaKeywordsTag.name = 'keywords'; + metaKeywordsTag.content = typeof inputs !== 'undefined' && typeof inputs.keywords !== 'undefined' ? inputs.keywords.join(', ') : '<% if(typeof keywords !== \'undefined\') { %><%= keywords.join(\', \') %><% } %>'; + + // Create the meta author tag + // Note, if no author is given then we specify a template string that allows specifying it later + const metaAuthorTag = this.document.createElement('meta'); + metaAuthorTag.name = 'author'; + metaAuthorTag.content = typeof inputs !== 'undefined' && typeof inputs.author !== 'undefined' ? inputs.author : '<% if(typeof author !== \'undefined\') { %><%= author %><% } %>'; + + // Create the meta viewport tag + const metaViewportTag = this.document.createElement('meta'); + metaViewportTag.name = 'viewport'; + const viewportWidth = 'device-width'; + const viewportWidthParam = `width=${viewportWidth}`; + const viewportScale = '1.0'; + const viewportScaleParam = `initial-scale=${viewportScale}`; + metaViewportTag.content = [viewportWidthParam, viewportScaleParam].join(', '); + + output += '\t\t' + metaCharsetTag.outerHTML + '\n'; + output += '\t\t' + metaDescTag.outerHTML + '\n'; + output += '\t\t' + metaKeywordsTag.outerHTML + '\n'; + output += '\t\t' + metaAuthorTag.outerHTML + '\n'; + output += '\t\t' + metaViewportTag.outerHTML + '\n'; + + return output; + } + + /** + * Create the title tag for the template + * + * @returns The string representation of the title tag + */ + private createTitleTag() { + // Create the title tag + const titleTag = this.document.createElement('title'); + const titlePrefixPortion = '<% if (typeof titlePrefix !== \'undefined\') { %><%= titlePrefix %><% } %>'; + const titlePortion = '<% if(typeof title !== \'undefined\') { %><%= title %><% } %>'; + const titleSuffixPortion = '<% if (typeof titleSuffix !== \'undefined\') { %><%= titleSuffix %><% } %>'; + titleTag.innerHTML = `${titlePrefixPortion}${titlePortion}${titleSuffixPortion}`; + + return '\t\t' + titleTag.outerHTML + '\n'; + } + + /** + * Create the extra styles block for the template + * + * This is a portion of the `` tag within the template that allows a individual controller to specify additional styles. + * In particular, it allows a controller to specify additional CSS stylesheets to include in the page. + * + * @returns The string representation of the extra styles block + */ + private createExtraStylesBlock() { + let output = ''; + + const extraStylesArrayLinkTag = this.document.createElement('link'); + extraStylesArrayLinkTag.rel = 'stylesheet'; + extraStylesArrayLinkTag.type = 'text/css'; + extraStylesArrayLinkTag.href = '/css/<%= style %>.css'; + + const extraStylesLinkTag = this.document.createElement('link'); + extraStylesLinkTag.rel = 'stylesheet'; + extraStylesLinkTag.type = 'text/css'; + extraStylesLinkTag.href = '/css/<%= extraStyles %>.css'; + + output += '<%# Add any additional stylesheets specified within a controller etc... %>' + '\n'; + output += '<%# This can either be a singular string or a array of strings %>' + '\n'; + output += '<%# Note, that the string should be the name of the stylesheet WITHOUT the `.css` extension and exist in the `css/` directory %>' + '\n'; + output += '<% if (typeof extraStyles !== \'undefined\') { %>' + '\n'; + output += '\t' + '<% if (Array.isArray(extraStyles)) { %>' + '\n'; + output += '\t\t' + '<%# Because it\'s an array, we need to loop through each stylesheet and include it %>' + '\n'; + output += '\t\t' + '<% for (let style of extraStyles) { %>' + '\n'; + output += '\t\t\t' + extraStylesArrayLinkTag.outerHTML + '\n'; + output += '\t\t' + '<% } %>' + '\n'; + output += '\t' + '<% } else { %>' + '\n'; + output += '\t\t' + '<%# Include the singular stylesheet %>' + '\n'; + output += '\t\t' + extraStylesLinkTag.outerHTML +'\n'; + output += '\t' + '<% } %>' + '\n'; + output += '<% } %>'; + + return output; + } + + /** + * Create the styles section of the head tag + * + * @param includeFoundationFramework If to include the Foundation Framework styles + * @returns The string representation of the styles section of the head tag + */ + private createHeadStylesSection(includeFoundationFramework: boolean = false) { + let output = ''; + + const stylesComment = this.document.createComment('Styles'); + output += '\t\t' + `` + '\n'; + + const customStyles = this.document.createComment('Custom styling'); + output += '\t\t' + `` + '\n'; + + const baseStylesLinkTag = this.document.createElement('link'); + baseStylesLinkTag.rel = 'stylesheet'; + baseStylesLinkTag.type = 'text/css'; + baseStylesLinkTag.href = '/css/style.css'; + output += '\t\t' + baseStylesLinkTag.outerHTML + '\n'; + + const accessibilityStylesLinkTag = this.document.createElement('link'); + accessibilityStylesLinkTag.rel = 'stylesheet'; + accessibilityStylesLinkTag.type = 'text/css'; + accessibilityStylesLinkTag.href = '/css/accessibility.css'; + output += '\t\t' + accessibilityStylesLinkTag.outerHTML + '\n'; + + if(includeFoundationFramework) { + const foundationFrameworkComment = this.document.createComment('Foundation Framework'); + + const foundationFrameworkLinkTag = this.document.createElement('link'); + foundationFrameworkLinkTag.rel = 'stylesheet'; + foundationFrameworkLinkTag.type = 'text/css'; + foundationFrameworkLinkTag.href = '/css/app.css'; + + output += '\t\t' + `` + '\n'; + output += '\t\t' + `${foundationFrameworkLinkTag.outerHTML}` + '\n'; + } + + //const sassComment = this.document.createComment('SASS components'); + + /*const systemThemedBackgroundLinkTag = this.document.createElement('link'); + systemThemedBackgroundLinkTag.rel = 'stylesheet'; + systemThemedBackgroundLinkTag.type = 'text/css'; + systemThemedBackgroundLinkTag.href = '/css/components/system-themed-background.css';*/ + + //output += '\t\t' + `` + '\n'; + //output += '\t\t' + `${systemThemedBackgroundLinkTag.outerHTML}` + '\n'; + + const ejsExtraStylesBlock = this.createExtraStylesBlock(); + output += '\t\t' + ejsExtraStylesBlock.replaceAll('\n', '\n\t\t') + '\n'; + + return output; + } + + /** + * Create the extra scripts block for the template + * + * This is a portion of the `` tag within the template that allows a individual controller to specify additional scripts + * These scripts can come in a few different forms which is one of the reason it requires a template block like this. + * + * @returns The string representation of the extra scripts block + */ + private createExtraScriptsBlock() { + let output = ''; + + const htmlBlockComment = this.document.createComment('Controller specific scripts'); + + const arrayItemStringExternalScriptTag = this.document.createElement('script'); + arrayItemStringExternalScriptTag.type = 'application/javascript'; + arrayItemStringExternalScriptTag.src = '<%= script %>'; + + const arrayItemStringLocalScriptTag = this.document.createElement('script'); + arrayItemStringLocalScriptTag.type = 'application/javascript' + arrayItemStringLocalScriptTag.src = '/js/<%= script %>.js' + arrayItemStringLocalScriptTag.defer = true; + + const singularStringExternalScriptTag = this.document.createElement('script'); + singularStringExternalScriptTag.type = 'application/javascript' + singularStringExternalScriptTag.src = '<%= extraScripts %>'; + + const singularStringLocalScriptTag = this.document.createElement('script'); + singularStringLocalScriptTag.type = 'application/javascript' + singularStringLocalScriptTag.src = '/js/<%= extraScripts %>.js' + singularStringLocalScriptTag.defer = true; + + output += `` + '\n'; + output += '<%# Add any additional scripts specified within a controller etc... %>' + '\n'; + output += '<%# %>' + '\n'; + output += '<%# Note, that these can come in multiple formats as described in the table below: %>' + '\n'; + output += '<%# | Type | Description | Format | Use Cases | %>' + '\n'; + output += '<%# | ------ | --------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------- | %>' + '\n'; + output += '<%# | string | The name of the script to include | `' + '\n'; + output += '\t\t\t\t' + '<% } else { %>' + '\n'; + output += '\t\t\t\t\t' + '<%# Because the `.script` property doesn\'t start with `http` or `https` we assume it\'s a "local" script and include it as a local script (from the `js/` folder and with a `.js` extension) %>' + '\n'; + output += '\t\t\t\t\t' + '' + '\n'; + output += '\t\t\t\t' + '<% } %>' + '\n'; + output += '\t\t\t' + '<% } else { %>' + '\n'; + output += '\t\t\t\t' + '<% if(script.startsWith(\'http\') || script.startsWith(\'https\')) { %>' + '\n'; + output += '\t\t\t\t\t' + '<%# Because the string starts with `http` or `https` we assume it\'s an "external" script and include it as a straight URL %>' + '\n'; + output += '\t\t\t\t\t' + arrayItemStringExternalScriptTag.outerHTML + '\n'; + output += '\t\t\t\t' + '<% } else { %>' + '\n'; + output += '\t\t\t\t\t' + '<%# Because the string doesn\'t start with `http` or `https` we assume it\'s a "local" script and include it as a local script (from the `js/` folder and with a `.js` extension) %>' + '\n'; + output += '\t\t\t\t\t' + arrayItemStringLocalScriptTag.outerHTML + '\n'; + output += '\t\t\t\t' + '<% } %>' + '\n'; + output += '\t\t\t' + '<% } %>' + '\n'; + output += '\t\t' + '<% } %>' + '\n'; + output += '\t' + '<% } else if (typeof extraScripts === \'object\') { %>' + '\n'; + output += '\t\t' + '<% if(extraScripts.script.startsWith(\'http\') || extraScripts.script.startsWith(\'https\')) { %>' + '\n'; + output += '\t\t\t' + '<%# Because the `.script` property of the singular object starts with `http` or `https` we assume it\'s an "external" script and include it as a straight URL %>' + '\n'; + output += '\t\t\t' + '' + '\n'; + output += '\t\t' + '<% } else { %>' + '\n'; + output += '\t\t\t' + '<%# Because the `.script` property of the singular object doesn\'t start with `http` or `https` we assume it\'s a "local" script and include it as a local script (from the `js/` folder and with a `.js` extension) %>' + '\n'; + output += '\t\t\t' + '' + '\n'; + output += '\t\t' + '<% } %>' + '\n'; + output += '\t' + '<% } else { %>' + '\n'; + output += '\t\t' + '<% if(extraScripts.startsWith(\'http\') || extraScripts.startsWith(\'https\')) { %>' + '\n'; + output += '\t\t\t' + '<%# Because the singular string starts with `http` or `https` we assume it\'s an "external" script and include it as a straight URL %>' + '\n'; + output += '\t\t\t' + singularStringExternalScriptTag.outerHTML + '\n'; + output += '\t\t' + '<% } else { %>' + '\n'; + output += '\t\t\t' + '<%# Because the singular string doesn\'t start with `http` or `https` we assume it\'s a "local" script and include it as a local script (from the `js/` folder and with a `.js` extension) %>' + '\n'; + output += '\t\t\t' + singularStringLocalScriptTag.outerHTML + '\n'; + output += '\t\t' + '<% } %>' + '\n'; + output += '\t' + '<% } %>' + '\n'; + output += '<% } %>' + '\n'; + + return output; + } + + /** + * Create the scripts section of the head tag + * + * @param includeStripe If to include the Stripe script(s) + * @param includeFoundationFramework If to include the Foundation Framework script(s) + * @param fontAwesomeKit The Font Awesome kit to include (if applicable) + * @returns The string representation of the scripts section of the head tag + */ + private createHeadScriptsSection(includeStripe: boolean = false, includeFoundationFramework: boolean = false, fontAwesomeKit?: string) { + let output = ''; + + const scriptsComment = this.document.createComment('Scripts'); + output += '\t\t' + `` + '\n'; + + if (includeStripe) { + const stripeScriptTag = this.document.createElement('script'); + stripeScriptTag.src = 'https://js.stripe.com/v3'; + stripeScriptTag.async = true; + + output += '\t\t' + stripeScriptTag.outerHTML + '\n'; + } + + if(typeof fontAwesomeKit !== 'undefined') { + const fontAwesomeScriptTag = this.document.createElement('script'); + fontAwesomeScriptTag.src = `https://kit.fontawesome.com/${fontAwesomeKit}.js`; + fontAwesomeScriptTag.crossOrigin = 'anonymous'; + + output += '\t\t' + fontAwesomeScriptTag.outerHTML + '\n' + } + + if(includeFoundationFramework) { + const foundationFrameworkComment = this.document.createComment('Foundation Framework'); + + const foundationFrameworkScriptTag = this.document.createElement('script'); + foundationFrameworkScriptTag.type = 'application/javascript'; + foundationFrameworkScriptTag.src = '/js/foundation/main.js'; + foundationFrameworkScriptTag.defer = true; + + output += '\t\t' + `` + '\n'; + output += '\t\t' + foundationFrameworkScriptTag.outerHTML + '\n'; + } + + const ejsExtraScriptsBlock = this.createExtraScriptsBlock(); + output += '\t\t' + ejsExtraScriptsBlock.replaceAll('\n', '\n\t\t') + '\n'; + + return output; + } + + /** + * Setup the head portion of the document (`` tag) + * + * @param inputs The inputs to use for setting up the head portion of the document + * @returns The string representation of the head portion of the document + */ + private setupHead(inputs?: SetupHeadInputs) { + const headTag = this.document.querySelector('head'); + + if (!headTag) { + console.error('Head tag not found'); + return; + } + + const includeFoundationFramework = typeof inputs !== 'undefined' && typeof inputs.includeFoundationFramework !== 'undefined' ? inputs.includeFoundationFramework : false; + const includeStripe = typeof inputs !== 'undefined' && typeof inputs.includeStripe !== 'undefined' ? inputs.includeStripe : false; + + let headTagContents = headTag.innerHTML + '\n'; + headTagContents += this.createHeadMetaTagSection(inputs); + headTagContents += this.createTitleTag(); + headTagContents += this.createHeadStylesSection(includeFoundationFramework); + headTagContents += this.createHeadScriptsSection(includeStripe, includeFoundationFramework, inputs?.fontAwesomeKit); + + headTag.innerHTML = headTagContents; + } + + /** + * Create the header block for the template + * + * @returns The string representation of the header block + */ + private createHeaderBlock() { + let output = ''; + + output += "<% if(typeof header !== 'undefined') { %>" + '\n'; + output += '\t' + '<%- include(header) %>' + '\n'; + output += '<% } else { %>' + '\n'; + output += '\t' + "<%- include('includes/header.ejs') %>" + '\n'; + output += '<% } %>'; + + return output; + } + + /** + * Create the footer block for the template + * + * @return The string representation of the footer block + */ + private createFooterBlock() { + let output = ''; + + output += "<% if(typeof footer !== 'undefined') { %>" + '\n'; + output += '\t' + '<%- include(footer) %>' + '\n'; + output += '<% } else { %>' + '\n'; + output += '\t' + "<%- include('includes/footer.ejs') %>" + '\n'; + output += '<% } %>'; + + return output; + } + + /** + * Setup the body portion of the document (`` tag) + * + * @returns The string representation of the body portion of the document + */ + private setupBody() { + const skipLink = this.document.createElement('a'); + skipLink.id = 'skip-link'; + skipLink.href = '#main'; + skipLink.innerText = 'Skip to main content'; + this.document.body.appendChild(skipLink); + + const contentsDiv = this.document.createElement('div'); + contentsDiv.id = 'contents'; + + const headerElem = this.document.createElement('header'); + let ejsHeaderIncludeBlock = this.createHeaderBlock(); + headerElem.innerHTML = ejsHeaderIncludeBlock; + contentsDiv.appendChild(headerElem); + + const mainElem = this.document.createElement('main'); + mainElem.id = 'main'; + + const containerDiv = this.document.createElement('div'); + containerDiv.classList.add('container'); + + const contentDiv = this.document.createElement('div'); + contentDiv.classList.add('content'); + contentDiv.innerHTML = '<%- include(page) %>'; + containerDiv.appendChild(contentDiv); + + mainElem.appendChild(containerDiv); + + contentsDiv.appendChild(mainElem); + + const footerElem = this.document.createElement('footer'); + const ejsFooterIncludeBlock = this.createFooterBlock(); + footerElem.innerHTML = ejsFooterIncludeBlock; + contentsDiv.appendChild(footerElem); + + this.document.body.appendChild(contentsDiv); + } + + /** + * Prepare the output of the template for writing to a file + */ + private prepare() { + this.output = this.dom.serialize() + // Fixing the EJS tags + .replaceAll('<%', '<%') + .replaceAll('%>', '%>') + // Logical operators (particularly used within EJS tags) + .replaceAll('&&', '&&') + .replaceAll(' > ', ' > ') + .replaceAll(' < ', ' < '); + } + + /** + * Write the template out to a `base.ejs` file + * + * @param folder The folder where the file should be written to + */ + private write(folder: string) { + fse.writeFileSync(path.resolve(folder, 'base.ejs'), this.output); + } + + /** + * The static method to create a base template + * + * @param folder The folder to write the base template to + * @param baseTemplateInputs The inputs to use for the base template + */ + static create(folder: string = 'pages', baseTemplateInputs?: BaseTemplateInputs) { + // Create the base template creator object (sets up an initial document to work with) + const templateCreator = new BaseTemplateCreator(); + + // Sets up the head portion of the document + templateCreator.setupHead(baseTemplateInputs); + templateCreator.setupBody(); + templateCreator.prepare(); + templateCreator.write(folder); + } +} \ No newline at end of file diff --git a/src/Initializer.ts b/src/Initializer.ts new file mode 100644 index 0000000..015fb04 --- /dev/null +++ b/src/Initializer.ts @@ -0,0 +1,113 @@ +import express, { Application, RequestHandler } from 'express'; + +import { Router } from './Router'; +import { StaticFileResolver } from './StaticFileResolver'; +import { Renderer } from './Renderer'; + +/** + * Object to encapsulate the setup of the app + * + * This is passed to the App object's `run` method. + * This approach allows easy injection of customizations to the app setup with minimal code. + * This is done by creating a customized version of the Initializer class (setting the constructor parameters). + * Or by creating a child class of the Initializer class and overriding the `init` method. + */ +export class Initializer { + /** The path to the controllers */ + private controllersPath?: string; + + /** The path to the static files (css, js, etc...) */ + private staticFilesPath?: string; + + /** The view engine and path to the view files */ + private view?: { filesPath: string, engine?: string }; + + /** The middlewares to use */ + private middlewares: ((...args: any[]) => RequestHandler)[]; + + /** + * Create a new Initializer + * + * Note, that the parameter is an object of inputs that can be used to customize the setup. + * It's done this way because most of the customizations are optional and order doesn't matter. + * + * @param inputs The inputs for the initializer + * @param inputs.controllersPath The path to the controllers + * @param inputs.staticFilesPath The path to the static files (css, js, etc...) + * @param inputs.view.engine The view engine to use (ex. 'ejs') + * @param inputs.view.filesPath The path to the view files + * @param middlewares Th middlewares to use + */ + constructor(inputs?: { controllersPath?: string, staticFilesPath?: string, view?: { filesPath: string, engine?: string } }, ...middlewares: ((...args: any[]) => RequestHandler)[]) { + this.controllersPath = typeof inputs !== 'undefined' && inputs.controllersPath !== 'undefined' ? inputs.controllersPath : undefined; + this.staticFilesPath = typeof inputs !== 'undefined' && inputs.staticFilesPath !== 'undefined' ? inputs.staticFilesPath : undefined; + this.view = typeof inputs !== 'undefined' && typeof inputs.view !== 'undefined' ? inputs.view : undefined; + + this.middlewares = middlewares; + } + + /** + * Create the Express app + * + * @returns The newly created Express app + */ + private async createExpressApp(): Promise { + // Create the Express app + const app = express(); + + return app; + } + + /** + * Setup the global middleware for the server + * + * @param app The Express app to setup the middleware on + */ + private async setupMiddleware(app: Application) { + this.middlewares.forEach(middleware => app.use(middleware())); + } + + /** + * Do the initial setup for the application + * + * @returns The Express app that gets created and setup + */ + async init(): Promise { + // Create the Express app + const app = await this.createExpressApp(); + + // Setup global middleware + await this.setupMiddleware(app); + + // Setup the router (how the app handles requests) + if(typeof this.controllersPath !== 'undefined') { + await (new Router(this.controllersPath)).setup(app); + } + else { + await (new Router()).setup(app); + } + + // Setup the static file resolver (how the app serves static files) + if(typeof this.staticFilesPath !== 'undefined') { + await (new StaticFileResolver(this.staticFilesPath)).setup(app); + } + else { + await (new StaticFileResolver()).setup(app); + } + + // Setup the renderer (how the app renders templates - templates can use any Express supported view engine) + if(typeof this.view !== 'undefined') { + if(typeof this.view.engine !== 'undefined') { + await (new Renderer(this.view.filesPath, this.view.engine)).setup(app); + } + else { + await (new Renderer(this.view.filesPath)).setup(app); + } + } + else { + await (new Renderer()).setup(app); + } + + return app; + } +} \ No newline at end of file diff --git a/src/Renderer.ts b/src/Renderer.ts new file mode 100644 index 0000000..f369814 --- /dev/null +++ b/src/Renderer.ts @@ -0,0 +1,53 @@ +import { existsSync, statSync } from 'fs'; +import path from 'path'; +import { Application } from 'express'; + +import { BaseTemplateInputs, BaseTemplateCreator } from './BaseTemplateCreator'; + +export class Renderer { + /** The default folder name for the views */ + private readonly DEFAULT_VIEWS_FOLDER = 'pages'; + + /** The view engine to use (Ex. EJS, Pug, etc...) */ + private engine: string; + + /** The path to the folder/directory that contains the view files (Ex. EJS files) */ + private viewsDir: string; + + /** + * Creates a new instance of the Renderer class + * + * Note, that if the base template doesn't exist and the engine is `ejs`, then a base template will be created automatically. + * + * @param viewsDir The path to the folder/directory that contains the view files + * @param engine The view engine to use (Ex. EJS, Pug, etc...) + * @param baseTemplateInputs The inputs to use for the base template (if the template is generated) + * @throws Error if the pages path is not a valid directory + */ + constructor(viewsDir: string = path.join(process.cwd(), this.DEFAULT_VIEWS_FOLDER), engine: string = 'ejs', baseTemplateInputs?: BaseTemplateInputs) { + // Verify the views directory exists and is a directory + if(!existsSync(viewsDir) || !statSync(viewsDir).isDirectory()) { + throw new Error('The views directory must be a valid directory'); + } + + this.viewsDir = path.resolve(viewsDir); + this.engine = engine; + + // We can only automatically generate a base template if the engine is `ejs` + // We also don't want to generate a base template if one already exists + if(this.engine === 'ejs' && !existsSync(path.resolve(this.viewsDir, 'base.ejs'))) { + BaseTemplateCreator.create(this.viewsDir, baseTemplateInputs); + } + } + + /** + * Sets up the views for the Express app + * + * @param app The Express app to setup the views for + */ + setup(app: Application) { + // Set the view engine to the appropriate value (ex. `ejs`) and to serve views from the view files directory + app.set('view engine', this.engine); + app.set('views', this.viewsDir); + } +} \ No newline at end of file diff --git a/src/Router.ts b/src/Router.ts new file mode 100644 index 0000000..dc63964 --- /dev/null +++ b/src/Router.ts @@ -0,0 +1,244 @@ +import path from 'path'; +import { Application } from 'express'; +import fse from 'fs-extra'; + +import { BaseController } from './controllers/BaseController'; + +/** + * The Router sets up the routes/endpoints for the App. + * + * More specifically, it does automatic detection of controllers (and their routes) within the specified folder. + * + * A controller is a class that extends the `BaseController` class and uses the `@Controller` decorator. + * Routes within a controller are defined by methods that use the `@GET` or `@POST` decorators. + */ +export class Router { + /** The default folder name for controllers */ + private readonly DEFAULT_CONTROLLERS_FOLDER = 'routes'; + + /** The path to the controllers folder */ + private controllersPath: string; + + /** + * Create a new Router + * + * @param controllersPath The path to the controllers folder (default is 'routes' in the current working directory) + * @throws Error if the controllers path is not a valid directory + */ + constructor(controllersPath: string = path.join(process.cwd(), this.DEFAULT_CONTROLLERS_FOLDER)) { + if(!fse.existsSync(controllersPath) || !fse.statSync(controllersPath).isDirectory()) { + throw new Error('The controllers path must be a valid directory'); + } + + this.controllersPath = path.resolve(controllersPath); + } + + /** + * Check if a file is a controller + * + * Note, if the specified "file" is a directory, this method will recurse into the directory and return all controllers found in the directory. + * + * @param file The file to check if it is a controller + * @param folder The folder that the file is in + * @returns The controller class (or list of controller classes) in the "file" or null if the file is not a controller + */ + private async checkIfControllerFile(file: string, folder: string): Promise { + // Get the path to the specific file + const controllerPath = path.join(folder, file); + + // Check if the path is a directory/folder + const stats = await fse.stat(controllerPath); + if(stats.isDirectory()) { + // Recurse into the directory + const controllersFoundInDir = await Promise.all((await fse.readdir(controllerPath)).map(file => this.checkIfControllerFile(file, controllerPath))); + const controllersInDir = controllersFoundInDir.filter(controller => controller !== null); + return controllersInDir.length === 0 ? null : controllersInDir.flat(); + } + + // We only want to load JavaScript files (Note, Typescript files get compiled to JavaScript files) + if(!controllerPath.endsWith('.js')) { + return null; + } + + // Load the file as a module + const controllerModule = require(controllerPath); + + console.log(`${controllerPath} loaded successfully: ${JSON.stringify(Object.keys(controllerModule))} exports found.`) + + // Get all the classes in the module that are controllers + // + // Note: We filter by if the class extends the `BaseController` class. + // This is because native JavaScript doesn't have decorators (which are used to mark a class as a controller). + // Consequently, we enforce that all controllers in addition to using the decorator must extend the `BaseController` class so that we can identify them. + // Further, note we can't do this as part of the decorator because of the way decorators get transpiled to JavaScript. + const controllers = Object.keys(controllerModule) + .filter(key => typeof controllerModule[key] === 'function' && /^\s*class\s+/.test(controllerModule[key].toString())) + .filter(exportedClassName => controllerModule[exportedClassName].prototype instanceof BaseController) + .map(controllerClassName => { + return controllerModule[controllerClassName]; + }); + + switch(controllers.length) { + case 0: + return null; + case 1: + return controllers[0]; + default: + return controllers.flat(); + } + } + + /** + * Get the routes in a controller + * + * A route is defined by a method in the controller that has the `@GET` or `@POST` decorator. + * + * @param controller The controller to get the routes from + * @returns The routes in the controller + */ + private getRoutesInController(controller: any) { + // Loop over all the methods in the provided class looking for methods that use the GET decorator + const pathsGET = Object.getOwnPropertyNames(controller.prototype) + // Find all methods that have a Get metadata key (GET decorator) + .filter((method) => Reflect.getMetadata('Get', controller.prototype, method)) + .map((method) => { + // Get the method + const fn = controller.prototype[method]; + + // Get the path + const path = Reflect.getMetadata('Get', controller.prototype, method) as string; + + return path; + }); + + // Loop over all the methods in the provided class looking for methods that use the POST decorator + const pathsPOST = Object.getOwnPropertyNames(controller.prototype) + // Find all methods that have a Post metadata key (POST decorator) + .filter((method) => Reflect.getMetadata('Post', controller.prototype, method)) + .map((method) => { + // Get the method + const fn = controller.prototype[method]; + + // Get the metadata object (which contains a path and middleware) + const postRoute = Reflect.getMetadata('Post', controller.prototype, method); + const path = postRoute.path; + + return path; + }); + + return { + GET: pathsGET, + POST: pathsPOST + } + } + + /** + * Setup the controllers for the application + * + * This loops over all the "files" in the directory (this includes subdirectories) and finds any controllers. + * A controller being a class that extends the BaseController class and uses the `@Controller` decorator. + * Once it knows the controllers it adds the routes they contain to the Express app. + * Routes are defined by methods in the controller that have the `@GET` or `@POST` decorators. + * + * @param app The Express app to add the routes to + */ + private async setupControllers(app: Application) { + // Get the list of files in the controllers folder + const files = await fse.readdir(this.controllersPath); + + // Get the controller classes from the files + const loadedControllers = (await Promise.all( + files.map(file => this.checkIfControllerFile(file, this.controllersPath/*, app*/)) + )).filter(controller => controller !== null); + + const addedRoutes: string[] = []; + + // Get all controllers that have the child controller decorator + const controllersWithChildControllers = loadedControllers.filter(controller => Reflect.getMetadata('ChildController', controller) !== undefined); + + // Get all those controllers that are designated as children controllers by parent controllers + const childrenControllers = controllersWithChildControllers.map(controller => Reflect.getMetadata('ChildController', controller)); + + // Get the exclusive set of controllers that don't fit in the group of controllers with child controllers OR a child controller themselves + const controllersWithoutChildren = loadedControllers.filter(controller => !controllersWithChildControllers.includes(controller) && !childrenControllers.includes(controller)); + + // Take the list of controllers with child controllers and the list without children (that excludes children themselves) + // And forma a list that has all child controllers remove + const topLevelControllers = [...controllersWithChildControllers, ...controllersWithoutChildren]; + + // Add the routes for the child controllers to the list of added routes WITHOUT calling the setup method (as this is called in the parent controller's setup method) + childrenControllers + .forEach(decoratedController => { + if(Array.isArray(decoratedController)) { + decoratedController.forEach(decoratedControllerCls => { + const controller = Reflect.getMetadata('originalClass', decoratedControllerCls); + + const routes = this.getRoutesInController(controller); + routes.GET.forEach((path: string) => { + addedRoutes.push(`GET ${path} from ${(new controller()).constructor.name}`); + }); + routes.POST.forEach((path: string) => { + addedRoutes.push(`POST ${path} from ${(new controller()).constructor.name}`); + }); + }); + } + else { + const controller = Reflect.getMetadata('originalClass', decoratedController); + + const routes = this.getRoutesInController(controller); + routes.GET.forEach((path: string) => { + addedRoutes.push(`GET ${path} from ${(new controller()).constructor.name}`); + }); + routes.POST.forEach((path: string) => { + addedRoutes.push(`POST ${path} from ${(new controller()).constructor.name}`); + }); + } + }); + + // Add the routes for the top level controllers to the list of added routes and call the setup method + topLevelControllers + .forEach(decoratedController => { + if(Array.isArray(decoratedController)) { + decoratedController.forEach(decoratedControllerCls => { + const controller = Reflect.getMetadata('originalClass', decoratedControllerCls); + + const routes = this.getRoutesInController(controller); + routes.GET.forEach((path: string) => { + addedRoutes.push(`GET ${path} from ${(new controller()).constructor.name}`); + }); + routes.POST.forEach((path: string) => { + addedRoutes.push(`POST ${path} from ${(new controller()).constructor.name}`); + }); + + decoratedControllerCls.setup(app); + }); + } + else { + const controller = Reflect.getMetadata('originalClass', decoratedController); + + const routes = this.getRoutesInController(controller); + routes.GET.forEach((path: string) => { + addedRoutes.push(`GET ${path} from ${(new controller()).constructor.name}`); + }); + routes.POST.forEach((path: string) => { + addedRoutes.push(`POST ${path} from ${(new controller()).constructor.name}`); + }); + (decoratedController as any).setup(app); + } + }); + + console.log('Routes added:'); + addedRoutes.forEach(route => console.log('\t' + route)); + } + + /** + * Setup the routes for the application + * + * @param app The Express app to add the routes to + */ + async setup(app: Application) { + console.log('Setting up routes...') + + await this.setupControllers(app); + } +} \ No newline at end of file diff --git a/src/StaticFileResolver.ts b/src/StaticFileResolver.ts new file mode 100644 index 0000000..eef2932 --- /dev/null +++ b/src/StaticFileResolver.ts @@ -0,0 +1,47 @@ +import { existsSync, statSync } from 'fs'; +import path from 'path'; +import express, { Application } from 'express'; + +/** + * A class that wraps around Express's static file serving functionality. + * + * This is mostly future proofing if additional logic/functionality around serving static files becomes necessary. + */ +export class StaticFileResolver { + /** The default folder name for the static files */ + private readonly DEFAULT_STATIC_FOLDER = 'static'; + + /** + * The path to the folder/directory that contains the static files + * + * The folder should contain the following sub-folders at minimum: + * - css + * - js + * - img + */ + private staticFilesDir: string; + + /** + * Create a new instance of the StaticFileResolver + * + * @param staticFilesDir The path to the folder/directory that contains the static files. Defaults to the 'static' folder in the current working directory. + * @throws Error if the staticFilesDir is not a valid directory + */ + constructor(staticFilesDir: string = path.join(process.cwd(), this.DEFAULT_STATIC_FOLDER)) { + if(!existsSync(staticFilesDir) || !statSync(staticFilesDir).isDirectory()) { + throw new Error('The static files path must be a valid directory'); + } + + this.staticFilesDir = path.resolve(staticFilesDir); + } + + /** + * Setup the Express app to serve static files from the static directory + * + * @param app The Express application to setup the static file serving on. + */ + setup(app: Application) { + // Serve static files from the static directory + app.use(express.static(this.staticFilesDir)); + } +} \ No newline at end of file diff --git a/src/bin/create-project.ts b/src/bin/create-project.ts new file mode 100644 index 0000000..d187b20 --- /dev/null +++ b/src/bin/create-project.ts @@ -0,0 +1,75 @@ +import fs from 'fs'; +import { exec } from 'child_process'; + +function writePackageJson() { + // Run `npm init` and appropriate `npm install` commands + exec('npm init -y', (error, stdout, stderr) => { console.log(stdout); }); + exec('npm install --save express', (error, stdout, stderr) => { console.log(stdout); }); + exec('npm install --save-dev @types/express @types/node gulp gulp-typescript nodemon ts-node typescript', (error, stdout, stderr) => { console.log(stdout); }); + + // Add build script section to package.json + const packageJson = JSON.parse(fs.readFileSync('package.json').toString()); + if(typeof packageJson.scripts === 'undefined') { + packageJson.scripts = {}; + } + if(typeof packageJson.scripts.start === 'undefined') { + packageJson.scripts.start = 'node dist/server.js'; + } + if(typeof packageJson.scripts.build === 'undefined') { + packageJson.scripts.dev = 'gulp'; + } + + fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2)); +} + +function createControllersDir() { + // Create a routes folder to keep all controller files and copy in the example HomeRoutes file + fs.mkdirSync('src/routes'); + fs.copyFileSync('examples/HomeRoutes.ts', 'src/routes/HomeRoutes.ts'); +} + +function createStaticFilesDir() { + // Create a static folder to keep all static files (css, js, images, etc...) and copy in the example stylesheet + fs.mkdirSync('static'); + + fs.mkdirSync('static/css'); + fs.copyFileSync('exmples/styles.css', 'src/static/css/styles.css') + + fs.mkdirSync('static/js'); + + fs.mkdirSync('static/img'); +} + +function createViewsDir() { + fs.mkdirSync('pages'); + fs.copyFileSync('examples/base.ejs', 'src/pages/base.ejs'); +} + +function createSrcDir() { + // Create a source folder to keep all source files + fs.mkdirSync('src'); + + // Copy the example server file to the source folder (starts the app/server) + fs.copyFileSync('examples/server.ts', 'src/server.ts') + + createControllersDir(); + + createStaticFilesDir(); + + createViewsDir(); +} + +// Create a package.json file with the necessary dependencies +writePackageJson(); + +// Create a source folder to keep all source files in +createSrcDir(); + +// Copy the example gulpfile to the root of the project +fs.copyFileSync('examples/gulpfile.mjs', 'gulpfile.mjs'); + +// Run the build script to compile the project +exec('npm run build', (error, stdout, stderr) => { console.log(stdout); }); + +// Start the server +exec('npm run start', (error, stdout, stderr) => { console.log(stdout); }); \ No newline at end of file diff --git a/src/controllers/BaseController.ts b/src/controllers/BaseController.ts new file mode 100644 index 0000000..10831d5 --- /dev/null +++ b/src/controllers/BaseController.ts @@ -0,0 +1,5 @@ +import { Application } from 'express'; + +export abstract class BaseController { + static setup(app: Application) {} +} \ No newline at end of file diff --git a/src/controllers/index.ts b/src/controllers/index.ts new file mode 100644 index 0000000..a96f133 --- /dev/null +++ b/src/controllers/index.ts @@ -0,0 +1,3 @@ +import { BaseController } from './BaseController'; + +export { BaseController }; \ No newline at end of file diff --git a/src/decorators/ChildController.ts b/src/decorators/ChildController.ts new file mode 100644 index 0000000..680c42e --- /dev/null +++ b/src/decorators/ChildController.ts @@ -0,0 +1,8 @@ +import { BaseController } from '../controllers/BaseController'; + +export function ChildController(childController: BaseController | BaseController[]) { + return function (target: T) { + // Define a metadata key for the child controller + Reflect.defineMetadata('ChildController', childController, target); + }; +} \ No newline at end of file diff --git a/src/decorators/Controller.ts b/src/decorators/Controller.ts new file mode 100644 index 0000000..5bd99de --- /dev/null +++ b/src/decorators/Controller.ts @@ -0,0 +1,95 @@ +import { Application, NextFunction } from 'express'; + +import { BaseController } from '../controllers/BaseController'; + +/** + * Class decorator to "fill in" the setup method which is called on server startup and connects the methods/functions and their desired paths in the express app. + * + * That is, this is largely for the convenience of controller writers/developers so that they don't have to manually create a `setup` method in their controller classes. + * + * @example + * The following example show how to use the Controller decorator to setup a path `/path` with a GET and POST method. + * ```ts + * import { Request, Response } from 'express'; + * + * import { Controller } from '../decorators/Controller'; + * import { GET } from '../decorators/GET'; + * import { POST } from '../decorators/POST'; + * + * import { BaseController } from './BaseController'; + * + * @Controller() + * export class MyController extends BaseController { + * @GET('/path') + * private myGetMethod(req: Request, res: Response) {} + * + * @POST('/path', express.json()) + * private myPostMethod(req: Request, res: Response) {} + * } + * ``` + */ +export function Controller() { + // Technically the function we return here with the target parameter is the actual decorator itself but we wrap it in case we ever want to add parameters to the decorator + return function(target: T){ + Reflect.defineMetadata('originalClass', target, target); + // We extend the class that is decorated and override the setup method to automatically setup the routes + return class extends target { + /** + * Setup the routes for the controller. + * + * @param app The express application to setup the routes on. + */ + static setup(app: Application) { + // If the decorated class is also decorated with the `@ChildController` decorator, + // then we call the child controller's setup method as well. + // + // Currently, there is very little linkage between the decorated controller class and the child controller class(es). + // Though in the future this may look more like forcing these to be on sub-paths of the parent controller etc... + const childControllers = Reflect.getMetadata('ChildController', target); + if(typeof childControllers !== 'undefined') { + if(Array.isArray(childControllers)) { + childControllers.forEach((childController) => { + childController.setup(app); + }); + } else { + childControllers.setup(app); + } + } + + const controller = new (Reflect.getMetadata('originalClass', target))(); + + // Loop over all the methods in the decorated class looking for methods that use the GET decorator + Object.getOwnPropertyNames(target.prototype) + // Find all methods that have a Get metadata key (GET decorator) + .filter((method) => Reflect.getMetadata('Get', target.prototype, method)) + .map((method) => { + // Get the method + const fn = target.prototype[method]; + + // Get the path + const path = Reflect.getMetadata('Get', target.prototype, method); + + // Bind the method to the class instance + app.get(path, fn.bind(controller)); + }); + + // Loop over all the methods in the decorated class looking for methods that use the POST decorator + Object.getOwnPropertyNames(target.prototype) + // Find all methods that have a Post metadata key (POST decorator) + .filter((method) => Reflect.getMetadata('Post', target.prototype, method)) + .map((method) => { + // Get the method + const fn = target.prototype[method]; + + // Get the metadata object (which contains a path and middleware) + const postRoute = Reflect.getMetadata('Post', target.prototype, method); + const path = postRoute.path; + const middleware: NextFunction[] = postRoute.middleware; + + // Bind the method to the class instance + app.post(path, ...middleware, fn.bind(controller)); + }); + } + } + } +} \ No newline at end of file diff --git a/src/decorators/GET.ts b/src/decorators/GET.ts new file mode 100644 index 0000000..6f9faed --- /dev/null +++ b/src/decorators/GET.ts @@ -0,0 +1,14 @@ +/** + * Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator. + * + * This in conjunction with the `@Controller` decorator will automatically setup a GET route that executes the decorated method. + * The specific path for the route is defined by the path parameter. + * + * @param path The path for the GET route. + */ +export function GET(path: string) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // Define a `Get` metadata key with the path as the value on the target's propertyKey + Reflect.defineMetadata('Get', path, target, propertyKey); + }; +} \ No newline at end of file diff --git a/src/decorators/POST.ts b/src/decorators/POST.ts new file mode 100644 index 0000000..b78dffb --- /dev/null +++ b/src/decorators/POST.ts @@ -0,0 +1,19 @@ +import { NextFunction } from 'express'; +import { NextHandleFunction } from 'connect'; + +/** + * Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator. + * + * This in conjunction with the `@Controller` decorator will automatically setup a POST route that executes the decorated method. + * The specific path for the route is defined by the path parameter. + * Controller authors can also specify middleware to be used with the POST route (because a middleware is commonly required to parse the body of a POST request). + * + * @param path The path for the GET route. + * @param middleware The middleware to use with the POST route. + */ +export function POST(path: string, ...middleware: (NextHandleFunction | NextFunction)[]) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // Define a `Get` metadata key with the path as the value on the target's propertyKey + Reflect.defineMetadata('Post', { path, middleware }, target, propertyKey); + }; +} \ No newline at end of file diff --git a/src/decorators/Page.ts b/src/decorators/Page.ts new file mode 100644 index 0000000..06337fd --- /dev/null +++ b/src/decorators/Page.ts @@ -0,0 +1,45 @@ +import { Request, Response } from 'express'; + +export function Page(title: string, page: string, extraScripts: (string | { script: string, defer: boolean })[] = [], extraStyles: string[] = [], ...otherParams: any[]) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const original = descriptor.value; + + descriptor.value = async function (req: Request, res: Response) { + // We run the original here so that if the decorated method has specific checks it needs to make (ex. if the ID of whatever actually exists) it can make them before rendering the page + // + // !!!IMPORTANT!!! + // This currently is kind of a hacky solution, because we bind to the target, which is the prototype rather than the instance. + // This means instance data is not properly available/accessible within the decorated method. + // However, you can use the `this` keyword to access other methods on the class, etc... + // + // The above (this approach) is necessary at time of writing because the `this` (which should be bound to for the instance) seems to be undefined within the context of the decorator function here for some reason + // Ideally, would dive into this and do it properly but this seems to be good enough for now. + const output = await original.bind(target)(req, res); + + // If the output is false, we don't want to render the page + // + // This allows the decorated method to handle the response itself + // Ex. Logging in, if the user is already logged in we want to redirect them, not render the login page again + if(typeof output === 'boolean' && !output) { + return; + } + + const renderParams: { [key: string]: any } = { + title: title, + page: page, + extraStyles: extraStyles, + extraScripts: extraScripts, + ...otherParams + }; + + // If the decorated method's output is an object, we want to merge it with the renderParams + if(typeof output === 'object') { + Object.entries(output).forEach((entry) => { + renderParams[entry[0]] = entry[1]; + }); + } + + res.render('base', renderParams); + } + } +} \ No newline at end of file diff --git a/src/decorators/index.ts b/src/decorators/index.ts new file mode 100644 index 0000000..dd186ee --- /dev/null +++ b/src/decorators/index.ts @@ -0,0 +1,13 @@ +import { Controller } from './Controller'; +import { ChildController } from './ChildController'; +import { Page } from './Page'; +import { GET } from './GET'; +import { POST } from './POST'; + +export { + Controller, + ChildController, + Page, + GET, + POST +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..f3214ad --- /dev/null +++ b/src/index.ts @@ -0,0 +1,16 @@ +import { App } from './App'; +import { Initializer } from './Initializer'; +import { Router } from './Router'; +import { Renderer } from './Renderer'; +import { StaticFileResolver } from './StaticFileResolver'; + +export { + App, + Initializer, + Router, + Renderer, + StaticFileResolver +}; +export * from './controllers'; +export * from './decorators'; +export * from './middlewares'; \ No newline at end of file diff --git a/src/middlewares/GlobalTemplateValuesMiddleware.ts b/src/middlewares/GlobalTemplateValuesMiddleware.ts new file mode 100644 index 0000000..5108557 --- /dev/null +++ b/src/middlewares/GlobalTemplateValuesMiddleware.ts @@ -0,0 +1,97 @@ +import { Request, Response, NextFunction, RequestHandler } from 'express'; + +/** + * Class that creates middleware to set global template values for all pages. + */ +class GlobalTemplateValuesMiddleware { + /** The description of the website. */ + private description?: string; + + /** The keywords for the website. */ + private keywords?: string; + + /** The author of the website. */ + private author?: string; + + /** The prefix for the title of the website. */ + private titlePrefix?: string; + + /** The suffix for the title of the website. */ + private titleSuffix?: string; + + /** Other values to set. */ + private otherValues: { [key: string]: string } = {}; + + /** + * Constructor for the GlobalTemplateValuesMiddleware class. + * + * @param options Options for the middleware. + * @param options.description The description of the website. + * @param options.keywords The keywords for the website. + * @param options.author The author of the website. + * @param options.titlePrefix The prefix for the title of the website. + * @param options.titleSuffix The suffix for the title of the website. + * @param options[key] Any other values to set. + */ + constructor(options: { description?: string, keywords?: string, author?: string, titlePrefix?: string, titleSuffix?: string, [key: string]: string | undefined }) { + this.description = options.description; + this.keywords = options.keywords; + this.author = options.author; + this.titlePrefix = options.titlePrefix; + this.titleSuffix = options.titleSuffix; + Object.keys(options).forEach(key => { + if(!['company', 'description', 'keywords', 'author', 'titlePrefix', 'titleSuffix'].includes(key)) { + this.otherValues[key] = options[key] as string; + } + }) + } + + /** + * Creates the middleware function. + * + * @returns The middleware function. + */ + middleware(): RequestHandler { + return async (req: Request, res: Response, next: NextFunction) => { + if(typeof this.description !== 'undefined') { + res.locals.description = this.description; + } + if(typeof this.keywords !== 'undefined') { + res.locals.keywords = this.keywords; + } + if(typeof this.author !== 'undefined') { + res.locals.author = this.author; + } + if(typeof this.titlePrefix !== 'undefined') { + res.locals.titlePrefix = this.titlePrefix; + } + if(typeof this.titleSuffix !== 'undefined') { + res.locals.titleSuffix = this.titleSuffix; + } + + Object.entries(this.otherValues).forEach(([key, value]: [string, string]) => { + res.locals[key] = value; + }); + + // Continue to the next middleware + next(); + } + } +} + +/** + * Middleware wrapper function to set global template values for all pages. + * + * @param options Options for the middleware. + * @param options.description The description of the website. + * @param options.keywords The keywords for the website. + * @param options.author The author of the website. + * @param options.titlePrefix The prefix for the title of the website. + * @param options.titleSuffix The suffix for the title of the website. + * @returns The middleware function. + */ +export function globalTemplateValues(options: { description?: string, keywords?: string, author?: string, titlePrefix?: string, titleSuffix?: string, [key: string]: string | undefined }) { + const instance = new GlobalTemplateValuesMiddleware({ ...options }); + + return instance.middleware.bind(instance); +} \ No newline at end of file diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts new file mode 100644 index 0000000..bd7c34e --- /dev/null +++ b/src/middlewares/index.ts @@ -0,0 +1,5 @@ +import { globalTemplateValues } from './GlobalTemplateValuesMiddleware'; + +export { + globalTemplateValues +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b4c2125 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,113 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": [ "DOM", "ES2022", "ESNext.AsyncIterable" ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "CommonJS", /* Specify what module code is generated. */ + "rootDir": "src", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "baseUrl": ".", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "dist/", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + "strictPropertyInitialization": false, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "exclude": [ "node_modules" ], + "include": [ + "./src/**/*.ts" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..369af44 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,984 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@asamuzakjp/css-color@^2.8.2": + version "2.8.3" + resolved "https://registry.yarnpkg.com/@asamuzakjp/css-color/-/css-color-2.8.3.tgz#665f0f5e8edb95d8f543847529e30fe5cc437ef7" + integrity sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw== + dependencies: + "@csstools/css-calc" "^2.1.1" + "@csstools/css-color-parser" "^3.0.7" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + lru-cache "^10.4.3" + +"@csstools/color-helpers@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.0.1.tgz#829f1c76f5800b79c51c709e2f36821b728e0e10" + integrity sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA== + +"@csstools/css-calc@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.1.tgz#a7dbc66627f5cf458d42aed14bda0d3860562383" + integrity sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag== + +"@csstools/css-color-parser@^3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.0.7.tgz#442d61d58e54ad258d52c309a787fceb33906484" + integrity sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA== + dependencies: + "@csstools/color-helpers" "^5.0.1" + "@csstools/css-calc" "^2.1.1" + +"@csstools/css-parser-algorithms@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz#74426e93bd1c4dcab3e441f5cc7ba4fb35d94356" + integrity sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A== + +"@csstools/css-tokenizer@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz#a5502c8539265fecbd873c1e395a890339f119c2" + integrity sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw== + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.19.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" + integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^4.17.21": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/fs-extra@^11.0.4": + version "11.0.4" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.4.tgz#e16a863bb8843fba8c5004362b5a73e17becca45" + integrity sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ== + dependencies: + "@types/jsonfile" "*" + "@types/node" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/jsdom@^21.1.7": + version "21.1.7" + resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-21.1.7.tgz#9edcb09e0b07ce876e7833922d3274149c898cfa" + integrity sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA== + dependencies: + "@types/node" "*" + "@types/tough-cookie" "*" + parse5 "^7.0.0" + +"@types/jsonfile@*": + version "6.1.4" + resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.4.tgz#614afec1a1164e7d670b4a7ad64df3e7beb7b702" + integrity sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ== + dependencies: + "@types/node" "*" + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node@*": + version "22.13.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.4.tgz#3fe454d77cd4a2d73c214008b3e331bfaaf5038a" + integrity sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg== + dependencies: + undici-types "~6.20.0" + +"@types/node@^20.12.12": + version "20.17.19" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.19.tgz#0f2869555719bef266ca6e1827fcdca903c1a697" + integrity sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A== + dependencies: + undici-types "~6.19.2" + +"@types/qs@*": + version "6.9.18" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.18.tgz#877292caa91f7c1b213032b34626505b746624c2" + integrity sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +agent-base@^7.1.0, agent-base@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1" + integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind-apply-helpers@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681" + integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA== + dependencies: + call-bind-apply-helpers "^1.0.1" + get-intrinsic "^1.2.6" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + +cssstyle@^4.0.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.2.1.tgz#5142782410fea95db66fb68147714a652a7c2381" + integrity sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw== + dependencies: + "@asamuzakjp/css-color" "^2.8.2" + rrweb-cssom "^0.8.0" + +data-urls@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde" + integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg== + dependencies: + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.3.4: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + +decimal.js@^10.4.3: + version "10.5.0" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.5.0.tgz#0f371c7cf6c4898ce0afb09836db73cd82010f22" + integrity sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +express@^4.19.2: + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.12" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +form-data@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c" + integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^11.2.0: + version "11.3.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d" + integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.2.5, get-intrinsic@^1.2.6: + version "1.2.7" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044" + integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + function-bind "^1.1.2" + get-proto "^1.0.0" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +html-encoding-sniffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448" + integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ== + dependencies: + whatwg-encoding "^3.1.1" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy-agent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + +https-proxy-agent@^7.0.5: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + +jsdom@^24.1.0: + version "24.1.3" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-24.1.3.tgz#88e4a07cb9dd21067514a619e9f17b090a394a9f" + integrity sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ== + dependencies: + cssstyle "^4.0.1" + data-urls "^5.0.0" + decimal.js "^10.4.3" + form-data "^4.0.0" + html-encoding-sniffer "^4.0.0" + http-proxy-agent "^7.0.2" + https-proxy-agent "^7.0.5" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.12" + parse5 "^7.1.2" + rrweb-cssom "^0.7.1" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.4" + w3c-xmlserializer "^5.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^3.1.1" + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + ws "^8.18.0" + xml-name-validator "^5.0.0" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +lru-cache@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +nwsapi@^2.2.12: + version "2.2.16" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.16.tgz#177760bba02c351df1d2644e220c31dfec8cdb43" + integrity sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ== + +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +parse5@^7.0.0, parse5@^7.1.2: + version "7.2.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.2.1.tgz#8928f55915e6125f430cc44309765bf17556a33a" + integrity sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ== + dependencies: + entities "^4.5.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +psl@^1.1.33: + version "1.15.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.15.0.tgz#bdace31896f1d97cec6a79e8224898ce93d974c6" + integrity sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w== + dependencies: + punycode "^2.3.1" + +punycode@^2.1.1, punycode@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +reflect-metadata@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" + integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +rrweb-cssom@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz#c73451a484b86dd7cfb1e0b2898df4b703183e4b" + integrity sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg== + +rrweb-cssom@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz#3021d1b4352fbf3b614aaeed0bc0d5739abe0bc2" + integrity sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw== + +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tough-cookie@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + +tr46@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.0.0.tgz#3b46d583613ec7283020d79019f1335723801cec" + integrity sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g== + dependencies: + punycode "^2.3.1" + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^5.4.5: + version "5.7.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e" + integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +w3c-xmlserializer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" + integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA== + dependencies: + xml-name-validator "^5.0.0" + +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +whatwg-encoding@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" + integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== + dependencies: + iconv-lite "0.6.3" + +whatwg-mimetype@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" + integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== + +whatwg-url@^14.0.0: + version "14.1.1" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.1.1.tgz#ce71e240c61541315833b5cdafd139a479e47058" + integrity sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ== + dependencies: + tr46 "^5.0.0" + webidl-conversions "^7.0.0" + +ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +xml-name-validator@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673" + integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==