diff --git a/azure-functions-nodejs-extensions-express/README.md b/azure-functions-nodejs-extensions-express/README.md new file mode 100644 index 0000000..21b64d0 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/README.md @@ -0,0 +1,141 @@ +# @azure/functions-extensions-express + +Express integration for Azure Functions - run Express apps natively within Azure Functions with full middleware and streaming support. + +## Features + +- **Native Express** - Express runs as a real HTTP server, not an adapter +- **Full Middleware Support** - All Express middleware works without modification +- **Streaming** - Server-Sent Events and large responses stream natively +- **Zero Code Changes** - Use your existing Express app as-is + +## Installation + +```bash +npm install @azure/functions-extensions-express express +``` + +## Quick Start + +```typescript +import express from 'express'; +import { expressApp } from '@azure/functions-extensions-express'; + +// Create your Express app +const app = express(); + +app.get('/health', (req, res) => { + res.json({ status: 'ok' }); +}); + +app.get('/users', (req, res) => { + res.json([ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' } + ]); +}); + +// Register with Azure Functions +expressApp('api', app, { + route: 'api/{*path}', + basePath: '/api' +}); +``` + +## Streaming Example (Server-Sent Events) + +```typescript +import express from 'express'; +import { expressApp } from '@azure/functions-extensions-express'; + +const app = express(); + +app.get('/stream', (req, res) => { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + let count = 0; + const interval = setInterval(() => { + res.write(`data: ${JSON.stringify({ event: ++count })}\n\n`); + if (count >= 10) { + clearInterval(interval); + res.end(); + } + }, 1000); +}); + +expressApp('api', app, { + route: 'api/{*path}', + basePath: '/api', + enableStreaming: true +}); +``` + +## API Reference + +### `expressApp(name, app, options?)` + +Registers an Express application as an Azure Function. + +**Parameters:** +- `name` - Function name (string) +- `app` - Express application +- `options` - Configuration options (optional) + +**Options:** +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `route` | string | `'{*path}'` | Azure Functions route pattern | +| `basePath` | string | `''` | Path prefix to strip before routing to Express | +| `methods` | string[] | All methods | HTTP methods to accept | +| `authLevel` | string | `'anonymous'` | Azure Functions auth level | +| `enableStreaming` | boolean | `true` | Enable streaming responses | +| `timeout` | number | `30000` | Request timeout (ms) | +| `port` | number | `0` | Express server port (0 = auto) | + +**Returns:** `ExpressServer` - Server instance for lifecycle management + +### `stopAllServers()` + +Stops all Express servers. Use during app termination. + +```typescript +import { app } from '@azure/functions'; +import { stopAllServers } from '@azure/functions-extensions-express'; + +app.hook.appTerminate(async () => { + await stopAllServers(); +}); +``` + +### `getServer(name)` + +Gets an Express server by function name. + +```typescript +import { getServer } from '@azure/functions-extensions-express'; + +const server = getServer('api'); +console.log(`Server running on port ${server?.port}`); +``` + +## How It Works + +This extension runs Express as a real HTTP server on localhost within the Azure Functions worker process. When an HTTP request arrives at Azure Functions: + +1. The request is proxied to the Express server +2. Express handles it with full middleware support +3. The response is streamed back to the client + +This architecture provides true Express compatibility without adaptation layers. + +## Requirements + +- Node.js 18+ +- @azure/functions ^4.0.0 +- Express ^4.0.0 or ^5.0.0 + +## License + +MIT diff --git a/azure-functions-nodejs-extensions-express/package-lock.json b/azure-functions-nodejs-extensions-express/package-lock.json new file mode 100644 index 0000000..83789f6 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/package-lock.json @@ -0,0 +1,83 @@ +{ + "name": "@azure/functions-extensions-express", + "version": "0.1.1-preview", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@azure/functions-extensions-express", + "version": "0.1.1-preview", + "license": "MIT", + "dependencies": { + "@azure/functions": "^4.11.0", + "@types/node": "^18.0.0", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "@azure/functions": "^4.11.0" + } + }, + "node_modules/@azure/functions": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.11.2.tgz", + "integrity": "sha512-U7qpPo0pUxDfdP3Q8gO5GLtust94nh8+RtIUvEKE4qU9yuDhL2vU1zzanuzkaV2j/TFv+EEmN8QDtchAgpeffw==", + "license": "MIT", + "dependencies": { + "@azure/functions-extensions-base": "0.2.0", + "cookie": "^0.7.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@azure/functions-extensions-base": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@azure/functions-extensions-base/-/functions-extensions-base-0.2.0.tgz", + "integrity": "sha512-ncCkHBNQYJa93dBIh+toH0v1iSgCzSo9tr94s6SMBe7DPWREkaWh8cq33A5P4rPSFX1g5W+3SPvIzDr/6/VOWQ==", + "license": "MIT", + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + } + } +} diff --git a/azure-functions-nodejs-extensions-express/package.json b/azure-functions-nodejs-extensions-express/package.json new file mode 100644 index 0000000..438cee1 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/package.json @@ -0,0 +1,39 @@ +{ + "name": "@azure/functions-extensions-express", + "version": "0.1.1-preview", + "description": "Express integration for Azure Functions - run Express apps natively in Azure Functions", + "main": "dist/index.js", + "types": "types/index.d.ts", + "license": "MIT", + "author": "Microsoft Corporation", + "homepage": "https://github.com/Azure/azure-functions-nodejs-extensions-express#readme", + "repository": { + "type": "git", + "url": "https://github.com/Azure/azure-functions-nodejs-extensions.git", + "directory": "/azure-functions-nodejs-extensions-express" + }, + "keywords": [ + "azure", + "functions", + "serverless", + "express", + "nodejs" + ], + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "lint": "eslint src/**/*.ts", + "test": "mocha --require ts-node/register test/**/*.test.ts" + }, + "engines": { + "node": ">=18.0" + }, + "dependencies": { + "@azure/functions": "^4.11.0", + "@types/node": "^18.0.0", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "@azure/functions": "^4.11.0" + } +} diff --git a/azure-functions-nodejs-extensions-express/samples/express-getting-started/.vscode/extensions.json b/azure-functions-nodejs-extensions-express/samples/express-getting-started/.vscode/extensions.json new file mode 100644 index 0000000..036c408 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/samples/express-getting-started/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions" + ] +} \ No newline at end of file diff --git a/azure-functions-nodejs-extensions-express/samples/express-getting-started/.vscode/launch.json b/azure-functions-nodejs-extensions-express/samples/express-getting-started/.vscode/launch.json new file mode 100644 index 0000000..b5b6e34 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/samples/express-getting-started/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to Node Functions", + "type": "node", + "request": "attach", + "restart": true, + "port": 9229, + "preLaunchTask": "func: host start" + } + ] +} \ No newline at end of file diff --git a/azure-functions-nodejs-extensions-express/samples/express-getting-started/.vscode/settings.json b/azure-functions-nodejs-extensions-express/samples/express-getting-started/.vscode/settings.json new file mode 100644 index 0000000..012588f --- /dev/null +++ b/azure-functions-nodejs-extensions-express/samples/express-getting-started/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "azureFunctions.deploySubpath": ".", + "azureFunctions.postDeployTask": "npm install (functions)", + "azureFunctions.projectLanguage": "TypeScript", + "azureFunctions.projectRuntime": "~4", + "debug.internalConsoleOptions": "neverOpen", + "azureFunctions.projectLanguageModel": 4, + "azureFunctions.preDeployTask": "npm prune (functions)" +} \ No newline at end of file diff --git a/azure-functions-nodejs-extensions-express/samples/express-getting-started/.vscode/tasks.json b/azure-functions-nodejs-extensions-express/samples/express-getting-started/.vscode/tasks.json new file mode 100644 index 0000000..78b10af --- /dev/null +++ b/azure-functions-nodejs-extensions-express/samples/express-getting-started/.vscode/tasks.json @@ -0,0 +1,44 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "func", + "label": "func: host start", + "command": "host start", + "problemMatcher": "$func-node-watch", + "isBackground": true, + "dependsOn": "npm watch (functions)" + }, + { + "type": "shell", + "label": "npm build (functions)", + "command": "npm run build", + "dependsOn": "npm install (functions)", + "problemMatcher": "$tsc" + }, + { + "type": "shell", + "label": "npm watch (functions)", + "command": "npm run watch", + "dependsOn": "npm install (functions)", + "problemMatcher": "$tsc-watch", + "group": { + "kind": "build", + "isDefault": true + }, + "isBackground": true + }, + { + "type": "shell", + "label": "npm install (functions)", + "command": "npm install" + }, + { + "type": "shell", + "label": "npm prune (functions)", + "command": "npm prune --production", + "dependsOn": "npm build (functions)", + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/azure-functions-nodejs-extensions-express/samples/express-getting-started/README.md b/azure-functions-nodejs-extensions-express/samples/express-getting-started/README.md new file mode 100644 index 0000000..4ade660 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/samples/express-getting-started/README.md @@ -0,0 +1,192 @@ +# Express on Azure Functions - Getting Started + +This sample shows how to create a new Express API from scratch and host it on Azure Functions using [`@azure/functions-extensions-express`](../../README.md). It demonstrates CRUD routes, middleware, Server-Sent Events (SSE) streaming, and graceful shutdown — all with standard Express code. + +## Prerequisites + +- [Node.js](https://nodejs.org/) v18+ +- [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-tools) v4 +- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) (for deployment) + +## Quick Start + +```bash +# Install dependencies +npm install + +# Build and run locally +npm start +``` + +The Functions host will start and list every registered endpoint: + +``` +api-GET-health -> http://localhost:7071/health +api-GET-items -> http://localhost:7071/items +api-GET-items-id -> http://localhost:7071/items/:id +api-POST-items -> http://localhost:7071/items +api-PUT-items-id -> http://localhost:7071/items/:id +api-DELETE-items-id -> http://localhost:7071/items/:id +api-GET-stream -> http://localhost:7071/stream +``` + +## Project Structure + +``` +express-getting-started/ +├── src/ +│ └── functions/ +│ └── index.ts # Express app — start here! +├── scripts/ +│ ├── create-deploy-zip.js # Zip packaging for deployment +│ └── create-deploy-zip.ps1 +├── host.json # Azure Functions host config +├── local.settings.json # Local development settings +├── package.json +└── tsconfig.json +``` + +## How It Works + +### 1. Create your Express app + +Write regular Express code — middleware, routes, error handlers — exactly as you would in any Node.js project: + +```typescript +import express, { Request, Response, NextFunction } from 'express'; + +const expressApp = express(); +expressApp.use(express.json()); + +expressApp.get('/health', (_req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +expressApp.get('/items', (_req, res) => res.json(items)); +``` + +### 2. Register with Azure Functions + +A single call to `expressApp()` from `@azure/functions-extensions-express` auto-discovers your routes and registers each one as a separate Azure Function: + +```typescript +import { expressApp as registerExpress } from '@azure/functions-extensions-express'; + +const server = registerExpress('endpoint', expressApp, { + enableStreaming: true, +}); +``` + +### 3. Optional: graceful shutdown + +```typescript +import { app } from '@azure/functions'; + +app.hook.appTerminate(async () => { + await server.stop(); +}); +``` + +That's it — no wrappers, no adapters, just your regular Express code. + +## API Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | /health | Health check | +| GET | /items | List all items | +| GET | /items/:id | Get a single item | +| POST | /items | Create a new item | +| PUT | /items/:id | Update an item | +| DELETE | /items/:id | Delete an item | +| GET | /stream | Server-Sent Events stream | + +## Test the API + +```bash +# Health check +curl http://localhost:7071/health + +# List items +curl http://localhost:7071/items + +# Create item +curl -X POST http://localhost:7071/items \ + -H "Content-Type: application/json" \ + -d '{"name": "New Item", "description": "A new item"}' + +# Update item +curl -X PUT http://localhost:7071/items/1 \ + -H "Content-Type: application/json" \ + -d '{"name": "Updated Name"}' + +# Delete item +curl -X DELETE http://localhost:7071/items/1 + +# Streaming (Server-Sent Events) +curl -N http://localhost:7071/stream +``` + +## Deploy to Azure + +### Option A: Azure Functions Core Tools + +```bash +az login + +# Create a Function App (if not already created) +az functionapp create \ + --resource-group \ + --consumption-plan-location \ + --runtime node --runtime-version 20 \ + --functions-version 4 \ + --name \ + --storage-account + +# Publish +func azure functionapp publish +``` + +### Option B: Zip Deploy + +The project includes a packaging script that resolves local symlinks/junctions and produces a Linux-compatible zip: + +```bash +# Build first +npm run build + +# Create deploy.zip +node scripts/create-deploy-zip.js + +# Deploy +az functionapp deployment source config-zip \ + -g -n --src deploy.zip +``` + +## Adding More Features + +Add routes, middleware, and routers exactly as you would in any Express project: + +```typescript +// Authentication middleware +expressApp.use('/admin', authMiddleware); + +// Additional routes +expressApp.get('/users', userController.getAll); +expressApp.post('/orders', orderController.create); + +// Express Router for organization +const v1 = express.Router(); +v1.get('/products', productController.list); +expressApp.use('/v1', v1); +``` + +All new routes are automatically registered as Azure Functions on the next start. + +## Key Benefits + +- **No code changes** — Use your existing Express knowledge and code as-is +- **Full middleware support** — All Express middleware works without modification +- **Streaming** — SSE and large responses stream natively +- **Per-route functions** — Each Express route becomes its own Azure Function for independent scaling and monitoring +- **Serverless scaling** — Azure Functions handles scaling automatically diff --git a/azure-functions-nodejs-extensions-express/samples/express-getting-started/host.json b/azure-functions-nodejs-extensions-express/samples/express-getting-started/host.json new file mode 100644 index 0000000..61167e2 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/samples/express-getting-started/host.json @@ -0,0 +1,20 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensions": { + "http": { + "routePrefix": "" + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} diff --git a/azure-functions-nodejs-extensions-express/samples/express-getting-started/local.settings.json b/azure-functions-nodejs-extensions-express/samples/express-getting-started/local.settings.json new file mode 100644 index 0000000..a597af8 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/samples/express-getting-started/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "node", + "AzureWebJobsStorage": "UseDevelopmentStorage=true" + } +} diff --git a/azure-functions-nodejs-extensions-express/samples/express-getting-started/package-lock.json b/azure-functions-nodejs-extensions-express/samples/express-getting-started/package-lock.json new file mode 100644 index 0000000..e61d5ee --- /dev/null +++ b/azure-functions-nodejs-extensions-express/samples/express-getting-started/package-lock.json @@ -0,0 +1,2102 @@ +{ + "name": "express-getting-started", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "express-getting-started", + "version": "1.0.0", + "dependencies": { + "@azure/functions": "^4.11.0", + "@azure/functions-extensions-express": "0.1.1-preview", + "express": "^4.18.2" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.0.0", + "archiver": "^7.0.1", + "typescript": "^5.0.0" + } + }, + "../..": { + "name": "@azure/functions-extensions-express", + "version": "0.1.1-preview", + "license": "MIT", + "dependencies": { + "@azure/functions": "^4.11.0", + "@types/node": "^18.0.0", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "@azure/functions": "^4.11.0" + } + }, + "../../extensions/functions-extensions-express": { + "name": "@azure/functions-extensions-express", + "version": "0.1.0", + "extraneous": true, + "license": "MIT", + "devDependencies": { + "@azure/functions": "^4.0.0", + "@types/node": "^18.0.0", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "@azure/functions": "^4.0.0" + } + }, + "node_modules/@azure/functions": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.11.2.tgz", + "integrity": "sha512-U7qpPo0pUxDfdP3Q8gO5GLtust94nh8+RtIUvEKE4qU9yuDhL2vU1zzanuzkaV2j/TFv+EEmN8QDtchAgpeffw==", + "license": "MIT", + "dependencies": { + "@azure/functions-extensions-base": "0.2.0", + "cookie": "^0.7.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@azure/functions-extensions-base": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@azure/functions-extensions-base/-/functions-extensions-base-0.2.0.tgz", + "integrity": "sha512-ncCkHBNQYJa93dBIh+toH0v1iSgCzSo9tr94s6SMBe7DPWREkaWh8cq33A5P4rPSFX1g5W+3SPvIzDr/6/VOWQ==", + "license": "MIT", + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@azure/functions-extensions-express": { + "resolved": "../..", + "link": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz", + "integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz", + "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.10.0.tgz", + "integrity": "sha512-DOPZF/DDcDruKDA43cOw6e9Quq5daua7ygcAwJE/pKJsRWhgSSemi7qVNGE5kyDIxIeN1533G/zfbvWX7Wcb9w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", + "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "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.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "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.14.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" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "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.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/azure-functions-nodejs-extensions-express/samples/express-getting-started/package.json b/azure-functions-nodejs-extensions-express/samples/express-getting-started/package.json new file mode 100644 index 0000000..88a71ca --- /dev/null +++ b/azure-functions-nodejs-extensions-express/samples/express-getting-started/package.json @@ -0,0 +1,24 @@ +{ + "name": "express-getting-started", + "version": "1.0.0", + "description": "Getting started with Express on Azure Functions", + "main": "dist/src/functions/index.js", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "start": "func start", + "prestart": "npm run build", + "package": "node scripts/create-deploy-zip.js" + }, + "dependencies": { + "@azure/functions": "^4.11.0", + "@azure/functions-extensions-express": "0.1.1-preview", + "express": "^4.18.2" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.0.0", + "archiver": "^7.0.1", + "typescript": "^5.0.0" + } +} diff --git a/azure-functions-nodejs-extensions-express/samples/express-getting-started/scripts/create-deploy-zip.js b/azure-functions-nodejs-extensions-express/samples/express-getting-started/scripts/create-deploy-zip.js new file mode 100644 index 0000000..3d529c8 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/samples/express-getting-started/scripts/create-deploy-zip.js @@ -0,0 +1,192 @@ +/** + * Creates a deployment zip for Azure Functions. + * - Uses archiver to produce forward-slash paths (Linux compatible) + * - Resolves npm junctions/symlinks (e.g. local-path dependencies) + * - No temp directory needed — adds files directly to zip + */ +const fs = require('fs'); +const path = require('path'); +const archiver = require('archiver'); + +const projectRoot = path.resolve(__dirname, '..'); +const outputPath = path.join(projectRoot, 'deploy.zip'); + +// The local extension source (junction target) +const expressExtSource = path.resolve(projectRoot, '../../extensions/functions-express'); + +async function createDeployZip() { + console.log('Creating deployment zip for Azure Functions...\n'); + + if (fs.existsSync(outputPath)) { + fs.unlinkSync(outputPath); + } + + const output = fs.createWriteStream(outputPath); + const archive = archiver('zip', { zlib: { level: 6 } }); + + const promise = new Promise((resolve, reject) => { + output.on('close', () => { + const sizeMB = (archive.pointer() / (1024 * 1024)).toFixed(2); + console.log(`\nDone! deploy.zip = ${sizeMB} MB`); + console.log('\nDeploy with:'); + console.log(' az functionapp deployment source config-zip -g -n --src deploy.zip'); + resolve(); + }); + archive.on('error', reject); + archive.on('warning', (err) => { + if (err.code !== 'ENOENT') reject(err); + }); + }); + + archive.pipe(output); + + // 1) Add host.json and package.json + console.log('Adding host.json...'); + archive.file(path.join(projectRoot, 'host.json'), { name: 'host.json' }); + + console.log('Adding package.json...'); + archive.file(path.join(projectRoot, 'package.json'), { name: 'package.json' }); + + // 2) Add dist/ + console.log('Adding dist/...'); + archive.directory(path.join(projectRoot, 'dist'), 'dist'); + + // 3) Add node_modules/ — but skip the junction for @azure/functions-extensions-express + console.log('Adding node_modules/...'); + const nodeModulesPath = path.join(projectRoot, 'node_modules'); + const entries = fs.readdirSync(nodeModulesPath); + + for (const entry of entries) { + const fullPath = path.join(nodeModulesPath, entry); + const lstat = fs.lstatSync(fullPath); + + // Skip .package-lock.json, .cache, etc. + if (entry.startsWith('.')) continue; + + if (entry.startsWith('@')) { + // Scoped package — iterate sub-entries + const scopedEntries = fs.readdirSync(fullPath); + for (const subEntry of scopedEntries) { + const subFullPath = path.join(fullPath, subEntry); + const subLstat = fs.lstatSync(subFullPath); + const zipPath = `node_modules/${entry}/${subEntry}`; + + if (subLstat.isSymbolicLink() || isJunction(subFullPath)) { + // Resolve the junction/symlink target + const realPath = fs.realpathSync(subFullPath); + console.log(` Resolving junction: ${entry}/${subEntry} -> ${realPath}`); + addResolvedPackage(archive, realPath, zipPath); + } else if (subLstat.isDirectory()) { + archive.directory(subFullPath, zipPath); + } else { + archive.file(subFullPath, { name: zipPath }); + } + } + } else if (lstat.isSymbolicLink() || isJunction(fullPath)) { + const realPath = fs.realpathSync(fullPath); + console.log(` Resolving junction: ${entry} -> ${realPath}`); + addResolvedPackage(archive, realPath, `node_modules/${entry}`); + } else if (lstat.isDirectory()) { + archive.directory(fullPath, `node_modules/${entry}`); + } else { + archive.file(fullPath, { name: `node_modules/${entry}` }); + } + } + + console.log('\nFinalizing zip...'); + archive.finalize(); + + return promise; +} + +/** + * Check if a path is a Windows junction (different from symlink). + * On Windows, junctions have lstat().isSymbolicLink() = false but reparse point attributes. + */ +function isJunction(fullPath) { + try { + const lstat = fs.lstatSync(fullPath); + const stat = fs.statSync(fullPath); + // If lstat says it's not a directory but stat says it is, it's a junction + // Actually for junctions, lstat.isDirectory() is true but realpath differs + const realPath = fs.realpathSync(fullPath); + return realPath !== fullPath && lstat.isDirectory(); + } catch { + return false; + } +} + +/** + * Add a resolved package (from a junction/symlink) to the archive. + * Only includes distributable files: dist/, types/, lib/, package.json, README.md, LICENSE + * Also includes production dependencies from the resolved package's node_modules. + */ +function addResolvedPackage(archive, realPath, zipPrefix) { + // Only include these specific items from the resolved package + const allowedItems = ['package.json', 'README.md', 'LICENSE', 'dist', 'types', 'types-core', 'lib']; + + // Read dependencies from package.json to know which transitive deps to include + let prodDeps = {}; + const pkgPath = path.join(realPath, 'package.json'); + if (fs.existsSync(pkgPath)) { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + prodDeps = pkg.dependencies || {}; + } + + // Add only allowed distributable items + for (const item of allowedItems) { + const itemPath = path.join(realPath, item); + if (!fs.existsSync(itemPath)) continue; + + const stat = fs.statSync(itemPath); + if (stat.isDirectory()) { + archive.directory(itemPath, `${zipPrefix}/${item}`); + } else { + archive.file(itemPath, { name: `${zipPrefix}/${item}` }); + } + } + + // Include production dependencies from the resolved package's node_modules + const nestedNodeModules = path.join(realPath, 'node_modules'); + if (fs.existsSync(nestedNodeModules) && Object.keys(prodDeps).length > 0) { + console.log(` Including production deps for ${zipPrefix}:`); + includeProductionDeps(archive, nestedNodeModules, 'node_modules', prodDeps); + } +} + +/** + * Include only production dependencies from a nested node_modules into top-level node_modules. + * @param {*} archive - archiver instance + * @param {string} nodeModulesPath - path to nested node_modules + * @param {string} topLevelPrefix - "node_modules" (where to put them in the zip) + * @param {Object} prodDeps - the "dependencies" from the parent package.json + */ +function includeProductionDeps(archive, nodeModulesPath, topLevelPrefix, prodDeps) { + if (!fs.existsSync(nodeModulesPath)) return; + + const depNames = Object.keys(prodDeps); + + for (const depName of depNames) { + // Handle scoped packages like @azure/functions-extensions-base + const depPath = path.join(nodeModulesPath, ...depName.split('/')); + + if (!fs.existsSync(depPath)) { + console.log(` SKIP (not found): ${depName}`); + continue; + } + + const zipPath = `${topLevelPrefix}/${depName}`; + console.log(` Adding: ${depName}`); + + if (fs.statSync(depPath).isDirectory()) { + archive.directory(depPath, zipPath); + } else { + archive.file(depPath, { name: zipPath }); + } + } +} + +createDeployZip().catch((err) => { + console.error('Error creating zip:', err); + process.exit(1); +}); diff --git a/azure-functions-nodejs-extensions-express/samples/express-getting-started/scripts/create-deploy-zip.ps1 b/azure-functions-nodejs-extensions-express/samples/express-getting-started/scripts/create-deploy-zip.ps1 new file mode 100644 index 0000000..7aa05a2 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/samples/express-getting-started/scripts/create-deploy-zip.ps1 @@ -0,0 +1,126 @@ +# create-deploy-zip.ps1 +# Creates a deployment zip for Azure Functions, resolving local symlinks/junctions +# Usage: powershell -ExecutionPolicy Bypass -File scripts/create-deploy-zip.ps1 + +$ErrorActionPreference = "Stop" +$projectRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +Set-Location $projectRoot + +$stageDir = Join-Path $projectRoot ".deploy-stage" +$zipPath = Join-Path $projectRoot "deploy.zip" +$expressExtSource = Resolve-Path (Join-Path $projectRoot "..\..\extensions\functions-express") + +Write-Host "=== Azure Functions Deploy Zip Creator ===" -ForegroundColor Cyan +Write-Host "Project: $projectRoot" +Write-Host "" + +# Step 1: Build +Write-Host "[1/4] Building project..." -ForegroundColor Yellow +npm run build 2>&1 | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Host "Build failed!" -ForegroundColor Red + exit 1 +} +Write-Host " Build complete." -ForegroundColor Green + +# Step 2: Clean up old artifacts +Write-Host "[2/4] Preparing staging directory..." -ForegroundColor Yellow +if (Test-Path $stageDir) { Remove-Item -Recurse -Force $stageDir } +if (Test-Path $zipPath) { Remove-Item -Force $zipPath } +New-Item -ItemType Directory -Path $stageDir -Force | Out-Null + +# Step 3: Copy files using xcopy (which resolves junctions/symlinks) +Write-Host "[3/4] Copying deployment files..." -ForegroundColor Yellow + +# Copy dist +Write-Host " Copying dist..." +xcopy "dist" "$stageDir\dist" /E /I /Q /Y > $null + +# Copy host.json +Write-Host " Copying host.json..." +Copy-Item "host.json" "$stageDir\host.json" + +# Copy package.json +Write-Host " Copying package.json..." +Copy-Item "package.json" "$stageDir\package.json" + +# Copy node_modules using xcopy (resolves symlinks automatically) +Write-Host " Copying node_modules (this may take a moment)..." +xcopy "node_modules" "$stageDir\node_modules" /E /I /Q /Y > $null + +# Verify the @azure/functions-extensions-express junction was resolved +$expressExtDest = Join-Path $stageDir "node_modules\@azure\functions-extensions-express" +if (-not (Test-Path (Join-Path $expressExtDest "dist"))) { + Write-Host " Resolving @azure/functions-extensions-express symlink manually..." -ForegroundColor Yellow + if (Test-Path $expressExtDest) { Remove-Item -Recurse -Force $expressExtDest } + New-Item -ItemType Directory -Path $expressExtDest -Force | Out-Null + + # Copy just what's needed from the extension source + Copy-Item (Join-Path $expressExtSource "package.json") $expressExtDest + xcopy (Join-Path $expressExtSource "dist") (Join-Path $expressExtDest "dist") /E /I /Q /Y > $null + if (Test-Path (Join-Path $expressExtSource "types")) { + xcopy (Join-Path $expressExtSource "types") (Join-Path $expressExtDest "types") /E /I /Q /Y > $null + } + if (Test-Path (Join-Path $expressExtSource "README.md")) { + Copy-Item (Join-Path $expressExtSource "README.md") $expressExtDest + } +} + +# Remove devDependencies from staged node_modules (archiver, etc.) +$devDeps = @("archiver", "archiver-utils", "async", "balanced-match", "bl", "brace-expansion", + "buffer-crc32", "compress-commons", "concat-map", "crc-32", "crc32-stream", + "end-of-stream", "fs-constants", "glob", "graceful-fs", "inflight", "inherits", + "is-stream", "isarray", "jackspeak", "lazystream", "lru-cache", "minimatch", + "minipass", "normalize-path", "once", "path-scurry", "process-nextick-args", + "pump", "readable-stream", "readdir-glob", "safe-buffer", "string_decoder", + "tar-stream", "util-deprecate", "wrappy", "zip-stream") + +foreach ($dep in $devDeps) { + $depPath = Join-Path $stageDir "node_modules\$dep" + if (Test-Path $depPath) { + Remove-Item -Recurse -Force $depPath -ErrorAction SilentlyContinue + } +} + +Write-Host " Files staged." -ForegroundColor Green + +# Step 4: Create zip using .NET (produces Linux-compatible forward-slash paths) +Write-Host "[4/4] Creating zip archive..." -ForegroundColor Yellow + +Add-Type -AssemblyName System.IO.Compression.FileSystem +[System.IO.Compression.ZipFile]::CreateFromDirectory($stageDir, $zipPath, [System.IO.Compression.CompressionLevel]::Optimal, $false) + +$zipSize = [math]::Round((Get-Item $zipPath).Length / 1MB, 2) +Write-Host "" +Write-Host "=== Done! ===" -ForegroundColor Green +Write-Host " Zip: $zipPath" +Write-Host " Size: $zipSize MB" +Write-Host "" +Write-Host "To deploy:" -ForegroundColor Cyan +Write-Host ' az functionapp deployment source config-zip -g -n --src deploy.zip' +Write-Host "" + +# Cleanup staging +Remove-Item -Recurse -Force $stageDir + +# Verify zip contents +Write-Host "Zip contents (top-level):" -ForegroundColor Yellow +$zip = [System.IO.Compression.ZipFile]::OpenRead($zipPath) +$topLevel = $zip.Entries | ForEach-Object { ($_.FullName -split '/')[0] } | Sort-Object -Unique +foreach ($item in $topLevel) { + Write-Host " $item" +} + +# Check path separator style +$sampleEntry = ($zip.Entries | Where-Object { $_.FullName -like "*@azure*" } | Select-Object -First 1).FullName +if ($sampleEntry) { + Write-Host "" + Write-Host "Path style check: $sampleEntry" -ForegroundColor Yellow + if ($sampleEntry -match '\\') { + Write-Host " WARNING: Backslashes detected! This may cause issues on Linux." -ForegroundColor Red + } else { + Write-Host " OK: Forward slashes (Linux compatible)" -ForegroundColor Green + } +} + +$zip.Dispose() diff --git a/azure-functions-nodejs-extensions-express/samples/express-getting-started/src/functions/index.ts b/azure-functions-nodejs-extensions-express/samples/express-getting-started/src/functions/index.ts new file mode 100644 index 0000000..394ea30 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/samples/express-getting-started/src/functions/index.ts @@ -0,0 +1,191 @@ +/** + * Express API on Azure Functions - Getting Started + * + * This sample shows how to create a new Express API from scratch + * and host it on Azure Functions. Just write your Express code + * like you normally would! + */ + +import express, { Request, Response, NextFunction } from 'express'; +import { app } from '@azure/functions'; +import { expressApp as registerExpress } from '@azure/functions-extensions-express'; + +// ============================================================================ +// STEP 1: Create your Express app (just like you normally would!) +// ============================================================================ +const expressApp = express(); + +// Add middleware +expressApp.use(express.json()); + +// Simple request logging +expressApp.use((req: Request, _res: Response, next: NextFunction) => { + console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`); + next(); +}); + +// ============================================================================ +// STEP 2: Define your API routes +// ============================================================================ + +// Health check endpoint +expressApp.get('/health', (_req: Request, res: Response) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// Simple CRUD example with in-memory storage +interface Item { + id: number; + name: string; + description?: string; +} + +const items: Item[] = [ + { id: 1, name: 'First Item', description: 'This is the first item' }, + { id: 2, name: 'Second Item', description: 'This is the second item' }, +]; +let nextId = 3; + +// GET all items +expressApp.get('/items', (_req: Request, res: Response) => { + console.log('Fetching all items'); + res.json(items); +}); + +// GET single item +expressApp.get('/items/:id', (req: Request, res: Response) => { + const item = items.find((i) => i.id === parseInt(req.params.id)); + if (!item) { + res.status(404).json({ error: 'Item not found' }); + return; + } + res.json(item); +}); + +// CREATE new item +expressApp.post('/items', (req: Request, res: Response) => { + const { name, description } = req.body as { name?: string; description?: string }; + if (!name) { + res.status(400).json({ error: 'Name is required' }); + return; + } + const newItem: Item = { id: nextId++, name, description }; + items.push(newItem); + res.status(201).json(newItem); +}); + +// UPDATE item +expressApp.put('/items/:id', (req: Request, res: Response) => { + const item = items.find((i) => i.id === parseInt(req.params.id)); + if (!item) { + res.status(404).json({ error: 'Item not found' }); + return; + } + const { name, description } = req.body as { name?: string; description?: string }; + if (name) item.name = name; + if (description !== undefined) item.description = description; + res.json(item); +}); + +// DELETE item +expressApp.delete('/items/:id', (req: Request, res: Response) => { + const index = items.findIndex((i) => i.id === parseInt(req.params.id)); + if (index === -1) { + res.status(404).json({ error: 'Item not found' }); + return; + } + const deleted = items.splice(index, 1)[0]; + res.json({ message: 'Deleted', item: deleted }); +}); + +// ============================================================================ +// BONUS: Streaming endpoint - showcases sidecar's streaming capability! +// ============================================================================ +expressApp.get('/stream', (_req: Request, res: Response) => { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + let count = 0; + const maxEvents = 5; + + console.log('Starting SSE stream...'); + + const interval = setInterval(() => { + count++; + const data = { + event: count, + timestamp: new Date().toISOString(), + message: `Server-Sent Event #${count}`, + }; + + res.write(`data: ${JSON.stringify(data)}\n\n`); + console.log(`Sent event ${count}`); + + if (count >= maxEvents) { + clearInterval(interval); + res.write(`data: ${JSON.stringify({ event: 'done', message: 'Stream ended' })}\n\n`); + res.end(); + console.log('Stream ended'); + } + }, 1000); + + // Handle client disconnect + res.on('close', () => { + clearInterval(interval); + console.log('Client disconnected from stream'); + }); +}); + +// Error handler +expressApp.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { + console.error('Error:', err.message); + res.status(500).json({ error: 'Internal Server Error' }); +}); + +// ============================================================================ +// STEP 3: Register your Express app with Azure Functions +// +// expressApp() automatically discovers ALL routes you defined above +// and creates a separate Azure Function for each one: +// +// api-GET-health -> route: 'health' +// api-GET-items -> route: 'items' +// api-GET-items-id -> route: 'items/{id}' +// api-POST-items -> route: 'items' +// api-PUT-items-id -> route: 'items/{id}' +// api-DELETE-items-id -> route: 'items/{id}' +// api-GET-stream -> route: 'stream' +// +// Express runs as a real HTTP server on localhost, which provides +// FULL Express compatibility including: +// - Streaming responses (SSE, file downloads) +// - All middleware works exactly as designed +// - Real socket connections +// ============================================================================ +const server = registerExpress('endpoint', expressApp, { + enableStreaming: true, // Enable streaming for SSE support +}); + +// Optional: Handle graceful shutdown +app.hook.appTerminate(async () => { + console.log('Shutting down Express server...'); + await server.stop(); +}); + +// ============================================================================ +// That's it! Each Express route is now a separate Azure Function. +// +// To run locally: +// npm install +// npm start +// +// You'll see each function registered individually: +// api-GET-health -> http://localhost:7071/api/health +// api-GET-items -> http://localhost:7071/api/items +// api-GET-items-id -> http://localhost:7071/api/items/:id +// api-POST-items -> http://localhost:7071/api/items +// api-PUT-items-id -> http://localhost:7071/api/items/:id +// api-DELETE-items-id -> http://localhost:7071/api/items/:id +// api-GET-stream -> http://localhost:7071/api/stream (SSE!) +// ============================================================================ diff --git a/azure-functions-nodejs-extensions-express/samples/express-getting-started/tsconfig.json b/azure-functions-nodejs-extensions-express/samples/express-getting-started/tsconfig.json new file mode 100644 index 0000000..5ddfc26 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/samples/express-getting-started/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/azure-functions-nodejs-extensions-express/src/index.ts b/azure-functions-nodejs-extensions-express/src/index.ts new file mode 100644 index 0000000..32c5792 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/src/index.ts @@ -0,0 +1,141 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +/** + * @azure/functions-extensions-express — Express integration for Azure Functions. + * + * Runs Express as a real HTTP server inside the Functions worker. + * Each Express route is automatically discovered and registered as + * a separate Azure Function with full streaming and middleware support. + * + * @example + * ```typescript + * import express from 'express'; + * import { expressApp } from '@azure/functions-extensions-express'; + * + * const app = express(); + * app.get('/items', (req, res) => res.json(items)); + * app.get('/items/:id', (req, res) => res.json(item)); + * expressApp('api', app); + * ``` + */ + +import { app, HttpHandler, HttpRequest, HttpResponse, InvocationContext } from '@azure/functions'; +import { Readable } from 'stream'; +import { extractRoutes } from './routeExtractor'; +import { ExpressSidecarServer, getOrCreateServer } from './server'; +import { ExpressApp, ExpressFunctionOptions, ExpressServer, SidecarOptions } from './types'; + +export { DiscoveredRoute } from './routeExtractor'; +export { getServer, stopAllServers } from './server'; +export { ExpressApp, ExpressFunctionOptions, ExpressServer } from './types'; + +const NULL_BODY_CODES = new Set([204, 304]); + +// Enable HTTP streaming at the Azure Functions runtime level. +// Without this, the host serializes the entire response over gRPC +// and the browser receives everything at once instead of incrementally. +let httpStreamEnabled = false; +function ensureHttpStream(): void { + if (!httpStreamEnabled) { + app.setup({ enableHttpStream: true }); + httpStreamEnabled = true; + } +} + +function createHandler(sidecar: ExpressSidecarServer, opts: SidecarOptions): HttpHandler { + return async (request: HttpRequest, context: InvocationContext) => { + const url = new URL(request.url); + let path = url.pathname + url.search; + + // Strip Azure Functions route prefix so Express sees its own paths + if (opts.routePrefix && path.startsWith(opts.routePrefix)) { + path = path.slice(opts.routePrefix.length) || '/'; + if (!path.startsWith('/')) path = '/' + path; + } + + const headers: Record = {}; + request.headers.forEach((v, k) => { + headers[k] = v; + }); + + let body: Buffer | null = null; + if (request.body) body = Buffer.from(await request.arrayBuffer()); + + const res = await sidecar.proxy(request.method, path, headers, body, context.invocationId); + const noBody = NULL_BODY_CODES.has(res.status) || (res.status >= 100 && res.status < 200); + + if (opts.enableStreaming && res.body instanceof Readable) { + // Return an HttpResponse with enableContentNegotiation disabled + // so the Azure Functions host streams the body incrementally. + const httpRes = new HttpResponse({ + status: res.status, + headers: res.headers, + enableContentNegotiation: false, + body: noBody ? undefined : res.body, + }); + return httpRes; + } + + return { + status: res.status, + headers: res.headers, + body: noBody ? undefined : res.body ?? undefined, + }; + }; +} + +/** + * Register an Express app as Azure Functions. + * + * Discovers all routes and creates one Azure Function per route. + * Falls back to a catch-all if no routes are found. + */ +export function expressApp( + name: string, + expressApplication: ExpressApp, + options: ExpressFunctionOptions = {} +): ExpressServer { + const routePrefix = options.routePrefix ?? ''; + const opts: SidecarOptions = { + port: options.port ?? 0, + basePath: options.basePath ?? '', + timeout: options.timeout ?? 30000, + enableStreaming: options.enableStreaming ?? true, + routePrefix: routePrefix ? '/' + routePrefix.replace(/^\//, '') : '', + }; + + const sidecar = getOrCreateServer(name, expressApplication, opts); + const handler = createHandler(sidecar, opts); + const routes = extractRoutes(expressApplication, opts.basePath); + + // Enable HTTP streaming so the Functions host proxies responses + // instead of buffering them over gRPC. + if (opts.enableStreaming) { + ensureHttpStream(); + } + + if (routes.length === 0) { + console.warn('[functions-express] No routes found, using catch-all.'); + app.http(name, { + route: options.route || '{*path}', + methods: options.methods || ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'], + authLevel: options.authLevel || 'anonymous', + handler, + }); + } else { + console.log(`[functions-express] Discovered ${routes.length} route(s):`); + for (const r of routes) { + const fn = `${name}-${r.functionName}`; + console.log(` ${fn} -> [${r.methods.join(', ')}] /${r.azureFunctionsRoute}`); + app.http(fn, { + route: r.azureFunctionsRoute, + methods: r.methods as ('GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS')[], + authLevel: options.authLevel || 'anonymous', + handler, + }); + } + } + + return sidecar; +} diff --git a/azure-functions-nodejs-extensions-express/src/routeExtractor.ts b/azure-functions-nodejs-extensions-express/src/routeExtractor.ts new file mode 100644 index 0000000..2a3d372 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/src/routeExtractor.ts @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +export interface DiscoveredRoute { + expressPath: string; + azureFunctionsRoute: string; + methods: string[]; + functionName: string; +} + +/** Walk the Express router stack and return all registered routes. */ +export function extractRoutes(expressApp: unknown, basePath = ''): DiscoveredRoute[] { + const routes: DiscoveredRoute[] = []; + const seen = new Set(); + const stack = ((expressApp as Record)?._router as Record)?.stack; + if (!Array.isArray(stack)) return routes; + processStack(stack, basePath, routes, seen); + return routes; +} + +function processStack(stack: unknown[], prefix: string, routes: DiscoveredRoute[], seen: Set): void { + let idx = 0; + while (idx < stack.length) { + const layer = stack[idx] as Record; + idx++; + const route = layer.route as Record | undefined; + const handle = layer.handle as Record | undefined; + + if (route) { + const methods = getMethods(route.methods as Record | undefined); + const fullPath = cleanPath(prefix + (route.path as string)); + const key = methods.sort().join(',') + ':' + fullPath; + if (!seen.has(key)) { + seen.add(key); + routes.push({ + expressPath: fullPath, + azureFunctionsRoute: expressPathToAzureRoute(fullPath), + methods, + functionName: generateFunctionName(fullPath, methods), + }); + } + } else if (handle && Array.isArray(handle.stack)) { + const nestedPrefix = getPrefix(layer, layer.regexp as RegExp | undefined); + processStack(handle.stack as unknown[], prefix + nestedPrefix, routes, seen); + } + } +} + +function getPrefix(layer: Record, regexp?: RegExp): string { + if (typeof layer.path === 'string' && layer.path !== '/') return layer.path; + if (regexp) { + const m = regexp.source.match(/^\^\\?\/?([\w\-\/]*)/); + if (m && m[1]) return '/' + m[1]; + } + return ''; +} + +function getMethods(methods: Record | undefined): string[] { + if (!methods) return ['GET']; + const result: string[] = []; + const keys = Object.keys(methods); + let i = 0; + while (i < keys.length) { + if (methods[keys[i]] && keys[i] !== '_all') result.push(keys[i].toUpperCase()); + i++; + } + return result; +} + +/** Convert Express path params to Azure Functions route format. */ +export function expressPathToAzureRoute(expressPath: string): string { + let route = expressPath.startsWith('/') ? expressPath.substring(1) : expressPath; + route = route.replace(/:(\w+)/g, '{$1}'); + route = route.replace(/\(\.\*\)/g, '{*path}'); + route = route.replace(/\*(\w*)/g, (_m: string, name: string) => '{*' + (name || 'path') + '}'); + return route; +} + +/** Generate a unique Azure Function name from route + methods. */ +export function generateFunctionName(expressPath: string, methods: string[]): string { + let p = expressPath.replace(/^\/|\/$/g, ''); + p = p.replace(/:(\w+)/g, '$1'); + p = p.replace(/\//g, '-'); + p = p.replace(/\*/g, 'wildcard').replace(/\(\.\*\)/g, 'wildcard'); + p = p.replace(/[^a-zA-Z0-9-]/g, ''); + return methods.join('-') + '-' + p; +} + +function cleanPath(p: string): string { + let n = p.replace(/\/+/g, '/'); + if (!n.startsWith('/')) n = '/' + n; + if (n.length > 1 && n.endsWith('/')) n = n.slice(0, -1); + return n; +} diff --git a/azure-functions-nodejs-extensions-express/src/server.ts b/azure-functions-nodejs-extensions-express/src/server.ts new file mode 100644 index 0000000..b2d2b4f --- /dev/null +++ b/azure-functions-nodejs-extensions-express/src/server.ts @@ -0,0 +1,143 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import * as http from 'http'; +import { Readable } from 'stream'; +import { ExpressApp, ExpressServer, SidecarOptions } from './types'; + +// --- Server registry --- + +const registry = new Map(); + +export function getOrCreateServer(name: string, app: ExpressApp, opts: SidecarOptions): ExpressSidecarServer { + let server = registry.get(name); + if (!server) { + server = new ExpressSidecarServer(app, opts); + registry.set(name, server); + } + return server; +} + +export function getServer(name: string): ExpressServer | undefined { + return registry.get(name); +} + +export async function stopAllServers(): Promise { + await Promise.all(Array.from(registry.values()).map((s) => s.stop())); + registry.clear(); +} + +// --- Sidecar server --- + +export class ExpressSidecarServer implements ExpressServer { + readonly #app: ExpressApp; + readonly #opts: SidecarOptions; + #server: http.Server | null = null; + #port = 0; + #started = false; + #starting: Promise | null = null; + + constructor(app: ExpressApp, opts: SidecarOptions) { + this.#app = app; + this.#opts = opts; + } + + get port(): number { + return this.#port; + } + get isStarted(): boolean { + return this.#started; + } + + async start(): Promise { + if (this.#started) return; + this.#starting ??= new Promise((resolve, reject) => { + this.#server = http.createServer(this.#app); + this.#server.on('error', reject); + this.#server.listen(this.#opts.port, '127.0.0.1', () => { + const addr = this.#server?.address(); + if (addr && typeof addr !== 'string') this.#port = addr.port; + this.#started = true; + console.log(`[functions-express] Started on port ${this.#port}`); + resolve(); + }); + }); + await this.#starting; + } + + async stop(): Promise { + if (!this.#server || !this.#started) return; + return new Promise((resolve, reject) => { + this.#server!.close((err) => { + if (err) return reject(err); + this.#started = false; + this.#server = null; + this.#starting = null; + resolve(); + }); + }); + } + + /** Proxy a request to the Express server. Returns streaming or buffered body based on options. */ + async proxy( + method: string, + url: string, + headers: Record, + body: Buffer | null, + invocationId: string + ): Promise<{ status: number; headers: Record; body: Readable | Buffer | null }> { + await this.start(); + + // Strip basePath prefix + let path = url; + if (this.#opts.basePath) { + const base = this.#opts.basePath.replace(/\/$/, ''); + if (path.startsWith(base)) path = path.slice(base.length) || '/'; + } + + return new Promise((resolve, reject) => { + const req = http.request( + { + hostname: '127.0.0.1', + port: this.#port, + path, + method, + headers: { + ...headers, + 'x-forwarded-for': headers['x-forwarded-for'] || headers['x-azure-clientip'] || '', + 'x-azure-functions-invocationid': invocationId, + }, + timeout: this.#opts.timeout, + }, + (res) => { + // Collect response headers + const h: Record = {}; + for (const [k, v] of Object.entries(res.headers)) { + if (v !== undefined) h[k] = Array.isArray(v) ? v.join(', ') : v; + } + delete h['connection']; + delete h['keep-alive']; + delete h['transfer-encoding']; + + const status = res.statusCode || 200; + + if (this.#opts.enableStreaming) { + resolve({ status, headers: h, body: res }); + } else { + const chunks: Buffer[] = []; + res.on('data', (c: Buffer) => chunks.push(c)); + res.on('end', () => resolve({ status, headers: h, body: Buffer.concat(chunks) })); + res.on('error', reject); + } + } + ); + + req.on('error', reject); + req.on('timeout', () => { + req.destroy(); + reject(new Error(`Timeout after ${this.#opts.timeout}ms`)); + }); + body ? req.end(body) : req.end(); + }); + } +} diff --git a/azure-functions-nodejs-extensions-express/src/types.ts b/azure-functions-nodejs-extensions-express/src/types.ts new file mode 100644 index 0000000..859b1e0 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/src/types.ts @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import * as http from 'http'; + +/** Node.js HTTP request handler (Express, Koa, Fastify, etc.) */ +export interface ExpressApp { + (req: http.IncomingMessage, res: http.ServerResponse, next?: (err?: unknown) => void): void; +} + +/** Options for registering an Express app with Azure Functions. */ +export interface ExpressFunctionOptions { + /** Fallback route pattern when no routes are discovered. @default '{*path}' */ + route?: string; + /** HTTP methods to accept. @default all standard methods */ + methods?: ('GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS')[]; + /** Authorization level. @default 'anonymous' */ + authLevel?: 'anonymous' | 'function' | 'admin'; + /** Base path prefix to strip before routing to Express. */ + basePath?: string; + /** Port for the Express server (0 = auto). @default 0 */ + port?: number; + /** Request timeout in ms. @default 30000 */ + timeout?: number; + /** Enable streaming responses (SSE, large payloads). @default true */ + enableStreaming?: boolean; + /** host.json routePrefix to strip from URLs. @default '' */ + routePrefix?: string; +} + +/** Resolved internal options for the sidecar server. */ +export interface SidecarOptions { + port: number; + basePath: string; + timeout: number; + enableStreaming: boolean; + routePrefix: string; +} + +/** Express server running inside the Azure Functions worker. */ +export interface ExpressServer { + readonly port: number; + readonly isStarted: boolean; + start(): Promise; + stop(): Promise; +} diff --git a/azure-functions-nodejs-extensions-express/tsconfig.json b/azure-functions-nodejs-extensions-express/tsconfig.json new file mode 100644 index 0000000..ec389ba --- /dev/null +++ b/azure-functions-nodejs-extensions-express/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": false, + "declarationMap": false, + "sourceMap": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/azure-functions-nodejs-extensions-express/types/index.d.ts b/azure-functions-nodejs-extensions-express/types/index.d.ts new file mode 100644 index 0000000..85ec9b8 --- /dev/null +++ b/azure-functions-nodejs-extensions-express/types/index.d.ts @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import * as http from 'http'; + +/** Node.js HTTP request handler (Express, Koa, Fastify, etc.) */ +export interface ExpressApp { + (req: http.IncomingMessage, res: http.ServerResponse, next?: (err?: unknown) => void): void; +} + +/** Options for registering an Express app with Azure Functions. */ +export interface ExpressFunctionOptions { + /** Fallback route pattern when no routes are discovered. @default '{*path}' */ + route?: string; + /** HTTP methods to accept. @default all standard methods */ + methods?: ('GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS')[]; + /** Authorization level. @default 'anonymous' */ + authLevel?: 'anonymous' | 'function' | 'admin'; + /** Base path prefix to strip before routing to Express. */ + basePath?: string; + /** Port for the Express server (0 = auto). @default 0 */ + port?: number; + /** Request timeout in ms. @default 30000 */ + timeout?: number; + /** Enable streaming responses (SSE, large payloads). @default true */ + enableStreaming?: boolean; + /** host.json routePrefix to strip from URLs. @default '' */ + routePrefix?: string; +} + +/** Express server running inside the Azure Functions worker. */ +export interface ExpressServer { + readonly port: number; + readonly isStarted: boolean; + start(): Promise; + stop(): Promise; +} + +/** A route discovered from an Express application. */ +export interface DiscoveredRoute { + expressPath: string; + azureFunctionsRoute: string; + methods: string[]; + functionName: string; +} + +/** + * Register an Express app as Azure Functions. + * + * Discovers all routes and creates one Azure Function per route. + * Express runs as a real HTTP server with full streaming and middleware support. + * + * @example + * ```typescript + * import express from 'express'; + * import { expressApp } from '@azure/functions-extensions-express'; + * + * const app = express(); + * app.get('/items', (req, res) => res.json(items)); + * app.get('/items/:id', (req, res) => res.json(item)); + * expressApp('api', app); + * ``` + */ +export function expressApp(name: string, expressApp: ExpressApp, options?: ExpressFunctionOptions): ExpressServer; + +/** Stops all Express servers. Call during app termination for graceful shutdown. */ +export function stopAllServers(): Promise; + +/** Gets an Express server by name. */ +export function getServer(name: string): ExpressServer | undefined;