diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..02db30a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# If there are abnormal line endings in any file, run "git add --renormalize ", +# review the changes, and commit them to fix the line endings. +* text=auto \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5077924..b6da1df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,62 +1,70 @@ -name: FeatureManagement-JavaScriptProvider CI - -on: - push: - branches: - - main - - preview - - release/* - pull_request: - branches: - - main - - preview - - release/* - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [18.x, 20.x] - - defaults: - run: - working-directory: sdk/feature-management - - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - cache-dependency-path: sdk/feature-management/package-lock.json - - - name: Install dependencies - run: npm ci - working-directory: sdk/feature-management - - - name: Run lint check for feature-management - run: npm run lint - working-directory: sdk/feature-management - - - name: Build feature-management - run: npm run build - working-directory: sdk/feature-management - - - name: Run tests - run: npm run test - working-directory: sdk/feature-management - - - name: Run browser tests - run: npm run test-browser - working-directory: sdk/feature-management - - - name: Build feature-management-applicationinsights-browser - run: npm run build - working-directory: sdk/feature-management-applicationinsights-browser - - - name: Run lint check for feature-management-applicationinsights-browser - run: npm run lint - working-directory: sdk/feature-management-applicationinsights-browser \ No newline at end of file +name: FeatureManagement-JavaScript CI + +on: + push: + branches: + - main + - preview + - release/* + pull_request: + branches: + - main + - preview + - release/* + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + + defaults: + run: + working-directory: src/feature-management + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: src/feature-management/package-lock.json + + - name: Install dependencies + run: npm ci + working-directory: src/feature-management + + - name: Run lint check for feature-management + run: npm run lint + working-directory: src/feature-management + + - name: Build feature-management + run: npm run build + working-directory: src/feature-management + + - name: Run tests + run: npm run test + working-directory: src/feature-management + + - name: Run browser tests + run: npm run test-browser + working-directory: src/feature-management + + - name: Build feature-management-applicationinsights-browser + run: npm run build + working-directory: src/feature-management-applicationinsights-browser + + - name: Run lint check for feature-management-applicationinsights-browser + run: npm run lint + working-directory: src/feature-management-applicationinsights-browser + + - name: Build feature-management-applicationinsights-node + run: npm run build + working-directory: src/feature-management-applicationinsights-node + + - name: Run lint check for feature-management-applicationinsights-node + run: npm run lint + working-directory: src/feature-management-applicationinsights-node diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..587f74b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,92 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main", "Preview", "Release" ] + pull_request: + branches: [ "main", "Preview", "Release" ] + schedule: + - cron: '45 7 * * 0' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.gitignore b/.gitignore index 250bd47..e8b8fb0 100644 --- a/.gitignore +++ b/.gitignore @@ -413,3 +413,5 @@ examples/**/**/package-lock.json # playwright test result test-results + +**/public \ No newline at end of file diff --git a/README.md b/README.md index 7ddcdc6..5f78a40 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,19 @@ [![feature-management](https://img.shields.io/npm/v/@microsoft/feature-management?label=@microsoft/feature-management)](https://www.npmjs.com/package/@microsoft/feature-management) -Feature Management is a library for enabling/disabling features at runtime. -Developers can use feature flags in simple use cases like conditional statement to more advanced scenarios like conditionally adding routes. +Feature management provides a way to develop and expose application functionality based on features. Many applications have special requirements when a new feature is developed such as when the feature should be enabled and under what conditions. This library provides a way to define these relationships, and also integrates into common JavaScript code patterns to make exposing these features possible. ## Getting Started -### Prerequisites +[Azure App Configuration Quickstart](https://learn.microsoft.com/azure/azure-app-configuration/quickstart-feature-flag-javascript): A quickstart guide about how to integrate feature flags from Azure App Configuration into your JavaScript applications. -- Node.js LTS version +[Feature Overview](https://learn.microsoft.com/azure/azure-app-configuration/feature-management-overview#feature-development-status): This document provides a feature status overview. + +[Feature Reference](https://learn.microsoft.com/azure/azure-app-configuration/feature-management-javascript-reference): This document provides a full feature rundown. ### Usage -You can use feature flags from the Azure App Configuration service, local files or any other sources. +You can use feature flags from the Azure App Configuration service, local files or any other sources. For more information, please go to [Feature flag configuration](https://learn.microsoft.com/azure/azure-app-configuration/feature-management-javascript-reference#feature-flag-configuration). #### Use feature flags from Azure App Configuration @@ -21,7 +22,9 @@ The App Configuration JavaScript provider provides feature flags in as a `Map` o A builtin `ConfigurationMapFeatureFlagProvider` helps to load feature flags in this case. ```js -const appConfig = await load(connectionString, {featureFlagOptions}); // load feature flags from Azure App Configuration service +import { load } from "@azure/app-configuration-provider"; +import { FeatureManager, ConfigurationMapFeatureFlagProvider } from "@microsoft/feature-management"; +const appConfig = await load("", {featureFlagOptions}); // load feature flags from Azure App Configuration service const featureProvider = new ConfigurationMapFeatureFlagProvider(appConfig); const featureManager = new FeatureManager(featureProvider); const isAlphaEnabled = await featureManager.isEnabled("Alpha"); @@ -54,6 +57,7 @@ Content of `sample.json`: Load feature flags from `sample.json` file. ```js +import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "@microsoft/feature-management"; const config = JSON.parse(await fs.readFile("path/to/sample.json")); const featureProvider = new ConfigurationObjectFeatureFlagProvider(config); const featureManager = new FeatureManager(featureProvider); @@ -61,6 +65,10 @@ const isAlphaEnabled = await featureManager.isEnabled("Alpha"); console.log("Feature Alpha is:", isAlphaEnabled); ``` +## Examples + +See code snippets under [examples/](./examples/) folder. + ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a diff --git a/SUPPORT.md b/SUPPORT.md index f176f1a..90187a0 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,11 +1,11 @@ -# Support - -## How to file issues and get help - -This project uses GitHub Issues to track bugs and feature requests. Please search the existing -issues before filing new issues to avoid duplicates. For new issues, file your bug or -feature request as a new Issue. - -## Microsoft Support Policy - -Support for this project is limited to the resources listed above. +# Support + +## How to file issues and get help + +This project uses GitHub Issues to track bugs and feature requests. Please search the existing +issues before filing new issues to avoid duplicates. For new issues, file your bug or +feature request as a new Issue. + +## Microsoft Support Policy + +Support for this project is limited to the resources listed above. diff --git a/examples/express-app/README.md b/examples/express-app/README.md new file mode 100644 index 0000000..a8ba572 --- /dev/null +++ b/examples/express-app/README.md @@ -0,0 +1,76 @@ +# Examples for Microsoft Feature Management for JavaScript + +These examples show how to use the Microsoft Feature Management in an express application. + +## Prerequisites + +The examples are compatible with [LTS versions of Node.js](https://github.com/nodejs/release#release-schedule). + +## Setup & Run + +1. Install the dependencies using `npm`: + + ```bash + npm install + ``` + +1. Run the examples: + + ```bash + node server.mjs + ``` + +1. Visit `http://localhost:3000/Beta` and use `userId` and `groups` query to specify the targeting context (e.g. /Beta?userId=Jeff or /Beta?groups=Admin). + + - If you are not targeted, you will get the message "Page not found". + + - If you are targeted, you will get the message "Welcome to the Beta page!". + +## Targeting + +The targeting mechanism uses the `exampleTargetingContextAccessor` to extract the targeting context from the request. This function retrieves the userId and groups from the query parameters of the request. + +```javascript +const exampleTargetingContextAccessor = { + getTargetingContext: () => { + const req = requestAccessor.getStore(); + if (req === undefined) { + return undefined; + } + // read user and groups from request query data + const { userId, groups } = req.query; + // return aa ITargetingContext with the appropriate user info + return { userId: userId, groups: groups ? groups.split(",") : [] }; + } +}; +``` + +The `FeatureManager` is configured with this targeting context accessor: + +```javascript +const featureManager = new FeatureManager( + featureProvider, + { + targetingContextAccessor: exampleTargetingContextAccessor + } +); +``` + +This allows you to get ambient targeting context while doing feature flag evaluation. + +### Request Accessor + +The `requestAccessor` is an instance of `AsyncLocalStorage` from the `async_hooks` module. It is used to store the request object in asynchronous local storage, allowing it to be accessed throughout the lifetime of the request. This is particularly useful for accessing request-specific data in asynchronous operations. For more information, please go to https://nodejs.org/api/async_context.html + +```javascript +import { AsyncLocalStorage } from "async_hooks"; +const requestAccessor = new AsyncLocalStorage(); +``` + +Middleware is used to store the request object in the AsyncLocalStorage: + +```javascript +server.use((req, res, next) => { + requestAccessor.run(req, next); +}); +``` \ No newline at end of file diff --git a/examples/express-app/config.json b/examples/express-app/config.json new file mode 100644 index 0000000..f085efe --- /dev/null +++ b/examples/express-app/config.json @@ -0,0 +1,31 @@ +{ + "feature_management": { + "feature_flags": [ + { + "id": "Beta", + "enabled": true, + "conditions": { + "client_filters": [ + { + "name": "Microsoft.Targeting", + "parameters": { + "Audience": { + "Users": [ + "Jeff" + ], + "Groups": [ + { + "Name": "Admin", + "RolloutPercentage": 100 + } + ], + "DefaultRolloutPercentage": 40 + } + } + } + ] + } + } + ] + } +} diff --git a/examples/express-app/package.json b/examples/express-app/package.json new file mode 100644 index 0000000..c9094e9 --- /dev/null +++ b/examples/express-app/package.json @@ -0,0 +1,9 @@ +{ + "scripts": { + "start": "node server.mjs" + }, + "dependencies": { + "@microsoft/feature-management": "2.1.0", + "express": "^4.21.2" + } +} \ No newline at end of file diff --git a/examples/express-app/server.mjs b/examples/express-app/server.mjs new file mode 100644 index 0000000..0404b33 --- /dev/null +++ b/examples/express-app/server.mjs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import fs from "fs/promises"; +import { ConfigurationObjectFeatureFlagProvider, FeatureManager } from "@microsoft/feature-management"; +// You can also use Azure App Configuration as the source of feature flags. +// For more information, please go to quickstart: https://learn.microsoft.com/azure/azure-app-configuration/quickstart-feature-flag-javascript + +const config = JSON.parse(await fs.readFile("config.json")); +const featureProvider = new ConfigurationObjectFeatureFlagProvider(config); + +// https://nodejs.org/api/async_context.html +import { AsyncLocalStorage } from "async_hooks"; +const requestAccessor = new AsyncLocalStorage(); +const exampleTargetingContextAccessor = { + getTargetingContext: () => { + const req = requestAccessor.getStore(); + if (req === undefined) { + return undefined; + } + // read user and groups from request query data + const { userId, groups } = req.query; + // return an ITargetingContext with the appropriate user info + return { userId: userId, groups: groups ? groups.split(",") : [] }; + } +}; + +const featureManager = new FeatureManager( + featureProvider, + { + targetingContextAccessor: exampleTargetingContextAccessor + } +); + +import express from "express"; +const server = express(); +const PORT = 3000; + +// Use a middleware to store the request object in async local storage. +// The async local storage allows the targeting context accessor to access the current request throughout its lifetime. +// Middleware 1 (request object is stored in async local storage here and it will be available across the following chained async operations) +// Middleware 2 +// Request Handler (feature flag evaluation happens here) +server.use((req, res, next) => { + requestAccessor.run(req, next); +}); + +server.get("/", (req, res) => { + res.send("Hello World!"); +}); + +server.get("/Beta", async (req, res) => { + if (await featureManager.isEnabled("Beta")) { + res.send("Welcome to the Beta page!"); + } else { + res.status(404).send("Page not found"); + } +}); + +// Start the server +server.listen(PORT, () => { + console.log(`Server is running at http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/examples/quote-of-the-day/.env.temlate b/examples/quote-of-the-day/.env.temlate new file mode 100644 index 0000000..2f14536 --- /dev/null +++ b/examples/quote-of-the-day/.env.temlate @@ -0,0 +1,6 @@ +# You can define environment variables in .env file and load them with 'dotenv' package. +# This is a template of related environment variables in examples. +# To use this file directly, please rename it to .env +APPCONFIG_CONNECTION_STRING= +APPLICATIONINSIGHTS_CONNECTION_STRING= +USE_APP_CONFIG=true \ No newline at end of file diff --git a/examples/quote-of-the-day/README.md b/examples/quote-of-the-day/README.md new file mode 100644 index 0000000..9cdef0a --- /dev/null +++ b/examples/quote-of-the-day/README.md @@ -0,0 +1,155 @@ +# Quote of the day - JavaScript + +These examples show how to use the Microsoft Feature Management in an express application. + +## Setup & Run + +1. Build the project. + +```cmd +npm run build +``` + +1. Start the application. + +```cmd +npm run start +``` + +## Telemetry + +The Quote of the Day example implements telemetry using Azure Application Insights to track feature flag evaluations. This helps monitor and analyze how feature flags are being used in your application. + +### Application Insights Integration + +The application uses the `@microsoft/feature-management-applicationinsights-node` package to integrate Feature Management with Application Insights: + +```javascript +const { createTelemetryPublisher } = require("@microsoft/feature-management-applicationinsights-node"); + +// When initializing Feature Management +const publishTelemetry = createTelemetryPublisher(appInsightsClient); +featureManager = new FeatureManager(featureFlagProvider, { + onFeatureEvaluated: publishTelemetry, + targetingContextAccessor: targetingContextAccessor +}); +``` + +The `onFeatureEvaluated` option registers a callback that automatically sends telemetry events to Application Insights whenever a feature flag is evaluated. + +### Targeting Context in Telemetry + +`createTargetingTelemetryProcessor` method creates a built-in Application Insights telemetry processor which gets targeting context from the targeting context accessor and attaches the targeting id to telemetry. + +```javascript +// Initialize Application Insights with targeting context +applicationInsights.defaultClient.addTelemetryProcessor( + createTargetingTelemetryProcessor(targetingContextAccessor) +); +``` + +This ensures that every telemetry sent to Application Insights includes the targeting id information, allowing you to correlate feature flag usage with specific users or groups in your analytics. + +### Experimentation and A/B Testing + +Telemetry is particularly valuable for running experiments like A/B tests. Here's how you can use telemetry to track whether different variants of a feature influence user behavior. + +In this example, a variant feature flag is used to track the like button click rate of a web application: + +```json +{ + "id": "Greeting", + "enabled": true, + "variants": [ + { + "name": "Default" + }, + { + "name": "Simple", + "configuration_value": "Hello!" + }, + { + "name": "Long", + "configuration_value": "I hope this makes your day!" + } + ], + "allocation": { + "percentile": [ + { + "variant": "Default", + "from": 0, + "to": 50 + }, + { + "variant": "Simple", + "from": 50, + "to": 75 + }, + { + "variant": "Long", + "from": 75, + "to": 100 + } + ], + "default_when_enabled": "Default", + "default_when_disabled": "Default" + }, + "telemetry": { + "enabled": true + } +} +``` + +## Targeting + +The targeting mechanism uses the `exampleTargetingContextAccessor` to extract the targeting context from the request. This function retrieves the userId and groups from the query parameters of the request. + +```javascript +const targetingContextAccessor = { + getTargetingContext: () => { + const req = requestAccessor.getStore(); + if (req === undefined) { + return undefined; + } + // read user and groups from request + const userId = req.query.userId ?? req.body.userId; + const groups = req.query.groups ?? req.body.groups; + // return an ITargetingContext with the appropriate user info + return { userId: userId, groups: groups ? groups.split(",") : [] }; + } +}; +``` + +The `FeatureManager` is configured with this targeting context accessor: + +```javascript +const featureManager = new FeatureManager( + featureProvider, + { + targetingContextAccessor: exampleTargetingContextAccessor + } +); +``` + +This allows you to get ambient targeting context while doing feature flag evaluation and variant allocation. + +### Request Accessor + +The `requestAccessor` is an instance of `AsyncLocalStorage` from the `async_hooks` module. It is used to store the request object in asynchronous local storage, allowing it to be accessed throughout the lifetime of the request. This is particularly useful for accessing request-specific data in asynchronous operations. For more information, please go to https://nodejs.org/api/async_context.html + +```javascript +import { AsyncLocalStorage } from "async_hooks"; +const requestAccessor = new AsyncLocalStorage(); +``` + +Middleware is used to store the request object in the AsyncLocalStorage: + +```javascript +const requestStorageMiddleware = (req, res, next) => { + requestAccessor.run(req, next); +}; + +... + +server.use(requestStorageMiddleware); +``` diff --git a/examples/quote-of-the-day/client/index.html b/examples/quote-of-the-day/client/index.html new file mode 100644 index 0000000..149e009 --- /dev/null +++ b/examples/quote-of-the-day/client/index.html @@ -0,0 +1,13 @@ + + + + + + + Quote of the Day + + +
+ + + diff --git a/examples/quote-of-the-day/client/package.json b/examples/quote-of-the-day/client/package.json new file mode 100644 index 0000000..e3dabf3 --- /dev/null +++ b/examples/quote-of-the-day/client/package.json @@ -0,0 +1,17 @@ +{ + "name": "quoteoftheday", + "type": "module", + "scripts": { + "build": "vite build --emptyOutDir" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.27.0", + "react-icons": "5.3.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.4.1" + } +} diff --git a/examples/quote-of-the-day/client/src/App.css b/examples/quote-of-the-day/client/src/App.css new file mode 100644 index 0000000..0df6d08 --- /dev/null +++ b/examples/quote-of-the-day/client/src/App.css @@ -0,0 +1,198 @@ +body { + margin: 0; + font-family: 'Georgia', serif; +} + +.quote-page { + display: flex; + flex-direction: column; + min-height: 100vh; + background-color: #f4f4f4; +} + +.navbar { + background-color: white; + border-bottom: 1px solid #eaeaea; + display: flex; + justify-content: space-between; + padding: 10px 20px; + align-items: center; + font-family: 'Arial', sans-serif; + font-size: 16px; +} + +.navbar-left { + display: flex; + align-items: center; + margin-left: 40px; +} + +.logo { + font-size: 1.25em; + text-decoration: none; + color: black; + margin-right: 20px; +} + +.navbar-left nav a { + margin-right: 20px; + text-decoration: none; + color: black; + font-weight: 500; + font-family: 'Arial', sans-serif; +} + +.navbar-right a { + margin-left: 20px; + text-decoration: none; + color: black; + font-weight: 500; + font-family: 'Arial', sans-serif; +} + +.quote-container { + display: flex; + justify-content: center; + align-items: center; + flex-grow: 1; +} + +.quote-card { + background-color: white; + padding: 30px; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + max-width: 700px; + position: relative; + text-align: left; +} + +.quote-card h2 { + font-weight: normal; +} + +.quote-card blockquote { + font-size: 2em; + font-family: 'Georgia', serif; + font-style: italic; + color: #4EC2F7; + margin: 0 0 20px 0; + line-height: 1.4; + text-align: left; +} + +.quote-card footer { + font-size: 0.55em; + color: black; + font-family: 'Arial', sans-serif; + font-style: normal; + text-align: left; + font-weight: bold; +} + +.vote-container { + position: absolute; + top: 10px; + right: 10px; + display: flex; + gap: 0em; +} + +.heart-button { + background-color: transparent; + border: none; + cursor: pointer; + padding: 5px; + font-size: 24px; +} + +.heart-button:hover { + background-color: #F0F0F0; +} + +.heart-button:focus { + outline: none; + box-shadow: none; +} + +footer { + background-color: white; + padding-top: 10px; + text-align: center; + border-top: 1px solid #eaeaea; +} + +footer a { + color: #4EC2F7; + text-decoration: none; +} + +.register-login-card { + width: 300px; + margin: 50px auto; + padding: 20px; + border-radius: 8px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); + background-color: #ffffff; + text-align: center; +} + +h2 { + margin-bottom: 20px; + color: #333; +} + +.input-container { + margin-bottom: 15px; + text-align: left; + width: 100%; /* Ensure the container takes the full width */ +} + +label { + display: block; + margin-bottom: 5px; + font-size: 14px; + color: #555; +} + +input { + width: calc(100%); /* Add padding for both left and right */ + padding: 10px; + border-radius: 4px; + border: 1px solid #ccc; + font-size: 14px; + box-sizing: border-box; /* Ensure padding doesn't affect the width */ +} + +input:focus { + outline: none; + border-color: #007bff; +} + +.register-login-button { + width: 100%; + padding: 10px; + background-color: #007bff; + border: none; + border-radius: 4px; + color: white; + font-size: 16px; + cursor: pointer; + margin-top: 10px; +} + +.register-login-button:hover { + background-color: #0056b3; +} + +.error-message { + color: red; +} + +.logout-btn { + margin-left: 20px; + background-color: transparent; + border: none; + cursor: pointer; + font-size: 16px; +} \ No newline at end of file diff --git a/examples/quote-of-the-day/client/src/App.jsx b/examples/quote-of-the-day/client/src/App.jsx new file mode 100644 index 0000000..36b34d6 --- /dev/null +++ b/examples/quote-of-the-day/client/src/App.jsx @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import React from "react"; +import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; +import { ContextProvider } from "./pages/AppContext"; +import Layout from "./Layout"; +import Home from "./pages/Home"; +import Privacy from "./pages/Privacy"; +import Register from "./pages/Register"; +import Login from "./pages/Login"; + + +function App() { + return ( + + + + + } /> + } /> + } /> + } /> + + + + + ); +} + +export default App; diff --git a/examples/quote-of-the-day/client/src/Layout.jsx b/examples/quote-of-the-day/client/src/Layout.jsx new file mode 100644 index 0000000..f1f594a --- /dev/null +++ b/examples/quote-of-the-day/client/src/Layout.jsx @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import React from "react"; +import { useContext } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { AppContext } from "./pages/AppContext"; + +const Layout = ({ children }) => { + const { currentUser, logoutUser } = useContext(AppContext); + const navigate = useNavigate(); + + const handleLogout = () => { + logoutUser(); + navigate("/"); + }; + + return ( +
+
+
+ QuoteOfTheDay + +
+
+ {currentUser ? + ( + <> + Hello, {currentUser}! + + + ) : + ( + <> + Register + Login + + ) + } +
+
+ +
+ {children} +
+ +
+

© 2024 - QuoteOfTheDay - Privacy

+
+
+ ); +}; + +export default Layout; \ No newline at end of file diff --git a/examples/quote-of-the-day/client/src/index.jsx b/examples/quote-of-the-day/client/src/index.jsx new file mode 100644 index 0000000..dc1ea2f --- /dev/null +++ b/examples/quote-of-the-day/client/src/index.jsx @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./App.css"; + +window.addEventListener("beforeunload", (event) => { + // clear the localStorage when the user leaves the page + localStorage.clear() +}); + +const root = ReactDOM.createRoot(document.getElementById("root")); +root.render( + + + +); \ No newline at end of file diff --git a/examples/quote-of-the-day/client/src/pages/AppContext.jsx b/examples/quote-of-the-day/client/src/pages/AppContext.jsx new file mode 100644 index 0000000..de6be88 --- /dev/null +++ b/examples/quote-of-the-day/client/src/pages/AppContext.jsx @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import React from "react"; +import { createContext, useState } from "react"; + +export const AppContext = createContext(); + +export const ContextProvider = ({ children }) => { + const [currentUser, setCurrentUser] = useState(undefined); + + + const loginUser = (user) => { + setCurrentUser(user); + }; + + const logoutUser = () => { + setCurrentUser(undefined); + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/examples/quote-of-the-day/client/src/pages/Home.jsx b/examples/quote-of-the-day/client/src/pages/Home.jsx new file mode 100644 index 0000000..28b09ec --- /dev/null +++ b/examples/quote-of-the-day/client/src/pages/Home.jsx @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import React from "react"; +import { useState, useEffect, useContext } from "react"; +import { FaHeart, FaRegHeart } from "react-icons/fa"; +import { AppContext } from "./AppContext"; + +function Home() { + const { featureManager, currentUser } = useContext(AppContext); + const [liked, setLiked] = useState(false); + const [message, setMessage] = useState(undefined); + + useEffect(() => { + const init = async () => { + const response = await fetch( + `/api/getGreetingMessage?userId=${currentUser ?? ""}`, + { + method: "GET", + } + ); + if (response.ok) { + const result = await response.json(); + setMessage(result.message ?? "Quote of the Day"); // default message is "Quote of the Day" + } else { + console.error("Failed to get greeting message."); + } + setLiked(false); + }; + + init(); + }, [featureManager, currentUser]); + + const handleClick = async () => { + if (!liked) { + try { + const response = await fetch("/api/like", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ userId: currentUser ?? "" }), + }); + + if (response.ok) { + console.log("Like the quote successfully."); + } else { + console.error("Failed to like the quote."); + } + } catch (error) { + console.error("Error:", error); + } + } + setLiked(!liked); + }; + + return ( +
+ { message != undefined ? + ( + <> +

+ <>{message} +

+
+

"You cannot change what you are, only what you do."

+
— Philip Pullman
+
+
+ +
+ + ) + :

Loading

+ } +
+ ); +} + +export default Home; \ No newline at end of file diff --git a/examples/quote-of-the-day/client/src/pages/Login.jsx b/examples/quote-of-the-day/client/src/pages/Login.jsx new file mode 100644 index 0000000..e099abb --- /dev/null +++ b/examples/quote-of-the-day/client/src/pages/Login.jsx @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import React from "react"; +import { useState, useContext } from "react"; +import { useNavigate } from "react-router-dom"; + +import { AppContext } from "./AppContext"; + +const Login = () => { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [message, setMessage] = useState(""); + + const { loginUser } = useContext(AppContext); + const navigate = useNavigate(); + + const handleLogin = (e) => { + + e.preventDefault(); + + // Retrieve user from localStorage + const users = JSON.parse(localStorage.getItem("users")) || []; + const user = users.find((user) => user.username === username && user.password === password); + + if (user) { + loginUser(username); + navigate("/"); + } + else { + setMessage("Invalid username or password!"); + } + }; + + return ( +
+

Login

+
+
+ + setUsername(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+ +
+
+

{message}

+
+
+ ); +}; + +export default Login; \ No newline at end of file diff --git a/examples/quote-of-the-day/client/src/pages/Privacy.jsx b/examples/quote-of-the-day/client/src/pages/Privacy.jsx new file mode 100644 index 0000000..e1b573a --- /dev/null +++ b/examples/quote-of-the-day/client/src/pages/Privacy.jsx @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import React from "react"; + +const Privacy = () => { + return

Use this page to detail your site's privacy policy.

; + }; + +export default Privacy; \ No newline at end of file diff --git a/examples/quote-of-the-day/client/src/pages/Register.jsx b/examples/quote-of-the-day/client/src/pages/Register.jsx new file mode 100644 index 0000000..f585366 --- /dev/null +++ b/examples/quote-of-the-day/client/src/pages/Register.jsx @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import React from "react"; +import { useState, useContext } from "react"; +import { useNavigate } from "react-router-dom"; + +import { AppContext } from "./AppContext"; + +const Register = () => { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [message, setMessage] = useState(""); + + const { loginUser } = useContext(AppContext); + const navigate = useNavigate(); + + const handleRegister = (e) => { + + e.preventDefault(); + + const users = JSON.parse(localStorage.getItem("users")) || []; + const existingUser = users.some((user) => (user.username === username)); + + if (existingUser) { + setMessage("User already exists!"); + } + else { + users.push({ username, password }); + localStorage.setItem("users", JSON.stringify(users)); + loginUser(username); + navigate("/"); + } + }; + + return ( +
+

Register

+
+
+ + setUsername(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+ +
+
+

{message}

+
+
+ ); +}; + +export default Register; \ No newline at end of file diff --git a/examples/quote-of-the-day/client/vite.config.js b/examples/quote-of-the-day/client/vite.config.js new file mode 100644 index 0000000..6c4a25b --- /dev/null +++ b/examples/quote-of-the-day/client/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from "vite" +import react from "@vitejs/plugin-react" + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + build: { + outDir: "../public", + } +}) diff --git a/examples/quote-of-the-day/config.js b/examples/quote-of-the-day/config.js new file mode 100644 index 0000000..9cb0554 --- /dev/null +++ b/examples/quote-of-the-day/config.js @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +require("dotenv").config(); + +// Export configuration variables +module.exports = { + appConfigConnectionString: process.env.APPCONFIG_CONNECTION_STRING, + appInsightsConnectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING, + port: process.env.PORT || "8080" +}; \ No newline at end of file diff --git a/examples/quote-of-the-day/featureManagement.js b/examples/quote-of-the-day/featureManagement.js new file mode 100644 index 0000000..969cb03 --- /dev/null +++ b/examples/quote-of-the-day/featureManagement.js @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +const { load } = require("@azure/app-configuration-provider"); +const { FeatureManager, ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider } = require("@microsoft/feature-management"); +const { createTelemetryPublisher } = require("@microsoft/feature-management-applicationinsights-node"); +const config = require("./config"); + +// Variables to hold the AppConfig and FeatureManager instances +let appConfig; +let featureManager; + +// Initialize AppConfig and FeatureManager +async function initializeFeatureManagement(appInsightsClient, targetingContextAccessor) { + console.log("Loading configuration..."); + appConfig = await load(config.appConfigConnectionString, { + featureFlagOptions: { + enabled: true, + selectors: [ + { + keyFilter: "*" + } + ], + refresh: { + enabled: true, + refreshIntervalInMs: 10_000 + } + } + }); + const featureFlagProvider = new ConfigurationMapFeatureFlagProvider(appConfig); + + // You can also alternatively use local feature flag source. + // const fs = require('fs/promises'); + // const localFeatureFlags = JSON.parse(await fs.readFile("localFeatureFlags.json")); + // const featureFlagProvider = new ConfigurationObjectFeatureFlagProvider(localFeatureFlags); + + const publishTelemetry = createTelemetryPublisher(appInsightsClient); + featureManager = new FeatureManager(featureFlagProvider, { + onFeatureEvaluated: publishTelemetry, + targetingContextAccessor: targetingContextAccessor + }); + + return { featureManager, appConfig }; +} + +// Middleware to refresh configuration before each request +const featureFlagRefreshMiddleware = (req, res, next) => { + // The configuration refresh happens asynchronously to the processing of your app's incoming requests. + // It will not block or slow down the incoming request that triggered the refresh. + // The request that triggered the refresh may not get the updated configuration values, but later requests will get new configuration values. + appConfig?.refresh(); // intended to not await the refresh + next(); +}; + +module.exports = { + initializeFeatureManagement, + featureFlagRefreshMiddleware +}; \ No newline at end of file diff --git a/examples/quote-of-the-day/localFeatureFlags.json b/examples/quote-of-the-day/localFeatureFlags.json new file mode 100644 index 0000000..a89c000 --- /dev/null +++ b/examples/quote-of-the-day/localFeatureFlags.json @@ -0,0 +1,47 @@ +{ + "feature_management": { + "feature_flags": [ + { + "id": "Greeting", + "enabled": true, + "variants": [ + { + "name": "Default" + }, + { + "name": "Simple", + "configuration_value": "Hello!" + }, + { + "name": "Long", + "configuration_value": "I hope this makes your day!" + } + ], + "allocation": { + "percentile": [ + { + "variant": "Default", + "from": 0, + "to": 50 + }, + { + "variant": "Simple", + "from": 50, + "to": 75 + }, + { + "variant": "Long", + "from": 75, + "to": 100 + } + ], + "default_when_enabled": "Default", + "default_when_disabled": "Default" + }, + "telemetry": { + "enabled": true + } + } + ] + } +} \ No newline at end of file diff --git a/examples/quote-of-the-day/package.json b/examples/quote-of-the-day/package.json new file mode 100644 index 0000000..8a46357 --- /dev/null +++ b/examples/quote-of-the-day/package.json @@ -0,0 +1,16 @@ +{ + "name": "quoteoftheday", + "scripts": { + "build-client": "cd client && npm install && npm run build", + "build": "npm install && npm run build-client", + "start": "node server.js" + }, + "dependencies": { + "@azure/app-configuration-provider": "latest", + "@microsoft/feature-management": "2.1.0", + "@microsoft/feature-management-applicationinsights-node": "2.1.0", + "applicationinsights": "^2.9.6", + "dotenv": "^16.5.0", + "express": "^4.19.2" + } +} diff --git a/examples/quote-of-the-day/routes.js b/examples/quote-of-the-day/routes.js new file mode 100644 index 0000000..a3a1186 --- /dev/null +++ b/examples/quote-of-the-day/routes.js @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +const express = require("express"); +const router = express.Router(); + +// Initialize routes with dependencies +function initializeRoutes(featureManager, appInsightsClient) { + // API route to get greeting message with feature variants + router.get("/api/getGreetingMessage", async (req, res) => { + const variant = await featureManager.getVariant("Greeting"); + res.status(200).send({ + message: variant?.configuration + }); + }); + + // API route to track likes + router.post("/api/like", (req, res) => { + const { userId } = req.body; + if (userId === undefined) { + return res.status(400).send({ error: "UserId is required" }); + } + appInsightsClient.trackEvent({ name: "Like" }); + res.status(200).send({ message: "Like event logged successfully" }); + }); + + return router; +} + +module.exports = { initializeRoutes }; \ No newline at end of file diff --git a/examples/quote-of-the-day/server.js b/examples/quote-of-the-day/server.js new file mode 100644 index 0000000..e294854 --- /dev/null +++ b/examples/quote-of-the-day/server.js @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +const config = require("./config"); + +const express = require("express"); +const { targetingContextAccessor, requestStorageMiddleware } = require("./targetingContextAccessor"); +const { initializeAppInsights } = require("./telemetry"); +const { initializeFeatureManagement, featureFlagRefreshMiddleware } = require("./featureManagement"); +const { initializeRoutes } = require("./routes"); + +// Initialize Express server +const server = express(); + +// Initialize Application Insights +const appInsights = initializeAppInsights(targetingContextAccessor); + +// Global variables to store feature manager and app config +let featureManager; + +// Initialize the configuration and start the server +async function startApp() { + try { + // Initialize AppConfig and FeatureManager + const result = await initializeFeatureManagement( + appInsights.defaultClient, + targetingContextAccessor + ); + featureManager = result.featureManager; + + console.log("Configuration loaded. Starting server..."); + + // Set up middleware + server.use(requestStorageMiddleware); + server.use(featureFlagRefreshMiddleware); + server.use(express.json()); + server.use(express.static("public")); + + // Set up routes + const routes = initializeRoutes(featureManager, appInsights.defaultClient); + server.use(routes); + + // Start the server + server.listen(config.port, () => { + console.log(`Server is running at http://localhost:${config.port}`); + }); + } catch (error) { + console.error("Failed to load configuration:", error); + process.exit(1); + } +} + +// Start the application +startApp(); diff --git a/examples/quote-of-the-day/targetingContextAccessor.js b/examples/quote-of-the-day/targetingContextAccessor.js new file mode 100644 index 0000000..a9b8868 --- /dev/null +++ b/examples/quote-of-the-day/targetingContextAccessor.js @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +const { AsyncLocalStorage } = require("async_hooks"); + +// Create AsyncLocalStorage for request access across async operations +const requestAccessor = new AsyncLocalStorage(); + +// Create targeting context accessor to get user information for feature targeting +const targetingContextAccessor = { + getTargetingContext: () => { + const req = requestAccessor.getStore(); + if (req === undefined) { + return undefined; + } + // read user and groups from request + const userId = req.query.userId ?? req.body.userId; + const groups = req.query.groups ?? req.body.groups; + // return an ITargetingContext with the appropriate user info + return { userId: userId, groups: groups ? groups.split(",") : [] }; + } +}; + +// Create middleware to store request in AsyncLocalStorage +const requestStorageMiddleware = (req, res, next) => { + requestAccessor.run(req, next); +}; + +module.exports = { + targetingContextAccessor, + requestStorageMiddleware +}; \ No newline at end of file diff --git a/examples/quote-of-the-day/telemetry.js b/examples/quote-of-the-day/telemetry.js new file mode 100644 index 0000000..80bb2da --- /dev/null +++ b/examples/quote-of-the-day/telemetry.js @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +const config = require('./config'); +const applicationInsights = require("applicationinsights"); +const { createTargetingTelemetryProcessor } = require("@microsoft/feature-management-applicationinsights-node"); + +// Initialize Application Insights +const initializeAppInsights = (targetingContextAccessor) => { + applicationInsights.setup(config.appInsightsConnectionString).start(); + + // Use the targeting telemetry processor to attach targeting id to the telemetry data sent to Application Insights. + applicationInsights.defaultClient.addTelemetryProcessor( + createTargetingTelemetryProcessor(targetingContextAccessor) + ); + + return applicationInsights; +}; + +module.exports = { + initializeAppInsights +}; \ No newline at end of file diff --git a/scripts/build-and-pack.sh b/scripts/build-and-pack.sh index 07f0014..7a76419 100755 --- a/scripts/build-and-pack.sh +++ b/scripts/build-and-pack.sh @@ -5,10 +5,16 @@ set -e SCRIPT_DIR=$(dirname $(readlink -f $0)) PROJECT_BASE_DIR=$(dirname $SCRIPT_DIR) -SDK_DIR="$PROJECT_BASE_DIR/sdk" +NPMRC_FILE="$PROJECT_BASE_DIR/.npmrc" +SRC_DIR="$PROJECT_BASE_DIR/src" PACKAGE="feature-management" -PACKAGE_DIR="$SDK_DIR/$PACKAGE" +PACKAGE_DIR="$SRC_DIR/$PACKAGE" + +if [ -f "$NPMRC_FILE" ]; then + echo "Copy .npmrc file to $PACKAGE_DIR" + cp "$NPMRC_FILE" "$PACKAGE_DIR" +fi echo "Building package $PACKAGE in $PACKAGE_DIR" cd "$PACKAGE_DIR" @@ -29,7 +35,12 @@ echo "copy $PACKAGE package to $PROJECT_BASE_DIR" cp "$PACKAGE_DIR"/*.tgz "$PROJECT_BASE_DIR" PACKAGE="feature-management-applicationinsights-browser" -PACKAGE_DIR="$SDK_DIR/$PACKAGE" +PACKAGE_DIR="$SRC_DIR/$PACKAGE" + +if [ -f "$NPMRC_FILE" ]; then + echo "Copy .npmrc file to $PACKAGE_DIR" + cp "$NPMRC_FILE" "$PACKAGE_DIR" +fi echo "Building package $PACKAGE in $PACKAGE_DIR" cd "$PACKAGE_DIR" @@ -41,4 +52,24 @@ echo "npm pack in $PACKAGE_DIR" npm pack echo "copy $PACKAGE package to $PROJECT_BASE_DIR" -cp "$PACKAGE_DIR"/*.tgz "$PROJECT_BASE_DIR" \ No newline at end of file +cp "$PACKAGE_DIR"/*.tgz "$PROJECT_BASE_DIR" + +PACKAGE="feature-management-applicationinsights-node" +PACKAGE_DIR="$SRC_DIR/$PACKAGE" + +if [ -f "$NPMRC_FILE" ]; then + echo "Copy .npmrc file to $PACKAGE_DIR" + cp "$NPMRC_FILE" "$PACKAGE_DIR" +fi + +echo "Building package $PACKAGE in $PACKAGE_DIR" +cd "$PACKAGE_DIR" + +echo "npm run build in $PACKAGE_DIR" +npm run build + +echo "npm pack in $PACKAGE_DIR" +npm pack + +echo "copy $PACKAGE package to $PROJECT_BASE_DIR" +cp "$PACKAGE_DIR"/*.tgz "$PROJECT_BASE_DIR" diff --git a/sdk/feature-management-applicationinsights-browser/package-lock.json b/sdk/feature-management-applicationinsights-browser/package-lock.json deleted file mode 100644 index 06d0043..0000000 --- a/sdk/feature-management-applicationinsights-browser/package-lock.json +++ /dev/null @@ -1,3076 +0,0 @@ -{ - "name": "@microsoft/feature-management-applicationinsights-browser", - "version": "2.0.0-preview.2", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@microsoft/feature-management-applicationinsights-browser", - "version": "2.0.0-preview.1", - "license": "MIT", - "dependencies": { - "@microsoft/applicationinsights-web": "^3.3.2", - "@microsoft/feature-management": "1.0.0-preview.1" - }, - "devDependencies": { - "@rollup/plugin-typescript": "^11.1.5", - "@types/node": "^20.10.7", - "@typescript-eslint/eslint-plugin": "^6.18.1", - "@typescript-eslint/parser": "^6.18.1", - "eslint": "^8.56.0", - "rimraf": "^5.0.5", - "rollup": "^4.9.4", - "rollup-plugin-dts": "^6.1.0", - "tslib": "^2.6.2", - "typescript": "^5.3.3" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@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/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@microsoft/applicationinsights-analytics-js": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-3.3.2.tgz", - "integrity": "sha512-btFX0IfmKfIZRWQVIRMfYqvLSmsYUTQhx3hcbAgPhNSfLlm1AXko4+dIiE5uBKdXa0iGcHXM4g95/SwKFVd2+w==", - "license": "MIT", - "dependencies": { - "@microsoft/applicationinsights-common": "3.3.2", - "@microsoft/applicationinsights-core-js": "3.3.2", - "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" - }, - "peerDependencies": { - "tslib": ">= 1.0.0" - } - }, - "node_modules/@microsoft/applicationinsights-cfgsync-js": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-cfgsync-js/-/applicationinsights-cfgsync-js-3.3.2.tgz", - "integrity": "sha512-ns6QjGyHT82EZ+LXnisAKyrZW3JcHfkhV9f810c0TF0SlRcyJKxMRoLTtaVIz4FHnKS9Jp/O2DeM6F2xL009lg==", - "license": "MIT", - "dependencies": { - "@microsoft/applicationinsights-common": "3.3.2", - "@microsoft/applicationinsights-core-js": "3.3.2", - "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" - }, - "peerDependencies": { - "tslib": ">= 1.0.0" - } - }, - "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.2.tgz", - "integrity": "sha512-ic1oCQgGzitCLMYMHOgGuQvqd8G81s8R7ynI0NBgXc7pkzGiatCkLGx2lDiIYTYWV+Cd/3+cJwPB+3N8/yTGew==", - "license": "MIT", - "dependencies": { - "@microsoft/applicationinsights-common": "3.3.2", - "@microsoft/applicationinsights-core-js": "3.3.2", - "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" - }, - "peerDependencies": { - "tslib": ">= 1.0.0" - } - }, - "node_modules/@microsoft/applicationinsights-common": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.2.tgz", - "integrity": "sha512-RR6LpIC9JLQGTcgUaAXhuIZW69W00T6sF7GC6LvLy5PRjvg4m3MqflpQfNHt7yGy72ZHFG/TpLH5zJIghrNRew==", - "license": "MIT", - "dependencies": { - "@microsoft/applicationinsights-core-js": "3.3.2", - "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" - }, - "peerDependencies": { - "tslib": ">= 1.0.0" - } - }, - "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.2.tgz", - "integrity": "sha512-Jo3Mkl6zPswjS90SBbe+TxAOjQqHbwpD00VPca1ZeICc+31dbVNIBgkK42G0DbMB/ml1kqwkFV2/kpAH8QJCrw==", - "license": "MIT", - "dependencies": { - "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" - }, - "peerDependencies": { - "tslib": ">= 1.0.0" - } - }, - "node_modules/@microsoft/applicationinsights-dependencies-js": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-dependencies-js/-/applicationinsights-dependencies-js-3.3.2.tgz", - "integrity": "sha512-Zd3BI7vQ96kIGiDOxX7tmNUnpSVBmx1OG8Vi3vappo3oswyiQWdOI3cwEfEeQil1qiODZOYTr8WRMSuTC9zQWg==", - "license": "MIT", - "dependencies": { - "@microsoft/applicationinsights-common": "3.3.2", - "@microsoft/applicationinsights-core-js": "3.3.2", - "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" - }, - "peerDependencies": { - "tslib": ">= 1.0.0" - } - }, - "node_modules/@microsoft/applicationinsights-properties-js": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-properties-js/-/applicationinsights-properties-js-3.3.2.tgz", - "integrity": "sha512-tG96jiVwD5Z+b684O8hYdqXtEMEAev84kE7P35uYzHPmBKHgCJ4y6rmZQOyxaqTkpGZ8ahcFNzcxJgh+p0ahvQ==", - "license": "MIT", - "dependencies": { - "@microsoft/applicationinsights-common": "3.3.2", - "@microsoft/applicationinsights-core-js": "3.3.2", - "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" - }, - "peerDependencies": { - "tslib": ">= 1.0.0" - } - }, - "node_modules/@microsoft/applicationinsights-shims": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", - "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", - "license": "MIT", - "dependencies": { - "@nevware21/ts-utils": ">= 0.9.4 < 2.x" - } - }, - "node_modules/@microsoft/applicationinsights-web": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web/-/applicationinsights-web-3.3.2.tgz", - "integrity": "sha512-qixMWKoymNtqvEzHkxpD6brABbejjUA/pFS9FRSeYAzGpNY0pfEWmlmuFfenqasphXl+5R+81TG0fS+n6fGWmQ==", - "license": "MIT", - "dependencies": { - "@microsoft/applicationinsights-analytics-js": "3.3.2", - "@microsoft/applicationinsights-cfgsync-js": "3.3.2", - "@microsoft/applicationinsights-channel-js": "3.3.2", - "@microsoft/applicationinsights-common": "3.3.2", - "@microsoft/applicationinsights-core-js": "3.3.2", - "@microsoft/applicationinsights-dependencies-js": "3.3.2", - "@microsoft/applicationinsights-properties-js": "3.3.2", - "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" - }, - "peerDependencies": { - "tslib": ">= 1.0.0" - } - }, - "node_modules/@microsoft/dynamicproto-js": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", - "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", - "license": "MIT", - "dependencies": { - "@nevware21/ts-utils": ">= 0.10.4 < 2.x" - } - }, - "node_modules/@microsoft/feature-management": { - "version": "1.0.0-preview.1", - "resolved": "https://registry.npmjs.org/@microsoft/feature-management/-/feature-management-1.0.0-preview.1.tgz", - "integrity": "sha512-P26Buw6L27SYo/ODsOaf2CcYbvyMWjq0hMc9HyXgOO5t9Ze599QCPJwE4hlat9gIn6ZnpDLaO31ndop8CdzLnw==", - "dependencies": { - "chai-as-promised": "^7.1.1" - } - }, - "node_modules/@nevware21/ts-async": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.2.tgz", - "integrity": "sha512-Zf2vUNjCw2vJsiVKhWXA9hCNHsn59AOSGa5jGP4tWrp/vTH9XrI4eG/65khuoAgrS83migj0Xv5/j6fUAz69Zw==", - "license": "MIT", - "dependencies": { - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" - } - }, - "node_modules/@nevware21/ts-utils": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.3.tgz", - "integrity": "sha512-oipW+tyKN68bREjoESYAzOZiatM+1LF+ez1TX3BaeinhCkI18xsLgmpH9tvwHaVgKf1Tsth25sdbXVtYmgRYvQ==", - "license": "MIT" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "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/@rollup/plugin-typescript": { - "version": "11.1.6", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz", - "integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.1.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.14.0||^3.0.0||^4.0.0", - "tslib": "*", - "typescript": ">=3.7.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - }, - "tslib": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", - "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz", - "integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz", - "integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz", - "integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz", - "integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz", - "integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz", - "integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz", - "integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz", - "integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz", - "integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz", - "integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz", - "integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz", - "integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz", - "integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz", - "integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz", - "integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz", - "integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", - "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "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/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/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "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/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chai": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", - "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", - "peer": true, - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/chai-as-promised": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz", - "integrity": "sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==", - "dependencies": { - "check-error": "^1.0.2" - }, - "peerDependencies": { - "chai": ">= 2.1.2 < 6" - } - }, - "node_modules/chai-as-promised/node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "peer": true, - "engines": { - "node": ">= 16" - } - }, - "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/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "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": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "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/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/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flat-cache/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/flat-cache/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/flat-cache/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/flat-cache/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true, - "license": "ISC" - }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "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==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "engines": { - "node": "*" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "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/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/loupe": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", - "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", - "peer": true - }, - "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/magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "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-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", - "peer": true, - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "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/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz", - "integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.3", - "@rollup/rollup-android-arm64": "4.21.3", - "@rollup/rollup-darwin-arm64": "4.21.3", - "@rollup/rollup-darwin-x64": "4.21.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.3", - "@rollup/rollup-linux-arm-musleabihf": "4.21.3", - "@rollup/rollup-linux-arm64-gnu": "4.21.3", - "@rollup/rollup-linux-arm64-musl": "4.21.3", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.3", - "@rollup/rollup-linux-riscv64-gnu": "4.21.3", - "@rollup/rollup-linux-s390x-gnu": "4.21.3", - "@rollup/rollup-linux-x64-gnu": "4.21.3", - "@rollup/rollup-linux-x64-musl": "4.21.3", - "@rollup/rollup-win32-arm64-msvc": "4.21.3", - "@rollup/rollup-win32-ia32-msvc": "4.21.3", - "@rollup/rollup-win32-x64-msvc": "4.21.3", - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup-plugin-dts": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.1.1.tgz", - "integrity": "sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA==", - "dev": true, - "license": "LGPL-3.0-only", - "dependencies": { - "magic-string": "^0.30.10" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/Swatinem" - }, - "optionalDependencies": { - "@babel/code-frame": "^7.24.2" - }, - "peerDependencies": { - "rollup": "^3.29.4 || ^4", - "typescript": "^4.5 || ^5.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "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": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "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/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/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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/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/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "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-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-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", - "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "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/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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/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/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/sdk/feature-management-applicationinsights-browser/src/telemetry.ts b/sdk/feature-management-applicationinsights-browser/src/telemetry.ts deleted file mode 100644 index 10b1c76..0000000 --- a/sdk/feature-management-applicationinsights-browser/src/telemetry.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { EvaluationResult } from "@microsoft/feature-management"; -import { ApplicationInsights } from "@microsoft/applicationinsights-web"; -import { IEventTelemetry } from "@microsoft/applicationinsights-web"; - -/** - * Creates a telemetry publisher that sends feature evaluation events to Application Insights. - * @param client The Application Insights telemetry client. - * @returns A callback function that takes an evaluation result and tracks an event with the evaluation details. - */ -export function createTelemetryPublisher(client: ApplicationInsights): (event: EvaluationResult) => void { - return (event: EvaluationResult) => { - const eventProperties = { - "FeatureName": event.feature ? event.feature.id : "", - "Enabled": event.enabled.toString(), - // Ensure targetingId is string so that it will be placed in customDimensions - "TargetingId": event.targetingId ? event.targetingId.toString() : "", - "Variant": event.variant ? event.variant.name : "", - "VariantAssignmentReason": event.variantAssignmentReason, - }; - - const metadata = event.feature?.telemetry?.metadata; - if (metadata) { - for (const key in metadata) { - if (!(key in eventProperties)) { - eventProperties[key] = metadata[key]; - } - } - } - - client.trackEvent({ name: "FeatureEvaluation" }, eventProperties); - }; -} - -/** - * Tracks a custom event using Application Insights, ensuring that the "TargetingId" - * is included in the custom properties. If the "TargetingId" already exists in - * the provided custom properties, it will be overwritten. - * - * @param client The Application Insights client instance used to track the event. - * @param targetingId The unique targeting identifier that will be included in the custom properties. - * @param event The event telemetry object to be tracked, containing event details. - * @param customProperties (Optional) Additional properties to include in the event telemetry. - */ -export function trackEvent(client: ApplicationInsights, targetingId: string, event: IEventTelemetry, customProperties?: {[key: string]: any}): void { - const properties = customProperties ? { ...customProperties } : {}; - // Ensure targetingId is string so that it will be placed in customDimensions - properties["TargetingId"] = targetingId ? targetingId.toString() : ""; - client.trackEvent(event, properties); -} diff --git a/sdk/feature-management/src/common/ITargetingContext.ts b/sdk/feature-management/src/common/ITargetingContext.ts deleted file mode 100644 index 1d5a426..0000000 --- a/sdk/feature-management/src/common/ITargetingContext.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -export interface ITargetingContext { - userId?: string; - groups?: string[]; -} - diff --git a/sdk/feature-management/src/model.ts b/sdk/feature-management/src/model.ts deleted file mode 100644 index ccdb9d5..0000000 --- a/sdk/feature-management/src/model.ts +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -// Converted from: -// https://github.com/Azure/AppConfiguration/blob/6e544296a5607f922a423df165f60801717c7800/docs/FeatureManagement/FeatureFlag.v2.0.0.schema.json - -/** - * A feature flag is a named property that can be toggled to enable or disable some feature of an application. - */ -export interface FeatureFlag { - /** - * An ID used to uniquely identify and reference the feature. - */ - id: string; - /** - * A description of the feature. - */ - description?: string; - /** - * A display name for the feature to use for display rather than the ID. - */ - display_name?: string; - /** - * A feature is OFF if enabled is false. If enabled is true, then the feature is ON if there are no conditions (null or empty) or if the conditions are satisfied. - */ - enabled?: boolean; - /** - * The declaration of conditions used to dynamically enable the feature. - */ - conditions?: FeatureEnablementConditions; - /** - * The list of variants defined for this feature. A variant represents a configuration value of a feature flag that can be a string, a number, a boolean, or a JSON object. - */ - variants?: VariantDefinition[]; - /** - * Determines how variants should be allocated for the feature to various users. - */ - allocation?: VariantAllocation; - /** - * The declaration of options used to configure telemetry for this feature. - */ - telemetry?: TelemetryOptions -} - -/** -* The declaration of conditions used to dynamically enable the feature -*/ -interface FeatureEnablementConditions { - /** - * Determines whether any or all registered client filters must be evaluated as true for the feature to be considered enabled. - */ - requirement_type?: RequirementType; - /** - * Filters that must run on the client and be evaluated as true for the feature to be considered enabled. - */ - client_filters?: ClientFilter[]; -} - -export type RequirementType = "Any" | "All"; - -interface ClientFilter { - /** - * The name used to refer to a client filter. - */ - name: string; - /** - * Parameters for a given client filter. A client filter can require any set of parameters of any type. - */ - parameters?: Record; -} - -export interface VariantDefinition { - /** - * The name used to refer to a feature variant. - */ - name: string; - /** - * The configuration value for this feature variant. - */ - configuration_value?: unknown; - /** - * Overrides the enabled state of the feature if the given variant is assigned. Does not override the state if value is None. - */ - status_override?: "None" | "Enabled" | "Disabled"; -} - -/** -* Determines how variants should be allocated for the feature to various users. -*/ -interface VariantAllocation { - /** - * Specifies which variant should be used when the feature is considered disabled. - */ - default_when_disabled?: string; - /** - * Specifies which variant should be used when the feature is considered enabled and no other allocation rules are applicable. - */ - default_when_enabled?: string; - /** - * A list of objects, each containing a variant name and list of users for whom that variant should be used. - */ - user?: UserAllocation[]; - /** - * A list of objects, each containing a variant name and list of groups for which that variant should be used. - */ - group?: GroupAllocation[]; - /** - * A list of objects, each containing a variant name and percentage range for which that variant should be used. - */ - percentile?: PercentileAllocation[] - /** - * The value percentile calculations are based on. The calculated percentile is consistent across features for a given user if the same nonempty seed is used. - */ - seed?: string; -} - -interface UserAllocation { - /** - * The name of the variant to use if the user allocation matches the current user. - */ - variant: string; - /** - * Collection of users where if any match the current user, the variant specified in the user allocation is used. - */ - users: string[]; -} - -interface GroupAllocation { - /** - * The name of the variant to use if the group allocation matches a group the current user is in. - */ - variant: string; - /** - * Collection of groups where if the current user is in any of these groups, the variant specified in the group allocation is used. - */ - groups: string[]; -} - -interface PercentileAllocation { - /** - * The name of the variant to use if the calculated percentile for the current user falls in the provided range. - */ - variant: string; - /** - * The lower end of the percentage range for which this variant will be used. - */ - from: number; - /** - * The upper end of the percentage range for which this variant will be used. - */ - to: number; -} - -/** -* The declaration of options used to configure telemetry for this feature. -*/ -interface TelemetryOptions { - /** - * Indicates if telemetry is enabled. - */ - enabled?: boolean; - /** - * A container for metadata that should be bundled with flag telemetry. - */ - metadata?: Record; -} - -// Feature Management Section fed into feature manager. -// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json - -export const FEATURE_MANAGEMENT_KEY = "feature_management"; -export const FEATURE_FLAGS_KEY = "feature_flags"; - -export interface FeatureManagementConfiguration { - feature_management: FeatureManagement -} - -/** - * Declares feature management configuration. - */ -export interface FeatureManagement { - feature_flags: FeatureFlag[]; -} diff --git a/sdk/feature-management/test/featureManager.test.ts b/sdk/feature-management/test/featureManager.test.ts deleted file mode 100644 index 74723bc..0000000 --- a/sdk/feature-management/test/featureManager.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import * as chai from "chai"; -import * as chaiAsPromised from "chai-as-promised"; -import { FeatureManager, ConfigurationObjectFeatureFlagProvider, ConfigurationMapFeatureFlagProvider } from "../"; -chai.use(chaiAsPromised); -const expect = chai.expect; - -describe("feature manager", () => { - it("should load from json string", () => { - const jsonObject = { - "feature_management": { - "feature_flags": [ - { "id": "Alpha", "description": "", "enabled": true} - ] - } - }; - - const provider = new ConfigurationObjectFeatureFlagProvider(jsonObject); - const featureManager = new FeatureManager(provider); - return expect(featureManager.isEnabled("Alpha")).eventually.eq(true); - }); - - it("should load from map", () => { - const dataSource = new Map(); - dataSource.set("feature_management", { - feature_flags: [ - { id: "Alpha", enabled: true } - ], - }); - - const provider = new ConfigurationMapFeatureFlagProvider(dataSource); - const featureManager = new FeatureManager(provider); - return expect(featureManager.isEnabled("Alpha")).eventually.eq(true); - }); - - it("should load latest data if source is updated after initialization", () => { - const dataSource = new Map(); - dataSource.set("feature_management", { - feature_flags: [ - { id: "Alpha", enabled: true } - ], - }); - - const provider = new ConfigurationMapFeatureFlagProvider(dataSource); - const featureManager = new FeatureManager(provider); - dataSource.set("feature_management", { - feature_flags: [ - { id: "Alpha", enabled: false } - ], - }); - - return expect(featureManager.isEnabled("Alpha")).eventually.eq(false); - }); - - it("should evaluate features without conditions", () => { - const dataSource = new Map(); - dataSource.set("feature_management", { - feature_flags: [ - { "id": "Alpha", "description": "", "enabled": true, "conditions": { "client_filters": [] } }, - { "id": "Beta", "description": "", "enabled": false, "conditions": { "client_filters": [] } } - ], - }); - - const provider = new ConfigurationMapFeatureFlagProvider(dataSource); - const featureManager = new FeatureManager(provider); - return Promise.all([ - expect(featureManager.isEnabled("Alpha")).eventually.eq(true), - expect(featureManager.isEnabled("Beta")).eventually.eq(false) - ]); - }); - - it("should evaluate features with conditions"); - it("should override default filters with custom filters"); -}); diff --git a/sdk/feature-management/test/sampleFeatureFlags.ts b/sdk/feature-management/test/sampleFeatureFlags.ts deleted file mode 100644 index 9f95b36..0000000 --- a/sdk/feature-management/test/sampleFeatureFlags.ts +++ /dev/null @@ -1,532 +0,0 @@ -export enum Features { - VariantFeatureDefaultDisabled = "VariantFeatureDefaultDisabled", - VariantFeatureDefaultEnabled = "VariantFeatureDefaultEnabled", - VariantFeaturePercentileOn = "VariantFeaturePercentileOn", - VariantFeaturePercentileOff = "VariantFeaturePercentileOff", - VariantFeatureAlwaysOff = "VariantFeatureAlwaysOff", - VariantFeatureUser = "VariantFeatureUser", - VariantFeatureGroup = "VariantFeatureGroup", - VariantFeatureNoVariants = "VariantFeatureNoVariants", - VariantFeatureNoAllocation = "VariantFeatureNoAllocation", - VariantFeatureAlwaysOffNoAllocation = "VariantFeatureAlwaysOffNoAllocation", - VariantFeatureBothConfigurations = "VariantFeatureBothConfigurations", - VariantFeatureInvalidStatusOverride = "VariantFeatureInvalidStatusOverride", - VariantFeatureInvalidFromTo = "VariantFeatureInvalidFromTo", - VariantImplementationFeature = "VariantImplementationFeature", -} - -export const featureFlagsConfigurationObject = { - "feature_management": { - "feature_flags": [ - { - "id": "OnTestFeature", - "enabled": true - }, - { - "id": "OffTestFeature", - "enabled": false - }, - { - "id": "ConditionalFeature", - "enabled": true, - "conditions": { - "client_filters": [ - { - "name": "Test", - "parameters": { - "P1": "V1" - } - } - ] - } - }, - { - "id": "ContextualFeature", - "enabled": true, - "conditions": { - "client_filters": [ - { - "name": "ContextualTest", - "parameters": { - "AllowedAccounts": [ - "abc" - ] - } - } - ] - } - }, - { - "id": "AnyFilterFeature", - "enabled": true, - "conditions": { - "requirement_type": "Any", - "client_filters": [ - { - "name": "Test", - "parameters": { - "Id": "1" - } - }, - { - "name": "Test", - "parameters": { - "Id": "2" - } - } - ] - } - }, - { - "id": "AllFilterFeature", - "enabled": true, - "conditions": { - "requirement_type": "All", - "client_filters": [ - { - "name": "Test", - "parameters": { - "Id": "1" - } - }, - { - "name": "Test", - "parameters": { - "Id": "2" - } - } - ] - } - }, - { - "id": "FeatureUsesFiltersWithDuplicatedAlias", - "enabled": true, - "conditions": { - "client_filters": [ - { - "name": "DuplicatedFilterName" - }, - { - "name": "Percentage", - "parameters": { - "Value": 100 - } - } - ] - } - }, - { - "id": "TargetingTestFeature", - "enabled": true, - "conditions": { - "client_filters": [ - { - "name": "Targeting", - "parameters": { - "Audience": { - "Users": [ - "Jeff", - "Alicia" - ], - "Groups": [ - { - "Name": "Ring0", - "RolloutPercentage": 100 - }, - { - "Name": "Ring1", - "RolloutPercentage": 50 - } - ], - "DefaultRolloutPercentage": 20 - } - } - } - ] - } - }, - { - "id": "TargetingTestFeatureWithExclusion", - "enabled": true, - "conditions": { - "client_filters": [ - { - "name": "Targeting", - "parameters": { - "Audience": { - "Users": [ - "Jeff", - "Alicia" - ], - "Groups": [ - { - "Name": "Ring0", - "RolloutPercentage": 100 - }, - { - "Name": "Ring1", - "RolloutPercentage": 50 - } - ], - "DefaultRolloutPercentage": 20, - "Exclusion": { - "Users": [ - "Jeff" - ], - "Groups": [ - "Ring0", - "Ring2" - ] - } - } - } - } - ] - } - }, - { - "id": "CustomFilterFeature", - "enabled": true, - "conditions": { - "client_filters": [ - { - "name": "CustomTargetingFilter", - "parameters": { - "Audience": { - "Users": [ - "Jeff" - ] - } - } - } - ] - } - }, - { - "id": "VariantFeaturePercentileOn", - "enabled": true, - "variants": [ - { - "name": "Big", - "status_override": "Disabled" - } - ], - "allocation": { - "percentile": [ - { - "variant": "Big", - "from": 0, - "to": 50 - } - ], - "seed": 1234 - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeaturePercentileOff", - "enabled": true, - "variants": [ - { - "name": "Big" - } - ], - "allocation": { - "percentile": [ - { - "variant": "Big", - "from": 0, - "to": 50 - } - ], - "seed": 12345 - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureAlwaysOff", - "enabled": false, - "variants": [ - { - "name": "Big" - } - ], - "allocation": { - "percentile": [ - { - "variant": "Big", - "from": 0, - "to": 100 - } - ], - "seed": 12345 - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureDefaultDisabled", - "enabled": false, - "variants": [ - { - "name": "Small", - "configuration_value": "300px" - } - ], - "allocation": { - "default_when_disabled": "Small" - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureDefaultEnabled", - "enabled": true, - "variants": [ - { - "name": "Medium", - "configuration_value": { - "Size": "450px", - "Color": "Purple" - } - }, - { - "name": "Small", - "configuration_value": "300px" - } - ], - "allocation": { - "default_when_enabled": "Medium", - "user": [ - { - "variant": "Small", - "users": [ - "Jeff" - ] - } - ] - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureUser", - "enabled": true, - "variants": [ - { - "name": "Small", - "configuration_value": "300px" - } - ], - "allocation": { - "user": [ - { - "variant": "Small", - "users": [ - "Marsha" - ] - } - ] - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureGroup", - "enabled": true, - "variants": [ - { - "name": "Small", - "configuration_value": "300px" - } - ], - "allocation": { - "group": [ - { - "variant": "Small", - "groups": [ - "Group1" - ] - } - ] - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureNoVariants", - "enabled": true, - "variants": [], - "allocation": { - "user": [ - { - "variant": "Small", - "users": [ - "Marsha" - ] - } - ] - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureNoAllocation", - "enabled": true, - "variants": [ - { - "name": "Small", - "configuration_value": "300px" - } - ], - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureAlwaysOffNoAllocation", - "enabled": false, - "variants": [ - { - "name": "Small", - "configuration_value": "300px" - } - ], - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureBothConfigurations", - "enabled": true, - "variants": [ - { - "name": "Small", - "configuration_value": "600px" - } - ], - "allocation": { - "default_when_enabled": "Small" - } - }, - { - "id": "VariantFeatureInvalidStatusOverride", - "enabled": true, - "variants": [ - { - "name": "Small", - "configuration_value": "300px", - "status_override": "InvalidValue" - } - ], - "allocation": { - "default_when_enabled": "Small" - } - }, - { - "id": "VariantFeatureInvalidFromTo", - "enabled": true, - "variants": [ - { - "name": "Small", - "configuration_value": "300px" - } - ], - "allocation": { - "percentile": [ - { - "variant": "Small", - "from": "Invalid", - "to": "Invalid" - } - ] - } - }, - { - "id": "VariantImplementationFeature", - "enabled": true, - "conditions": { - "client_filters": [ - { - "name": "Targeting", - "parameters": { - "Audience": { - "Users": [ - "UserOmega", - "UserSigma", - "UserBeta" - ] - } - } - } - ] - }, - "variants": [ - { - "name": "AlgorithmBeta" - }, - { - "name": "Sigma", - "configuration_value": "AlgorithmSigma" - }, - { - "name": "Omega" - } - ], - "allocation": { - "user": [ - { - "variant": "AlgorithmBeta", - "users": [ - "UserBeta" - ] - }, - { - "variant": "Omega", - "users": [ - "UserOmega" - ] - }, - { - "variant": "Sigma", - "users": [ - "UserSigma" - ] - } - ] - } - }, - { - "id": "OnTelemetryTestFeature", - "enabled": true, - "telemetry": { - "enabled": true, - "metadata": { - "Tags.Tag1": "Tag1Value", - "Tags.Tag2": "Tag2Value", - "Etag": "EtagValue", - "Label": "LabelValue" - } - } - }, - { - "id": "OffTelemetryTestFeature", - "enabled": false, - "telemetry": { - "enabled": true - } - } - ] - } -}; - diff --git a/sdk/feature-management/.eslintrc b/src/feature-management-applicationinsights-browser/.eslintrc similarity index 96% rename from sdk/feature-management/.eslintrc rename to src/feature-management-applicationinsights-browser/.eslintrc index 27a13cc..7271d78 100644 --- a/sdk/feature-management/.eslintrc +++ b/src/feature-management-applicationinsights-browser/.eslintrc @@ -1,9 +1,7 @@ { "env": { "browser": true, - "commonjs": true, "es2021": true, - "node": true, "mocha": true }, "extends": [ diff --git a/sdk/feature-management-applicationinsights-browser/LICENSE b/src/feature-management-applicationinsights-browser/LICENSE similarity index 100% rename from sdk/feature-management-applicationinsights-browser/LICENSE rename to src/feature-management-applicationinsights-browser/LICENSE diff --git a/sdk/feature-management-applicationinsights-browser/README.md b/src/feature-management-applicationinsights-browser/README.md similarity index 83% rename from sdk/feature-management-applicationinsights-browser/README.md rename to src/feature-management-applicationinsights-browser/README.md index 15bfc3d..e55f448 100644 --- a/sdk/feature-management-applicationinsights-browser/README.md +++ b/src/feature-management-applicationinsights-browser/README.md @@ -1,22 +1,20 @@ # Microsoft Feature Management Application Insights Plugin for Browser -Feature Management Application Insights Plugin for Browser provides a solution for sending feature flag evaluation events produced by the Feature Management library. +Feature Management Application Insights Plugin for Browser provides a solution for sending feature flag evaluation telemetry produced by the [`@microsoft/feature-management`](https://www.npmjs.com/package/@microsoft/feature-management) library. ## Getting Started -### Prerequisites - -- Node.js LTS version +For more information, please go to [Feature reference](https://learn.microsoft.com/azure/azure-app-configuration/feature-management-javascript-reference#application-insights-integration). ### Usage ``` javascript import { ApplicationInsights } from "@microsoft/applicationinsights-web" import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "@microsoft/feature-management"; -import { trackEvent, createTelemetryPublisher } from "@microsoft/feature-management-applicationinsights-browser"; +import { createTelemetryPublisher, trackEvent } from "@microsoft/feature-management-applicationinsights-browser"; const appInsights = new ApplicationInsights({ config: { - connectionString: CONNECTION_STRING + connectionString: "" }}); appInsights.loadAppInsights(); diff --git a/sdk/feature-management-applicationinsights-browser/package.json b/src/feature-management-applicationinsights-browser/package.json similarity index 89% rename from sdk/feature-management-applicationinsights-browser/package.json rename to src/feature-management-applicationinsights-browser/package.json index 7b57fa3..00f792e 100644 --- a/sdk/feature-management-applicationinsights-browser/package.json +++ b/src/feature-management-applicationinsights-browser/package.json @@ -1,9 +1,10 @@ { "name": "@microsoft/feature-management-applicationinsights-browser", - "version": "2.0.0-preview.2", + "version": "2.1.0", "description": "Feature Management Application Insights Plugin for Browser provides a solution for sending feature flag evaluation events produced by the Feature Management library.", - "main": "./dist/umd/index.js", + "main": "./dist/esm/index.js", "module": "./dist/esm/index.js", + "type": "module", "types": "types/index.d.ts", "files": [ "dist/", @@ -24,6 +25,9 @@ "url": "git+https://github.com/microsoft/FeatureManagement-JavaScript.git" }, "license": "MIT", + "publishConfig": { + "tag": "latest" + }, "bugs": { "url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues" }, @@ -42,7 +46,7 @@ }, "dependencies": { "@microsoft/applicationinsights-web": "^3.3.2", - "@microsoft/feature-management": "2.0.0-preview.2" + "@microsoft/feature-management": "2.1.0" } } \ No newline at end of file diff --git a/sdk/feature-management-applicationinsights-browser/rollup.config.mjs b/src/feature-management-applicationinsights-browser/rollup.config.mjs similarity index 84% rename from sdk/feature-management-applicationinsights-browser/rollup.config.mjs rename to src/feature-management-applicationinsights-browser/rollup.config.mjs index 4dcb837..7d1c851 100644 --- a/sdk/feature-management-applicationinsights-browser/rollup.config.mjs +++ b/src/feature-management-applicationinsights-browser/rollup.config.mjs @@ -4,6 +4,7 @@ import dts from "rollup-plugin-dts"; export default [ { + external: ["@microsoft/feature-management"], input: "src/index.ts", output: [ { @@ -11,12 +12,6 @@ export default [ format: "esm", sourcemap: true, preserveModules: true, - }, - { - file: "dist/umd/index.js", - format: "umd", - name: 'FeatureManagementApplicationInsights', - sourcemap: true } ], plugins: [ @@ -43,5 +38,5 @@ export default [ input: "src/index.ts", output: [{ file: "types/index.d.ts", format: "esm" }], plugins: [dts()], - }, + } ]; diff --git a/src/feature-management-applicationinsights-browser/src/index.ts b/src/feature-management-applicationinsights-browser/src/index.ts new file mode 100644 index 0000000..efbc293 --- /dev/null +++ b/src/feature-management-applicationinsights-browser/src/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export { createTargetingTelemetryInitializer, createTelemetryPublisher, trackEvent } from "./telemetry.js"; +export { VERSION } from "./version.js"; diff --git a/src/feature-management-applicationinsights-browser/src/telemetry.ts b/src/feature-management-applicationinsights-browser/src/telemetry.ts new file mode 100644 index 0000000..3b9cfad --- /dev/null +++ b/src/feature-management-applicationinsights-browser/src/telemetry.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { EvaluationResult, createFeatureEvaluationEventProperties, ITargetingContextAccessor } from "@microsoft/feature-management"; +import { ApplicationInsights, IEventTelemetry, ITelemetryItem } from "@microsoft/applicationinsights-web"; + +const TARGETING_ID = "TargetingId"; +const FEATURE_EVALUATION_EVENT_NAME = "FeatureEvaluation"; + +/** + * Creates a telemetry publisher that sends feature evaluation events to Application Insights. + * @param client The Application Insights telemetry client. + * @returns A callback function that takes an evaluation result and tracks an event with the evaluation details. + */ +export function createTelemetryPublisher(client: ApplicationInsights): (result: EvaluationResult) => void { + return (result: EvaluationResult) => { + if (result.feature === undefined) { + return; + } + + const eventProperties = createFeatureEvaluationEventProperties(result); + client.trackEvent({ name: FEATURE_EVALUATION_EVENT_NAME }, eventProperties); + }; +} + +/** + * Tracks a custom event using Application Insights, ensuring that the "TargetingId" + * is included in the custom properties. If the "TargetingId" already exists in + * the provided custom properties, it will be overwritten. + * + * @param client The Application Insights client instance used to track the event. + * @param targetingId The unique targeting identifier that will be included in the custom properties. + * @param event The event telemetry object to be tracked, containing event details. + * @param customProperties (Optional) Additional properties to include in the event telemetry. + */ +export function trackEvent(client: ApplicationInsights, targetingId: string, event: IEventTelemetry, customProperties?: {[key: string]: any}): void { + const properties = customProperties ? { ...customProperties } : {}; + // Ensure targetingId is string so that it will be placed in customDimensions + properties[TARGETING_ID] = targetingId ? targetingId.toString() : ""; + client.trackEvent(event, properties); +} + +/** + * Creates a telemetry initializer that adds targeting id to telemetry item's custom properties. + * @param targetingContextAccessor The accessor function to get the targeting context. + * @returns A telemetry initializer that attaches targeting id to telemetry items. + */ +export function createTargetingTelemetryInitializer(targetingContextAccessor: ITargetingContextAccessor): (item: ITelemetryItem) => void { + return (item: ITelemetryItem) => { + const targetingContext = targetingContextAccessor.getTargetingContext(); + if (targetingContext !== undefined) { + if (targetingContext?.userId === undefined) { + console.warn("Targeting id is undefined."); + } + item.data = {...item.data, [TARGETING_ID]: targetingContext?.userId || ""}; + } + }; +} diff --git a/sdk/feature-management-applicationinsights-browser/src/version.ts b/src/feature-management-applicationinsights-browser/src/version.ts similarity index 64% rename from sdk/feature-management-applicationinsights-browser/src/version.ts rename to src/feature-management-applicationinsights-browser/src/version.ts index 551bb5c..0200538 100644 --- a/sdk/feature-management-applicationinsights-browser/src/version.ts +++ b/src/feature-management-applicationinsights-browser/src/version.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "2.0.0-preview.2"; +export const VERSION = "2.1.0"; diff --git a/sdk/feature-management-applicationinsights-browser/tsconfig.json b/src/feature-management-applicationinsights-browser/tsconfig.json similarity index 100% rename from sdk/feature-management-applicationinsights-browser/tsconfig.json rename to src/feature-management-applicationinsights-browser/tsconfig.json diff --git a/sdk/feature-management-applicationinsights-browser/.eslintrc b/src/feature-management-applicationinsights-node/.eslintrc similarity index 98% rename from sdk/feature-management-applicationinsights-browser/.eslintrc rename to src/feature-management-applicationinsights-node/.eslintrc index 27a13cc..9a45025 100644 --- a/sdk/feature-management-applicationinsights-browser/.eslintrc +++ b/src/feature-management-applicationinsights-node/.eslintrc @@ -1,6 +1,5 @@ { "env": { - "browser": true, "commonjs": true, "es2021": true, "node": true, diff --git a/sdk/feature-management/LICENSE b/src/feature-management-applicationinsights-node/LICENSE similarity index 100% rename from sdk/feature-management/LICENSE rename to src/feature-management-applicationinsights-node/LICENSE diff --git a/src/feature-management-applicationinsights-node/README.md b/src/feature-management-applicationinsights-node/README.md new file mode 100644 index 0000000..4dc36d8 --- /dev/null +++ b/src/feature-management-applicationinsights-node/README.md @@ -0,0 +1,54 @@ +# Microsoft Feature Management Application Insights Plugin for Node + +Feature Management Application Insights Plugin for Node provides a solution for sending feature flag evaluation telemetry produced by the [`@microsoft/feature-management`](https://www.npmjs.com/package/@microsoft/feature-management) library. + +## Getting Started + +For more information, please go to [Feature reference](https://learn.microsoft.com/azure/azure-app-configuration/feature-management-javascript-reference#application-insights-integration). + +### Prerequisites + +- Node.js LTS version +- `applicationinsights` SDK 2.x (classic) + +### Usage + +``` javascript +const appInsights = require("applicationinsights"); +appInsights.setup(process.env.APPINSIGHTS_CONNECTION_STRING).start(); + +const { FeatureManager, ConfigurationObjectFeatureFlagProvider } = require("@microsoft/feature-management"); +const { createTelemetryPublisher, trackEvent } = require("@microsoft/feature-management-applicationinsights-node"); + +const publishTelemetry = createTelemetryPublisher(appInsights.defaultClient); +const provider = new ConfigurationObjectFeatureFlagProvider(jsonObject); +const featureManager = new FeatureManager(provider, {onFeatureEvaluated: publishTelemetry}); + +// FeatureEvaluation event will be emitted when a feature flag is evaluated +featureManager.getVariant("TestFeature", {userId : ""}).then((variant) => { /* do something*/ }); + +// Emit a custom event with targeting id attached. +trackEvent(appInsights.defaultClient, "", {name: "TestEvent", properties: {"Tag": "Some Value"}}); +``` + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Trademarks + +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft +trademarks or logos is subject to and must follow +[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). +Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. +Any use of third-party trademarks or logos are subject to those third-party's policies. diff --git a/src/feature-management-applicationinsights-node/package.json b/src/feature-management-applicationinsights-node/package.json new file mode 100644 index 0000000..af65e8b --- /dev/null +++ b/src/feature-management-applicationinsights-node/package.json @@ -0,0 +1,51 @@ +{ + "name": "@microsoft/feature-management-applicationinsights-node", + "version": "2.1.0", + "description": "Feature Management Application Insights Plugin for Node.js provides a solution for sending feature flag evaluation events produced by the Feature Management library.", + "main": "./dist/commonjs/index.js", + "module": "./dist/esm/index.js", + "types": "types/index.d.ts", + "files": [ + "dist/", + "types/", + "LICENSE", + "README.md" + ], + "scripts": { + "build": "npm run link && npm run clean && rollup --config", + "clean": "rimraf dist types", + "link": "npm link ../feature-management", + "dev": "rollup --config --watch", + "lint": "eslint src/", + "fix-lint": "eslint src/ --fix" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/microsoft/FeatureManagement-JavaScript.git" + }, + "license": "MIT", + "publishConfig": { + "tag": "latest" + }, + "bugs": { + "url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues" + }, + "homepage": "https://github.com/microsoft/FeatureManagement-JavaScript#readme", + "devDependencies": { + "@rollup/plugin-typescript": "^11.1.5", + "@types/node": "^20.10.7", + "@typescript-eslint/eslint-plugin": "^6.18.1", + "@typescript-eslint/parser": "^6.18.1", + "eslint": "^8.56.0", + "rimraf": "^5.0.5", + "rollup": "^4.9.4", + "rollup-plugin-dts": "^6.1.0", + "tslib": "^2.6.2", + "typescript": "^5.3.3" + }, + "dependencies": { + "applicationinsights": "^2.9.6", + "@microsoft/feature-management": "2.1.0" + } +} + \ No newline at end of file diff --git a/src/feature-management-applicationinsights-node/rollup.config.mjs b/src/feature-management-applicationinsights-node/rollup.config.mjs new file mode 100644 index 0000000..019a779 --- /dev/null +++ b/src/feature-management-applicationinsights-node/rollup.config.mjs @@ -0,0 +1,48 @@ +// rollup.config.js +import typescript from "@rollup/plugin-typescript"; +import dts from "rollup-plugin-dts"; + +export default [ + { + external: ["@microsoft/feature-management"], + input: "src/index.ts", + output: [ + { + dir: "dist/commonjs/", + format: "cjs", + sourcemap: true, + preserveModules: true, + }, + { + dir: "dist/esm/", + format: "esm", + sourcemap: true, + preserveModules: true, + } + ], + plugins: [ + typescript({ + compilerOptions: { + "lib": [ + "DOM", + "WebWorker", + "ESNext" + ], + "skipDefaultLibCheck": true, + "module": "ESNext", + "moduleResolution": "Node", + "target": "ES2022", + "strictNullChecks": true, + "strictFunctionTypes": true, + "sourceMap": true, + "inlineSources": true + } + }) + ], + }, + { + input: "src/index.ts", + output: [{ file: "types/index.d.ts", format: "esm" }], + plugins: [dts()], + } +]; diff --git a/sdk/feature-management-applicationinsights-browser/src/index.ts b/src/feature-management-applicationinsights-node/src/index.ts similarity index 52% rename from sdk/feature-management-applicationinsights-browser/src/index.ts rename to src/feature-management-applicationinsights-node/src/index.ts index 6b53335..e8509a8 100644 --- a/sdk/feature-management-applicationinsights-browser/src/index.ts +++ b/src/feature-management-applicationinsights-node/src/index.ts @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export { createTelemetryPublisher, trackEvent } from "./telemetry.js"; +export { createTargetingTelemetryProcessor, createTelemetryPublisher, trackEvent } from "./telemetry.js"; export { VERSION } from "./version.js"; diff --git a/src/feature-management-applicationinsights-node/src/telemetry.ts b/src/feature-management-applicationinsights-node/src/telemetry.ts new file mode 100644 index 0000000..825db5b --- /dev/null +++ b/src/feature-management-applicationinsights-node/src/telemetry.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { EvaluationResult, createFeatureEvaluationEventProperties, ITargetingContextAccessor } from "@microsoft/feature-management"; +import { TelemetryClient, Contracts } from "applicationinsights"; + +const TARGETING_ID = "TargetingId"; +const FEATURE_EVALUATION_EVENT_NAME = "FeatureEvaluation"; + +/** + * Creates a telemetry publisher that sends feature evaluation events to Application Insights. + * @param client The Application Insights telemetry client. + * @returns A callback function that takes an evaluation result and tracks an event with the evaluation details. + */ +export function createTelemetryPublisher(client: TelemetryClient): (result: EvaluationResult) => void { + return (result: EvaluationResult) => { + if (result.feature === undefined) { + return; + } + + const eventProperties = createFeatureEvaluationEventProperties(result); + client.trackEvent({ name: FEATURE_EVALUATION_EVENT_NAME, properties: eventProperties }); + }; +} + +/** + * Tracks a custom event using Application Insights, ensuring that the "TargetingId" + * is included in the custom properties. If the "TargetingId" already exists in + * the provided custom properties, it will be overwritten. + * + * @param client The Application Insights client instance used to track the event. + * @param targetingId The unique targeting identifier that will be included in the custom properties. + * @param event The event telemetry object to be tracked, containing event details. + */ +export function trackEvent(client: TelemetryClient, targetingId: string, event: Contracts.EventTelemetry): void { + event.properties = { + ...event.properties, + [TARGETING_ID]: targetingId ? targetingId.toString() : "" + }; + client.trackEvent(event); +} + +/** + * Creates a telemetry processor that adds targeting id to telemetry envelope's custom properties. + * @param targetingContextAccessor The accessor function to get the targeting context. + * @returns A telemetry processor that attaches targeting id to telemetry envelopes. + */ +export function createTargetingTelemetryProcessor(targetingContextAccessor: ITargetingContextAccessor): (envelope: Contracts.EnvelopeTelemetry) => boolean { + return (envelope: Contracts.EnvelopeTelemetry) => { + const targetingContext = targetingContextAccessor.getTargetingContext(); + if (targetingContext?.userId !== undefined) { + envelope.data.baseData = envelope.data.baseData || {}; + envelope.data.baseData.properties = {...envelope.data.baseData.properties, [TARGETING_ID]: targetingContext?.userId || ""}; + } + return true; + }; +} diff --git a/sdk/feature-management/src/version.ts b/src/feature-management-applicationinsights-node/src/version.ts similarity index 64% rename from sdk/feature-management/src/version.ts rename to src/feature-management-applicationinsights-node/src/version.ts index 551bb5c..0200538 100644 --- a/sdk/feature-management/src/version.ts +++ b/src/feature-management-applicationinsights-node/src/version.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "2.0.0-preview.2"; +export const VERSION = "2.1.0"; diff --git a/sdk/feature-management/tsconfig.json b/src/feature-management-applicationinsights-node/tsconfig.json similarity index 100% rename from sdk/feature-management/tsconfig.json rename to src/feature-management-applicationinsights-node/tsconfig.json diff --git a/src/feature-management/.eslintrc b/src/feature-management/.eslintrc new file mode 100644 index 0000000..7956229 --- /dev/null +++ b/src/feature-management/.eslintrc @@ -0,0 +1,69 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es2021": true, + "node": true, + "mocha": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "ignorePatterns": [ + ], + "overrides": [ + { + "env": { + "node": true + }, + "files": [ + ".eslintrc.{js,cjs}" + ], + "parserOptions": { + "sourceType": "script" + } + } + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "keyword-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "quotes": [ + "error", + "double", + { + "avoidEscape": true + } + ], + "@typescript-eslint/no-explicit-any": "off", + "eol-last": [ + "error", + "always" + ], + "no-trailing-spaces": "error", + "space-before-blocks": [ + "error", + "always" + ], + "no-multi-spaces": "error", + "no-multiple-empty-lines": [ + "error", + { + "max": 1 + } + ], + "semi": ["error", "always"] + } +} \ No newline at end of file diff --git a/src/feature-management/LICENSE b/src/feature-management/LICENSE new file mode 100644 index 0000000..9e841e7 --- /dev/null +++ b/src/feature-management/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/sdk/feature-management/README.md b/src/feature-management/README.md similarity index 65% rename from sdk/feature-management/README.md rename to src/feature-management/README.md index 54aa97a..5f78a40 100644 --- a/sdk/feature-management/README.md +++ b/src/feature-management/README.md @@ -2,18 +2,19 @@ [![feature-management](https://img.shields.io/npm/v/@microsoft/feature-management?label=@microsoft/feature-management)](https://www.npmjs.com/package/@microsoft/feature-management) -Feature Management is a library for enabling/disabling features at runtime. -Developers can use feature flags in simple use cases like conditional statement to more advanced scenarios like conditionally adding routes. +Feature management provides a way to develop and expose application functionality based on features. Many applications have special requirements when a new feature is developed such as when the feature should be enabled and under what conditions. This library provides a way to define these relationships, and also integrates into common JavaScript code patterns to make exposing these features possible. ## Getting Started -### Prerequisites +[Azure App Configuration Quickstart](https://learn.microsoft.com/azure/azure-app-configuration/quickstart-feature-flag-javascript): A quickstart guide about how to integrate feature flags from Azure App Configuration into your JavaScript applications. -- Node.js LTS version +[Feature Overview](https://learn.microsoft.com/azure/azure-app-configuration/feature-management-overview#feature-development-status): This document provides a feature status overview. + +[Feature Reference](https://learn.microsoft.com/azure/azure-app-configuration/feature-management-javascript-reference): This document provides a full feature rundown. ### Usage -You can use feature flags from the Azure App Configuration service, local files or any other sources. +You can use feature flags from the Azure App Configuration service, local files or any other sources. For more information, please go to [Feature flag configuration](https://learn.microsoft.com/azure/azure-app-configuration/feature-management-javascript-reference#feature-flag-configuration). #### Use feature flags from Azure App Configuration @@ -21,7 +22,9 @@ The App Configuration JavaScript provider provides feature flags in as a `Map` o A builtin `ConfigurationMapFeatureFlagProvider` helps to load feature flags in this case. ```js -const appConfig = load(connectionString, {featureFlagOptions}); // load feature flags from Azure App Configuration service +import { load } from "@azure/app-configuration-provider"; +import { FeatureManager, ConfigurationMapFeatureFlagProvider } from "@microsoft/feature-management"; +const appConfig = await load("", {featureFlagOptions}); // load feature flags from Azure App Configuration service const featureProvider = new ConfigurationMapFeatureFlagProvider(appConfig); const featureManager = new FeatureManager(featureProvider); const isAlphaEnabled = await featureManager.isEnabled("Alpha"); @@ -42,7 +45,7 @@ Content of `sample.json`: { "id": "Alpha", "description": "", - "enabled": "true", + "enabled": true, "conditions": { "client_filters": [] } @@ -54,6 +57,7 @@ Content of `sample.json`: Load feature flags from `sample.json` file. ```js +import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "@microsoft/feature-management"; const config = JSON.parse(await fs.readFile("path/to/sample.json")); const featureProvider = new ConfigurationObjectFeatureFlagProvider(config); const featureManager = new FeatureManager(featureProvider); @@ -61,6 +65,10 @@ const isAlphaEnabled = await featureManager.isEnabled("Alpha"); console.log("Feature Alpha is:", isAlphaEnabled); ``` +## Examples + +See code snippets under [examples/](./examples/) folder. + ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a diff --git a/sdk/feature-management/package-lock.json b/src/feature-management/package-lock.json similarity index 96% rename from sdk/feature-management/package-lock.json rename to src/feature-management/package-lock.json index 1579a4a..cb413c2 100644 --- a/sdk/feature-management/package-lock.json +++ b/src/feature-management/package-lock.json @@ -1,12 +1,12 @@ { "name": "@microsoft/feature-management", - "version": "2.0.0-preview.2", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@microsoft/feature-management", - "version": "2.0.0-preview.1", + "version": "2.1.0", "license": "MIT", "devDependencies": { "@playwright/test": "^1.46.1", @@ -1028,10 +1028,11 @@ } }, "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1294,9 +1295,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -1308,12 +1309,13 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1355,10 +1357,11 @@ "dev": true }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -2225,12 +2228,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -2259,32 +2262,32 @@ } }, "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -2292,49 +2295,45 @@ }, "engines": { "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, "node_modules/mocha/node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "minimatch": "^5.0.1", + "once": "^1.3.0" }, "engines": { - "node": "*" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2342,21 +2341,6 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/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==", - "dev": true - }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -2373,22 +2357,11 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } + "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -2648,6 +2621,7 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -2825,7 +2799,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/semver": { "version": "7.5.4", @@ -2843,10 +2818,11 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -3096,10 +3072,11 @@ } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "7.0.0", @@ -3176,10 +3153,11 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } diff --git a/sdk/feature-management/package.json b/src/feature-management/package.json similarity index 96% rename from sdk/feature-management/package.json rename to src/feature-management/package.json index dfd40ea..bc9a10b 100644 --- a/sdk/feature-management/package.json +++ b/src/feature-management/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/feature-management", - "version": "2.0.0-preview.2", + "version": "2.1.0", "description": "Feature Management is a library for enabling/disabling features at runtime. Developers can use feature flags in simple use cases like conditional statement to more advanced scenarios like conditionally adding routes.", "main": "./dist/commonjs/index.js", "module": "./dist/esm/index.js", @@ -26,6 +26,9 @@ "url": "git+https://github.com/microsoft/FeatureManagement-JavaScript.git" }, "license": "MIT", + "publishConfig": { + "tag": "latest" + }, "bugs": { "url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues" }, diff --git a/sdk/feature-management/playwright.config.ts b/src/feature-management/playwright.config.ts similarity index 100% rename from sdk/feature-management/playwright.config.ts rename to src/feature-management/playwright.config.ts diff --git a/sdk/feature-management/rollup.config.mjs b/src/feature-management/rollup.config.mjs similarity index 97% rename from sdk/feature-management/rollup.config.mjs rename to src/feature-management/rollup.config.mjs index 2b19caf..1d61acd 100644 --- a/sdk/feature-management/rollup.config.mjs +++ b/src/feature-management/rollup.config.mjs @@ -22,7 +22,7 @@ export default [ { file: "dist/umd/index.js", format: "umd", - name: 'FeatureManagement', + name: "FeatureManagement", sourcemap: true } ], diff --git a/sdk/feature-management/src/IFeatureManager.ts b/src/feature-management/src/IFeatureManager.ts similarity index 93% rename from sdk/feature-management/src/IFeatureManager.ts rename to src/feature-management/src/IFeatureManager.ts index d673dce..f982a6c 100644 --- a/sdk/feature-management/src/IFeatureManager.ts +++ b/src/feature-management/src/IFeatureManager.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { ITargetingContext } from "./common/ITargetingContext"; +import { ITargetingContext } from "./common/targetingContext"; import { Variant } from "./variant/Variant"; export interface IFeatureManager { diff --git a/src/feature-management/src/common/targetingContext.ts b/src/feature-management/src/common/targetingContext.ts new file mode 100644 index 0000000..a133f15 --- /dev/null +++ b/src/feature-management/src/common/targetingContext.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Contextual information that is required to perform a targeting evaluation. + */ +export interface ITargetingContext { + /** + * The user id that should be considered when evaluating if the context is being targeted. + */ + userId?: string; + /** + * The groups that should be considered when evaluating if the context is being targeted. + */ + groups?: string[]; +} + +/** + * Provides access to the current targeting context. + */ +export interface ITargetingContextAccessor { + /** + * Retrieves the current targeting context. + */ + getTargetingContext: () => ITargetingContext | undefined; +} diff --git a/sdk/feature-management/src/common/targetingEvaluator.ts b/src/feature-management/src/common/targetingEvaluator.ts similarity index 100% rename from sdk/feature-management/src/common/targetingEvaluator.ts rename to src/feature-management/src/common/targetingEvaluator.ts diff --git a/sdk/feature-management/src/featureManager.ts b/src/feature-management/src/featureManager.ts similarity index 87% rename from sdk/feature-management/src/featureManager.ts rename to src/feature-management/src/featureManager.ts index ac0503e..69fc6cb 100644 --- a/sdk/feature-management/src/featureManager.ts +++ b/src/feature-management/src/featureManager.ts @@ -3,30 +3,30 @@ import { TimeWindowFilter } from "./filter/TimeWindowFilter.js"; import { IFeatureFilter } from "./filter/FeatureFilter.js"; -import { FeatureFlag, RequirementType, VariantDefinition } from "./model.js"; +import { FeatureFlag, RequirementType, VariantDefinition } from "./schema/model.js"; import { IFeatureFlagProvider } from "./featureProvider.js"; import { TargetingFilter } from "./filter/TargetingFilter.js"; import { Variant } from "./variant/Variant.js"; import { IFeatureManager } from "./IFeatureManager.js"; -import { ITargetingContext } from "./common/ITargetingContext.js"; +import { ITargetingContext, ITargetingContextAccessor } from "./common/targetingContext.js"; import { isTargetedGroup, isTargetedPercentile, isTargetedUser } from "./common/targetingEvaluator.js"; export class FeatureManager implements IFeatureManager { - #provider: IFeatureFlagProvider; - #featureFilters: Map = new Map(); - #onFeatureEvaluated?: (event: EvaluationResult) => void; + readonly #provider: IFeatureFlagProvider; + readonly #featureFilters: Map = new Map(); + readonly #onFeatureEvaluated?: (event: EvaluationResult) => void; + readonly #targetingContextAccessor?: ITargetingContextAccessor; constructor(provider: IFeatureFlagProvider, options?: FeatureManagerOptions) { this.#provider = provider; + this.#onFeatureEvaluated = options?.onFeatureEvaluated; + this.#targetingContextAccessor = options?.targetingContextAccessor; - const builtinFilters = [new TimeWindowFilter(), new TargetingFilter()]; - + const builtinFilters = [new TimeWindowFilter(), new TargetingFilter(options?.targetingContextAccessor)]; // If a custom filter shares a name with an existing filter, the custom filter overrides the existing one. for (const filter of [...builtinFilters, ...(options?.customFilters ?? [])]) { this.#featureFilters.set(filter.name, filter); } - - this.#onFeatureEvaluated = options?.onFeatureEvaluated; } async listFeatureNames(): Promise { @@ -78,7 +78,7 @@ export class FeatureManager implements IFeatureManager { return { variant: undefined, reason: VariantAssignmentReason.None }; } - async #isEnabled(featureFlag: FeatureFlag, context?: unknown): Promise { + async #isEnabled(featureFlag: FeatureFlag, appContext?: unknown): Promise { if (featureFlag.enabled !== true) { // If the feature is not explicitly enabled, then it is disabled by default. return false; @@ -106,7 +106,7 @@ export class FeatureManager implements IFeatureManager { console.warn(`Feature filter ${clientFilter.name} is not found.`); return false; } - if (await matchedFeatureFilter.evaluate(contextWithFeatureName, context) === shortCircuitEvaluationResult) { + if (await matchedFeatureFilter.evaluate(contextWithFeatureName, appContext) === shortCircuitEvaluationResult) { return shortCircuitEvaluationResult; } } @@ -115,7 +115,7 @@ export class FeatureManager implements IFeatureManager { return !shortCircuitEvaluationResult; } - async #evaluateFeature(featureName: string, context: unknown): Promise { + async #evaluateFeature(featureName: string, appContext: unknown): Promise { const featureFlag = await this.#provider.getFeatureFlag(featureName); const result = new EvaluationResult(featureFlag); @@ -128,9 +128,10 @@ export class FeatureManager implements IFeatureManager { validateFeatureFlagFormat(featureFlag); // Evaluate if the feature is enabled. - result.enabled = await this.#isEnabled(featureFlag, context); + result.enabled = await this.#isEnabled(featureFlag, appContext); - const targetingContext = context as ITargetingContext; + // Get targeting context from the app context or the targeting context accessor + const targetingContext = this.#getTargetingContext(appContext); result.targetingId = targetingContext?.userId; // Determine Variant @@ -151,7 +152,7 @@ export class FeatureManager implements IFeatureManager { } } else { // enabled, assign based on allocation - if (context !== undefined && featureFlag.allocation !== undefined) { + if (targetingContext !== undefined && featureFlag.allocation !== undefined) { const variantAndReason = await this.#assignVariant(featureFlag, targetingContext); variantDef = variantAndReason.variant; reason = variantAndReason.reason; @@ -189,6 +190,16 @@ export class FeatureManager implements IFeatureManager { return result; } + + #getTargetingContext(context: unknown): ITargetingContext | undefined { + let targetingContext: ITargetingContext | undefined = context as ITargetingContext; + if (targetingContext?.userId === undefined && + targetingContext?.groups === undefined && + this.#targetingContextAccessor !== undefined) { + targetingContext = this.#targetingContextAccessor.getTargetingContext(); + } + return targetingContext; + } } export interface FeatureManagerOptions { @@ -202,6 +213,11 @@ export interface FeatureManagerOptions { * The callback function is called only when telemetry is enabled for the feature flag. */ onFeatureEvaluated?: (event: EvaluationResult) => void; + + /** + * The accessor function that provides the @see ITargetingContext for targeting evaluation. + */ + targetingContextAccessor?: ITargetingContextAccessor; } export class EvaluationResult { diff --git a/sdk/feature-management/src/featureProvider.ts b/src/feature-management/src/featureProvider.ts similarity index 65% rename from sdk/feature-management/src/featureProvider.ts rename to src/feature-management/src/featureProvider.ts index 0f90ce6..d29eade 100644 --- a/sdk/feature-management/src/featureProvider.ts +++ b/src/feature-management/src/featureProvider.ts @@ -2,7 +2,8 @@ // Licensed under the MIT license. import { IGettable } from "./gettable.js"; -import { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./model.js"; +import { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./schema/model.js"; +import { validateFeatureFlag } from "./schema/validator.js"; export interface IFeatureFlagProvider { /** @@ -28,12 +29,18 @@ export class ConfigurationMapFeatureFlagProvider implements IFeatureFlagProvider } async getFeatureFlag(featureName: string): Promise { const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY); - return featureConfig?.[FEATURE_FLAGS_KEY]?.find((feature) => feature.id === featureName); + const featureFlag = featureConfig?.[FEATURE_FLAGS_KEY]?.findLast((feature) => feature.id === featureName); + validateFeatureFlag(featureFlag); + return featureFlag; } async getFeatureFlags(): Promise { const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY); - return featureConfig?.[FEATURE_FLAGS_KEY] ?? []; + const featureFlags = featureConfig?.[FEATURE_FLAGS_KEY] ?? []; + featureFlags.forEach(featureFlag => { + validateFeatureFlag(featureFlag); + }); + return featureFlags; } } @@ -49,10 +56,16 @@ export class ConfigurationObjectFeatureFlagProvider implements IFeatureFlagProvi async getFeatureFlag(featureName: string): Promise { const featureFlags = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY]; - return featureFlags?.find((feature: FeatureFlag) => feature.id === featureName); + const featureFlag = featureFlags?.findLast((feature: FeatureFlag) => feature.id === featureName); + validateFeatureFlag(featureFlag); + return featureFlag; } async getFeatureFlags(): Promise { - return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? []; + const featureFlags = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? []; + featureFlags.forEach(featureFlag => { + validateFeatureFlag(featureFlag); + }); + return featureFlags; } } diff --git a/sdk/feature-management/src/filter/FeatureFilter.ts b/src/feature-management/src/filter/FeatureFilter.ts similarity index 100% rename from sdk/feature-management/src/filter/FeatureFilter.ts rename to src/feature-management/src/filter/FeatureFilter.ts diff --git a/sdk/feature-management/src/filter/TargetingFilter.ts b/src/feature-management/src/filter/TargetingFilter.ts similarity index 56% rename from sdk/feature-management/src/filter/TargetingFilter.ts rename to src/feature-management/src/filter/TargetingFilter.ts index b675d87..eb4b73d 100644 --- a/sdk/feature-management/src/filter/TargetingFilter.ts +++ b/src/feature-management/src/filter/TargetingFilter.ts @@ -3,7 +3,7 @@ import { IFeatureFilter } from "./FeatureFilter.js"; import { isTargetedPercentile } from "../common/targetingEvaluator.js"; -import { ITargetingContext } from "../common/ITargetingContext.js"; +import { ITargetingContext, ITargetingContextAccessor } from "../common/targetingContext.js"; type TargetingFilterParameters = { Audience: { @@ -26,28 +26,36 @@ type TargetingFilterEvaluationContext = { } export class TargetingFilter implements IFeatureFilter { - name: string = "Microsoft.Targeting"; + readonly name: string = "Microsoft.Targeting"; + readonly #targetingContextAccessor?: ITargetingContextAccessor; + + constructor(targetingContextAccessor?: ITargetingContextAccessor) { + this.#targetingContextAccessor = targetingContextAccessor; + } async evaluate(context: TargetingFilterEvaluationContext, appContext?: ITargetingContext): Promise { const { featureName, parameters } = context; - TargetingFilter.#validateParameters(parameters); + TargetingFilter.#validateParameters(featureName, parameters); - if (appContext === undefined) { - throw new Error("The app context is required for targeting filter."); + let targetingContext: ITargetingContext | undefined; + if (appContext?.userId !== undefined || appContext?.groups !== undefined) { + targetingContext = appContext; + } else if (this.#targetingContextAccessor !== undefined) { + targetingContext = this.#targetingContextAccessor.getTargetingContext(); } if (parameters.Audience.Exclusion !== undefined) { // check if the user is in the exclusion list - if (appContext?.userId !== undefined && + if (targetingContext?.userId !== undefined && parameters.Audience.Exclusion.Users !== undefined && - parameters.Audience.Exclusion.Users.includes(appContext.userId)) { + parameters.Audience.Exclusion.Users.includes(targetingContext.userId)) { return false; } // check if the user is in a group within exclusion list - if (appContext?.groups !== undefined && + if (targetingContext?.groups !== undefined && parameters.Audience.Exclusion.Groups !== undefined) { for (const excludedGroup of parameters.Audience.Exclusion.Groups) { - if (appContext.groups.includes(excludedGroup)) { + if (targetingContext.groups.includes(excludedGroup)) { return false; } } @@ -55,19 +63,19 @@ export class TargetingFilter implements IFeatureFilter { } // check if the user is being targeted directly - if (appContext?.userId !== undefined && + if (targetingContext?.userId !== undefined && parameters.Audience.Users !== undefined && - parameters.Audience.Users.includes(appContext.userId)) { + parameters.Audience.Users.includes(targetingContext.userId)) { return true; } // check if the user is in a group that is being targeted - if (appContext?.groups !== undefined && + if (targetingContext?.groups !== undefined && parameters.Audience.Groups !== undefined) { for (const group of parameters.Audience.Groups) { - if (appContext.groups.includes(group.Name)) { + if (targetingContext.groups.includes(group.Name)) { const hint = `${featureName}\n${group.Name}`; - if (await isTargetedPercentile(appContext.userId, hint, 0, group.RolloutPercentage)) { + if (await isTargetedPercentile(targetingContext.userId, hint, 0, group.RolloutPercentage)) { return true; } } @@ -76,18 +84,18 @@ export class TargetingFilter implements IFeatureFilter { // check if the user is being targeted by a default rollout percentage const hint = featureName; - return isTargetedPercentile(appContext?.userId, hint, 0, parameters.Audience.DefaultRolloutPercentage); + return isTargetedPercentile(targetingContext?.userId, hint, 0, parameters.Audience.DefaultRolloutPercentage); } - static #validateParameters(parameters: TargetingFilterParameters): void { + static #validateParameters(featureName: string, parameters: TargetingFilterParameters): void { if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) { - throw new Error("Audience.DefaultRolloutPercentage must be a number between 0 and 100."); + throw new Error(`Invalid feature flag: ${featureName}. Audience.DefaultRolloutPercentage must be a number between 0 and 100.`); } // validate RolloutPercentage for each group if (parameters.Audience.Groups !== undefined) { for (const group of parameters.Audience.Groups) { if (group.RolloutPercentage < 0 || group.RolloutPercentage > 100) { - throw new Error(`RolloutPercentage of group ${group.Name} must be a number between 0 and 100.`); + throw new Error(`Invalid feature flag: ${featureName}. RolloutPercentage of group ${group.Name} must be a number between 0 and 100.`); } } } diff --git a/sdk/feature-management/src/filter/TimeWindowFilter.ts b/src/feature-management/src/filter/TimeWindowFilter.ts similarity index 95% rename from sdk/feature-management/src/filter/TimeWindowFilter.ts rename to src/feature-management/src/filter/TimeWindowFilter.ts index 3cd0ead..beb0136 100644 --- a/sdk/feature-management/src/filter/TimeWindowFilter.ts +++ b/src/feature-management/src/filter/TimeWindowFilter.ts @@ -15,7 +15,7 @@ type TimeWindowFilterEvaluationContext = { } export class TimeWindowFilter implements IFeatureFilter { - name: string = "Microsoft.TimeWindow"; + readonly name: string = "Microsoft.TimeWindow"; evaluate(context: TimeWindowFilterEvaluationContext): boolean { const {featureName, parameters} = context; diff --git a/sdk/feature-management/src/gettable.ts b/src/feature-management/src/gettable.ts similarity index 100% rename from sdk/feature-management/src/gettable.ts rename to src/feature-management/src/gettable.ts diff --git a/sdk/feature-management/src/index.ts b/src/feature-management/src/index.ts similarity index 69% rename from sdk/feature-management/src/index.ts rename to src/feature-management/src/index.ts index d616a5b..093d3fd 100644 --- a/sdk/feature-management/src/index.ts +++ b/src/feature-management/src/index.ts @@ -3,5 +3,7 @@ export { FeatureManager, FeatureManagerOptions, EvaluationResult, VariantAssignmentReason } from "./featureManager.js"; export { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider, IFeatureFlagProvider } from "./featureProvider.js"; +export { createFeatureEvaluationEventProperties } from "./telemetry/featureEvaluationEvent.js"; export { IFeatureFilter } from "./filter/FeatureFilter.js"; +export { ITargetingContext, ITargetingContextAccessor } from "./common/targetingContext.js"; export { VERSION } from "./version.js"; diff --git a/src/feature-management/src/schema/model.ts b/src/feature-management/src/schema/model.ts new file mode 100644 index 0000000..21749a0 --- /dev/null +++ b/src/feature-management/src/schema/model.ts @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Converted from: +// https://github.com/Azure/AppConfiguration/blob/6e544296a5607f922a423df165f60801717c7800/docs/FeatureManagement/FeatureFlag.v2.0.0.schema.json + +/** + * A feature flag is a named property that can be toggled to enable or disable some feature of an application. + */ +export interface FeatureFlag { + /** + * An ID used to uniquely identify and reference the feature. + */ + id: string; + /** + * A feature is OFF if enabled is false. If enabled is true, then the feature is ON if there are no conditions (null or empty) or if the conditions are satisfied. + */ + enabled?: boolean; + /** + * The declaration of conditions used to dynamically enable the feature. + */ + conditions?: FeatureEnablementConditions; + /** + * The list of variants defined for this feature. A variant represents a configuration value of a feature flag that can be a string, a number, a boolean, or a JSON object. + */ + variants?: VariantDefinition[]; + /** + * Determines how variants should be allocated for the feature to various users. + */ + allocation?: VariantAllocation; + /** + * The declaration of options used to configure telemetry for this feature. + */ + telemetry?: TelemetryOptions + } + + /** + * The declaration of conditions used to dynamically enable the feature + */ + interface FeatureEnablementConditions { + /** + * Determines whether any or all registered client filters must be evaluated as true for the feature to be considered enabled. + */ + requirement_type?: RequirementType; + /** + * Filters that must run on the client and be evaluated as true for the feature to be considered enabled. + */ + client_filters?: ClientFilter[]; + } + + export type RequirementType = "Any" | "All"; + + interface ClientFilter { + /** + * The name used to refer to a client filter. + */ + name: string; + /** + * Parameters for a given client filter. A client filter can require any set of parameters of any type. + */ + parameters?: Record; + } + + export interface VariantDefinition { + /** + * The name used to refer to a feature variant. + */ + name: string; + /** + * The configuration value for this feature variant. + */ + configuration_value?: unknown; + /** + * Overrides the enabled state of the feature if the given variant is assigned. Does not override the state if value is None. + */ + status_override?: "None" | "Enabled" | "Disabled"; + } + + /** + * Determines how variants should be allocated for the feature to various users. + */ + interface VariantAllocation { + /** + * Specifies which variant should be used when the feature is considered disabled. + */ + default_when_disabled?: string; + /** + * Specifies which variant should be used when the feature is considered enabled and no other allocation rules are applicable. + */ + default_when_enabled?: string; + /** + * A list of objects, each containing a variant name and list of users for whom that variant should be used. + */ + user?: UserAllocation[]; + /** + * A list of objects, each containing a variant name and list of groups for which that variant should be used. + */ + group?: GroupAllocation[]; + /** + * A list of objects, each containing a variant name and percentage range for which that variant should be used. + */ + percentile?: PercentileAllocation[] + /** + * The value percentile calculations are based on. The calculated percentile is consistent across features for a given user if the same nonempty seed is used. + */ + seed?: string; + } + + interface UserAllocation { + /** + * The name of the variant to use if the user allocation matches the current user. + */ + variant: string; + /** + * Collection of users where if any match the current user, the variant specified in the user allocation is used. + */ + users: string[]; + } + + interface GroupAllocation { + /** + * The name of the variant to use if the group allocation matches a group the current user is in. + */ + variant: string; + /** + * Collection of groups where if the current user is in any of these groups, the variant specified in the group allocation is used. + */ + groups: string[]; + } + + interface PercentileAllocation { + /** + * The name of the variant to use if the calculated percentile for the current user falls in the provided range. + */ + variant: string; + /** + * The lower end of the percentage range for which this variant will be used. + */ + from: number; + /** + * The upper end of the percentage range for which this variant will be used. + */ + to: number; + } + + /** + * The declaration of options used to configure telemetry for this feature. + */ + interface TelemetryOptions { + /** + * Indicates if telemetry is enabled. + */ + enabled?: boolean; + /** + * A container for metadata that should be bundled with flag telemetry. + */ + metadata?: Record; + } + + // Feature Management Section fed into feature manager. + // Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json + + export const FEATURE_MANAGEMENT_KEY = "feature_management"; + export const FEATURE_FLAGS_KEY = "feature_flags"; + + export interface FeatureManagementConfiguration { + feature_management: FeatureManagement + } + + /** + * Declares feature management configuration. + */ + export interface FeatureManagement { + feature_flags: FeatureFlag[]; + } diff --git a/src/feature-management/src/schema/validator.ts b/src/feature-management/src/schema/validator.ts new file mode 100644 index 0000000..2b805b7 --- /dev/null +++ b/src/feature-management/src/schema/validator.ts @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Validates a feature flag object, checking if it conforms to the schema. + * @param featureFlag The feature flag object to validate. + */ +export function validateFeatureFlag(featureFlag: any): void { + if (featureFlag === undefined) { + return; // no-op if feature flag is undefined, indicating that the feature flag is not found + } + if (featureFlag === null || typeof featureFlag !== "object") { // Note: typeof null = "object" + throw new TypeError("Feature flag must be an object."); + } + if (typeof featureFlag.id !== "string") { + throw new TypeError("Feature flag 'id' must be a string."); + } + + if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== "boolean") { + throw new TypeError(`Invalid feature flag: ${featureFlag.id}. Feature flag 'enabled' must be a boolean.`); + } + if (featureFlag.conditions !== undefined) { + validateFeatureEnablementConditions(featureFlag.id, featureFlag.conditions); + } + if (featureFlag.variants !== undefined) { + validateVariants(featureFlag.id, featureFlag.variants); + } + if (featureFlag.allocation !== undefined) { + validateVariantAllocation(featureFlag.id, featureFlag.allocation); + } + if (featureFlag.telemetry !== undefined) { + validateTelemetryOptions(featureFlag.id, featureFlag.telemetry); + } +} + +function validateFeatureEnablementConditions(id: string, conditions: any) { + if (typeof conditions !== "object") { + throw new TypeError(`Invalid feature flag: ${id}. Feature flag 'conditions' must be an object.`); + } + if (conditions.requirement_type !== undefined && conditions.requirement_type !== "Any" && conditions.requirement_type !== "All") { + throw new TypeError(`Invalid feature flag: ${id}. 'requirement_type' must be 'Any' or 'All'.`); + } + if (conditions.client_filters !== undefined) { + validateClientFilters(id, conditions.client_filters); + } +} + +function validateClientFilters(id: string, client_filters: any) { + if (!Array.isArray(client_filters)) { + throw new TypeError(`Invalid feature flag: ${id}. Feature flag conditions 'client_filters' must be an array.`); + } + + for (const filter of client_filters) { + if (typeof filter.name !== "string") { + throw new TypeError(`Invalid feature flag: ${id}. Client filter 'name' must be a string.`); + } + if (filter.parameters !== undefined && typeof filter.parameters !== "object") { + throw new TypeError(`Invalid feature flag: ${id}. Client filter 'parameters' must be an object.`); + } + } +} + +function validateVariants(id: string, variants: any) { + if (!Array.isArray(variants)) { + throw new TypeError(`Invalid feature flag: ${id}. Feature flag 'variants' must be an array.`); + } + + for (const variant of variants) { + if (typeof variant.name !== "string") { + throw new TypeError(`Invalid feature flag: ${id}. Variant 'name' must be a string.`); + } + // skip configuration_value validation as it accepts any type + if (variant.status_override !== undefined && typeof variant.status_override !== "string") { + throw new TypeError(`Invalid feature flag: ${id}. Variant 'status_override' must be a string.`); + } + if (variant.status_override !== undefined && variant.status_override !== "None" && variant.status_override !== "Enabled" && variant.status_override !== "Disabled") { + throw new TypeError(`Invalid feature flag: ${id}. Variant 'status_override' must be 'None', 'Enabled', or 'Disabled'.`); + } + } +} + +function validateVariantAllocation(id: string, allocation: any) { + if (typeof allocation !== "object") { + throw new TypeError(`Invalid feature flag: ${id}. Variant 'allocation' must be an object.`); + } + + if (allocation.default_when_disabled !== undefined && typeof allocation.default_when_disabled !== "string") { + throw new TypeError(`Invalid feature flag: ${id}. Variant allocation 'default_when_disabled' must be a string.`); + } + if (allocation.default_when_enabled !== undefined && typeof allocation.default_when_enabled !== "string") { + throw new TypeError(`Invalid feature flag: ${id}. Variant allocation 'default_when_enabled' must be a string.`); + } + if (allocation.user !== undefined) { + validateUserVariantAllocation(id, allocation.user); + } + if (allocation.group !== undefined) { + validateGroupVariantAllocation(id, allocation.group); + } + if (allocation.percentile !== undefined) { + validatePercentileVariantAllocation(id, allocation.percentile); + } + if (allocation.seed !== undefined && typeof allocation.seed !== "string") { + throw new TypeError(`Invalid feature flag: ${id}. Variant allocation 'seed' must be a string.`); + } +} + +function validateUserVariantAllocation(id: string, UserAllocations: any) { + if (!Array.isArray(UserAllocations)) { + throw new TypeError(`Invalid feature flag: ${id}. Variant 'user' allocation must be an array.`); + } + + for (const allocation of UserAllocations) { + if (typeof allocation !== "object") { + throw new TypeError(`Invalid feature flag: ${id}. Elements in variant 'user' allocation must be an object.`); + } + if (typeof allocation.variant !== "string") { + throw new TypeError(`Invalid feature flag: ${id}. User allocation 'variant' must be a string.`); + } + if (!Array.isArray(allocation.users)) { + throw new TypeError(`Invalid feature flag: ${id}. User allocation 'users' must be an array.`); + } + for (const user of allocation.users) { + if (typeof user !== "string") { + throw new TypeError(`Invalid feature flag: ${id}. Elements in user allocation 'users' must be strings.`); + } + } + } +} + +function validateGroupVariantAllocation(id: string, groupAllocations: any) { + if (!Array.isArray(groupAllocations)) { + throw new TypeError(`Invalid feature flag: ${id}. Variant 'group' allocation must be an array.`); + } + + for (const allocation of groupAllocations) { + if (typeof allocation !== "object") { + throw new TypeError(`Invalid feature flag: ${id}. Elements in variant 'group' allocation must be an object.`); + } + if (typeof allocation.variant !== "string") { + throw new TypeError(`Invalid feature flag: ${id}. Group allocation 'variant' must be a string.`); + } + if (!Array.isArray(allocation.groups)) { + throw new TypeError(`Invalid feature flag: ${id}. Group allocation 'groups' must be an array.`); + } + for (const group of allocation.groups) { + if (typeof group !== "string") { + throw new TypeError(`Invalid feature flag: ${id}. Elements in group allocation 'groups' must be strings.`); + } + } + } +} + +function validatePercentileVariantAllocation(id: string, percentileAllocations: any) { + if (!Array.isArray(percentileAllocations)) { + throw new TypeError(`Invalid feature flag: ${id}. Variant 'percentile' allocation must be an array.`); + } + + for (const allocation of percentileAllocations) { + if (typeof allocation !== "object") { + throw new TypeError(`Invalid feature flag: ${id}. Elements in variant 'percentile' allocation must be an object.`); + } + if (typeof allocation.variant !== "string") { + throw new TypeError(`Invalid feature flag: ${id}. Percentile allocation 'variant' must be a string.`); + } + if (typeof allocation.from !== "number" || allocation.from < 0 || allocation.from > 100) { + throw new TypeError(`Invalid feature flag: ${id}. Percentile allocation 'from' must be a number between 0 and 100.`); + } + if (typeof allocation.to !== "number" || allocation.to < 0 || allocation.to > 100) { + throw new TypeError(`Invalid feature flag: ${id}. Percentile allocation 'to' must be a number between 0 and 100.`); + } + } +} +// #endregion + +// #region Telemetry +function validateTelemetryOptions(id: string, telemetry: any) { + if (typeof telemetry !== "object") { + throw new TypeError(`Invalid feature flag: ${id}. Feature flag 'telemetry' must be an object.`); + } + if (telemetry.enabled !== undefined && typeof telemetry.enabled !== "boolean") { + throw new TypeError(`Invalid feature flag: ${id}. Telemetry 'enabled' must be a boolean.`); + } + if (telemetry.metadata !== undefined && typeof telemetry.metadata !== "object") { + throw new TypeError(`Invalid feature flag: ${id}. Telemetry 'metadata' must be an object.`); + } +} +// #endregion diff --git a/src/feature-management/src/telemetry/featureEvaluationEvent.ts b/src/feature-management/src/telemetry/featureEvaluationEvent.ts new file mode 100644 index 0000000..2c195c6 --- /dev/null +++ b/src/feature-management/src/telemetry/featureEvaluationEvent.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { EvaluationResult, VariantAssignmentReason } from "../featureManager"; +import { EVALUATION_EVENT_VERSION } from "../version.js"; + +const VERSION = "Version"; +const FEATURE_NAME = "FeatureName"; +const ENABLED = "Enabled"; +const TARGETING_ID = "TargetingId"; +const VARIANT = "Variant"; +const VARIANT_ASSIGNMENT_REASON = "VariantAssignmentReason"; +const DEFAULT_WHEN_ENABLED = "DefaultWhenEnabled"; +const VARIANT_ASSIGNMENT_PERCENTAGE = "VariantAssignmentPercentage"; + +export function createFeatureEvaluationEventProperties(result: EvaluationResult): any { + if (result.feature === undefined) { + return undefined; + } + + const eventProperties = { + [VERSION]: EVALUATION_EVENT_VERSION, + [FEATURE_NAME]: result.feature ? result.feature.id : "", + [ENABLED]: result.enabled ? "True" : "False", + // Ensure targetingId is string so that it will be placed in customDimensions + [TARGETING_ID]: result.targetingId ? result.targetingId.toString() : "", + [VARIANT]: result.variant ? result.variant.name : "", + [VARIANT_ASSIGNMENT_REASON]: result.variantAssignmentReason, + }; + + if (result.feature.allocation?.default_when_enabled) { + eventProperties[DEFAULT_WHEN_ENABLED] = result.feature.allocation.default_when_enabled; + } + + if (result.variantAssignmentReason === VariantAssignmentReason.DefaultWhenEnabled) { + let percentileAllocationPercentage = 0; + if (result.variant !== undefined && result.feature.allocation !== undefined && result.feature.allocation.percentile !== undefined) { + for (const percentile of result.feature.allocation.percentile) { + percentileAllocationPercentage += percentile.to - percentile.from; + } + } + eventProperties[VARIANT_ASSIGNMENT_PERCENTAGE] = (100 - percentileAllocationPercentage).toString(); + } + else if (result.variantAssignmentReason === VariantAssignmentReason.Percentile) { + let percentileAllocationPercentage = 0; + if (result.variant !== undefined && result.feature.allocation !== undefined && result.feature.allocation.percentile !== undefined) { + for (const percentile of result.feature.allocation.percentile) { + if (percentile.variant === result.variant.name) { + percentileAllocationPercentage += percentile.to - percentile.from; + } + } + } + eventProperties[VARIANT_ASSIGNMENT_PERCENTAGE] = percentileAllocationPercentage.toString(); + } + + const metadata = result.feature.telemetry?.metadata; + if (metadata) { + for (const key in metadata) { + if (!(key in eventProperties)) { + eventProperties[key] = metadata[key]; + } + } + } + + return eventProperties; +} diff --git a/sdk/feature-management/src/variant/Variant.ts b/src/feature-management/src/variant/Variant.ts similarity index 100% rename from sdk/feature-management/src/variant/Variant.ts rename to src/feature-management/src/variant/Variant.ts diff --git a/src/feature-management/src/version.ts b/src/feature-management/src/version.ts new file mode 100644 index 0000000..816cce6 --- /dev/null +++ b/src/feature-management/src/version.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export const VERSION = "2.1.0"; +export const EVALUATION_EVENT_VERSION = "1.0.0"; diff --git a/sdk/feature-management/test/browser/browser.test.ts b/src/feature-management/test/browser/browser.test.ts similarity index 100% rename from sdk/feature-management/test/browser/browser.test.ts rename to src/feature-management/test/browser/browser.test.ts diff --git a/sdk/feature-management/test/browser/index.html b/src/feature-management/test/browser/index.html similarity index 100% rename from sdk/feature-management/test/browser/index.html rename to src/feature-management/test/browser/index.html diff --git a/sdk/feature-management/test/browser/testcases.js b/src/feature-management/test/browser/testcases.js similarity index 100% rename from sdk/feature-management/test/browser/testcases.js rename to src/feature-management/test/browser/testcases.js diff --git a/sdk/feature-management/test/featureEvaluation.test.ts b/src/feature-management/test/featureEvaluation.test.ts similarity index 100% rename from sdk/feature-management/test/featureEvaluation.test.ts rename to src/feature-management/test/featureEvaluation.test.ts diff --git a/src/feature-management/test/featureManager.test.ts b/src/feature-management/test/featureManager.test.ts new file mode 100644 index 0000000..f7f6daa --- /dev/null +++ b/src/feature-management/test/featureManager.test.ts @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; +chai.use(chaiAsPromised); +const expect = chai.expect; + +import { FeatureManager, ConfigurationObjectFeatureFlagProvider, ConfigurationMapFeatureFlagProvider } from "../"; + +describe("feature manager", () => { + it("should load from json string", async () => { + const jsonObject = { + "feature_management": { + "feature_flags": [ + { "id": "Alpha", "description": "", "enabled": true}, + { "id": "Beta", "description": "", "enabled": false} + ] + } + }; + + const provider = new ConfigurationObjectFeatureFlagProvider(jsonObject); + const featureManager = new FeatureManager(provider); + expect(await featureManager.isEnabled("Alpha")).to.eq(true); + expect(await featureManager.isEnabled("Beta")).to.eq(false); + expect(await featureManager.isEnabled("not existed")).to.eq(false); + + const featureFlags = await featureManager.listFeatureNames(); + expect(featureFlags.length).to.eq(2); + expect(featureFlags[0]).to.eq("Alpha"); + expect(featureFlags[1]).to.eq("Beta"); + }); + + it("should load from map", async () => { + const dataSource = new Map(); + dataSource.set("feature_management", { + feature_flags: [ + { id: "Alpha", enabled: true }, + { id: "Beta", enabled: false} + ], + }); + + const provider = new ConfigurationMapFeatureFlagProvider(dataSource); + const featureManager = new FeatureManager(provider); + expect(await featureManager.isEnabled("Alpha")).to.eq(true); + expect(await featureManager.isEnabled("Beta")).to.eq(false); + expect(await featureManager.isEnabled("not existed")).to.eq(false); + + const featureFlags = await featureManager.listFeatureNames(); + expect(featureFlags.length).to.eq(2); + expect(featureFlags[0]).to.eq("Alpha"); + expect(featureFlags[1]).to.eq("Beta"); + }); + + it("should fail when feature flag id is invalid", async () => { + const dataSource = new Map(); + dataSource.set("feature_management", { + feature_flags: [ + { id: "Alpha", enabled: "true" } + ], + }); + const mapProvider = new ConfigurationMapFeatureFlagProvider(dataSource); + let featureManager = new FeatureManager(mapProvider); + await expect(featureManager.isEnabled("Alpha")).to.eventually.be.rejectedWith("Invalid feature flag: Alpha. Feature flag 'enabled' must be a boolean."); + + const jsonObject = { + "feature_management": { + "feature_flags": [ + { "id": 123, "enabled": true} + ] + } + }; + const objectProvider = new ConfigurationObjectFeatureFlagProvider(jsonObject); + featureManager = new FeatureManager(objectProvider); + await expect(featureManager.listFeatureNames()).to.eventually.be.rejectedWith("Feature flag 'id' must be a string."); + }); + + it("should load latest data if source is updated after initialization", () => { + const dataSource = new Map(); + dataSource.set("feature_management", { + feature_flags: [ + { id: "Alpha", enabled: true } + ], + }); + + const provider = new ConfigurationMapFeatureFlagProvider(dataSource); + const featureManager = new FeatureManager(provider); + dataSource.set("feature_management", { + feature_flags: [ + { id: "Alpha", enabled: false } + ], + }); + + return expect(featureManager.isEnabled("Alpha")).eventually.eq(false); + }); + + it("should evaluate features without conditions", () => { + const dataSource = new Map(); + dataSource.set("feature_management", { + feature_flags: [ + { "id": "Alpha", "description": "", "enabled": true, "conditions": { "client_filters": [] } }, + { "id": "Beta", "description": "", "enabled": false, "conditions": { "client_filters": [] } } + ], + }); + + const provider = new ConfigurationMapFeatureFlagProvider(dataSource); + const featureManager = new FeatureManager(provider); + return Promise.all([ + expect(featureManager.isEnabled("Alpha")).eventually.eq(true), + expect(featureManager.isEnabled("Beta")).eventually.eq(false) + ]); + }); + + it("should fail when feature flag is invalid", () => { + const dataSource = new Map(); + dataSource.set("feature_management", { + feature_flags: [ + { + "id": "Gamma", + "description": "", + "enabled": true, + "conditions": { + "requirement_type": "invalid type", + "client_filters": [ + { "name": "Microsoft.Targeting", "parameters": { "Audience": { "DefaultRolloutPercentage": 50 } } } + ] + } + }, + { + "id": "Delta", + "description": "", + "enabled": true, + "conditions": { + "requirement_type": "Any", + "client_filters": [ + { "name": "Microsoft.Targeting", "parameters": "invalid parameter" } + ] + } + } + ], + }); + + const provider = new ConfigurationMapFeatureFlagProvider(dataSource); + const featureManager = new FeatureManager(provider); + return Promise.all([ + expect(featureManager.isEnabled("Gamma")).eventually.rejectedWith("Invalid feature flag: Gamma. 'requirement_type' must be 'Any' or 'All'."), + expect(featureManager.isEnabled("Delta")).eventually.rejectedWith("Invalid feature flag: Delta. Client filter 'parameters' must be an object.") + ]); + }); + + it("should let the last feature flag win", () => { + const jsonObject = { + "feature_management": { + "feature_flags": [ + { "id": "Alpha", "description": "", "enabled": false, "conditions": { "client_filters": [] } }, + { "id": "Alpha", "description": "", "enabled": true, "conditions": { "client_filters": [] } } + ] + } + }; + + const provider1 = new ConfigurationObjectFeatureFlagProvider(jsonObject); + const featureManager1 = new FeatureManager(provider1); + + const dataSource = new Map(); + dataSource.set("feature_management", { + feature_flags: [ + { "id": "Alpha", "description": "", "enabled": false, "conditions": { "client_filters": [] } }, + { "id": "Alpha", "description": "", "enabled": true, "conditions": { "client_filters": [] } } + ], + }); + + const provider2 = new ConfigurationMapFeatureFlagProvider(dataSource); + const featureManager2 = new FeatureManager(provider2); + + return Promise.all([ + expect(featureManager1.isEnabled("Alpha")).eventually.eq(true), + expect(featureManager2.isEnabled("Alpha")).eventually.eq(true) + ]); + }); + + it("should evaluate client filters based on requirement type", () => { + const jsonObject = { + "feature_management": { + "feature_flags": [ + { + "id": "AllFeature", + "enabled": true, + "conditions": { + "requirement_type": "All", + "client_filters": [ + { + "name": "Microsoft.TimeWindow", + "parameters": { + "Start": "Tue, 4 Feb 2025 16:00:00 GMT" + } + }, + { + "name": "Microsoft.Targeting", + "parameters": { + "Audience": { + "Users": [ "Alice" ], + "Exclusion": { + "Users": [ "Dave" ] + } + } + } + } + ] + } + }, + { + "id": "AnyFeature", + "enabled": true, + "conditions": { + // "requirement_type": "Any", + "client_filters": [ + { + "name": "Microsoft.TimeWindow", + "parameters": { + "Start": "Tue, 4 Feb 2025 16:00:00 GMT" + } + }, + { + "name": "Microsoft.Targeting", + "parameters": { + "Audience": { + "Users": [ "Alice" ], + "Exclusion": { + "Users": [ "Dave" ] + } + } + } + } + ] + } + } + ] + } + }; + + const provider = new ConfigurationObjectFeatureFlagProvider(jsonObject); + const featureManager = new FeatureManager(provider); + return Promise.all([ + expect(featureManager.isEnabled("AllFeature", {userId: "Alice"})).eventually.eq(true), + expect(featureManager.isEnabled("AllFeature", {userId: "Dave"})).eventually.eq(false), + expect(featureManager.isEnabled("AnyFeature", {userId: "Dave"})).eventually.eq(true) + ]); + }); + + it("should override default filters with custom filters"); +}); diff --git a/sdk/feature-management/test/noFilters.test.ts b/src/feature-management/test/noFilters.test.ts similarity index 95% rename from sdk/feature-management/test/noFilters.test.ts rename to src/feature-management/test/noFilters.test.ts index e87f04e..889a8d9 100644 --- a/sdk/feature-management/test/noFilters.test.ts +++ b/src/feature-management/test/noFilters.test.ts @@ -3,10 +3,11 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; -import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "../"; chai.use(chaiAsPromised); const expect = chai.expect; +import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "../"; + const featureFlagsDataObject = { "feature_management": { "feature_flags": [ @@ -60,7 +61,7 @@ describe("feature flags with no filters", () => { return Promise.all([ expect(featureManager.isEnabled("BooleanTrue")).eventually.eq(true), expect(featureManager.isEnabled("BooleanFalse")).eventually.eq(false), - expect(featureManager.isEnabled("InvalidEnabled")).eventually.rejectedWith("Feature flag InvalidEnabled has an invalid 'enabled' value."), + expect(featureManager.isEnabled("InvalidEnabled")).eventually.rejectedWith("Invalid feature flag: InvalidEnabled. Feature flag 'enabled' must be a boolean."), expect(featureManager.isEnabled("Minimal")).eventually.eq(true), expect(featureManager.isEnabled("NoEnabled")).eventually.eq(false), expect(featureManager.isEnabled("EmptyConditions")).eventually.eq(true) diff --git a/src/feature-management/test/sampleVariantFeatureFlags.ts b/src/feature-management/test/sampleVariantFeatureFlags.ts new file mode 100644 index 0000000..21f0020 --- /dev/null +++ b/src/feature-management/test/sampleVariantFeatureFlags.ts @@ -0,0 +1,221 @@ +export enum Features { + VariantFeatureDefaultDisabled = "VariantFeatureDefaultDisabled", + VariantFeatureDefaultEnabled = "VariantFeatureDefaultEnabled", + VariantFeaturePercentileOn = "VariantFeaturePercentileOn", + VariantFeaturePercentileOff = "VariantFeaturePercentileOff", + VariantFeatureUser = "VariantFeatureUser", + VariantFeatureGroup = "VariantFeatureGroup", + VariantFeatureNoVariants = "VariantFeatureNoVariants", + VariantFeatureNoAllocation = "VariantFeatureNoAllocation", + VariantFeatureInvalidStatusOverride = "VariantFeatureInvalidStatusOverride", + VariantFeatureInvalidFromTo = "VariantFeatureInvalidFromTo", +} + +export const featureFlagsConfigurationObject = { + "feature_management": { + "feature_flags": [ + { + "id": "VariantFeaturePercentileOn", + "enabled": true, + "variants": [ + { + "name": "Big", + "status_override": "Disabled" + } + ], + "allocation": { + "percentile": [ + { + "variant": "Big", + "from": 0, + "to": 50 + } + ], + "seed": "1234" + }, + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeaturePercentileOff", + "enabled": true, + "variants": [ + { + "name": "Big" + } + ], + "allocation": { + "percentile": [ + { + "variant": "Big", + "from": 0, + "to": 50 + } + ], + "seed": "12345" + }, + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeatureDefaultDisabled", + "enabled": false, + "variants": [ + { + "name": "Small", + "configuration_value": "300px" + } + ], + "allocation": { + "default_when_disabled": "Small" + }, + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeatureDefaultEnabled", + "enabled": true, + "variants": [ + { + "name": "Medium", + "configuration_value": { + "Size": "450px", + "Color": "Purple" + } + }, + { + "name": "Small", + "configuration_value": "300px" + } + ], + "allocation": { + "default_when_enabled": "Medium", + "user": [ + { + "variant": "Small", + "users": [ + "Jeff" + ] + } + ] + }, + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeatureUser", + "enabled": true, + "variants": [ + { + "name": "Small", + "configuration_value": "300px" + } + ], + "allocation": { + "user": [ + { + "variant": "Small", + "users": [ + "Marsha" + ] + } + ] + }, + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeatureGroup", + "enabled": true, + "variants": [ + { + "name": "Small", + "configuration_value": "300px" + } + ], + "allocation": { + "group": [ + { + "variant": "Small", + "groups": [ + "Group1" + ] + } + ] + }, + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeatureNoVariants", + "enabled": true, + "variants": [], + "allocation": { + "user": [ + { + "variant": "Small", + "users": [ + "Marsha" + ] + } + ] + }, + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeatureNoAllocation", + "enabled": true, + "variants": [ + { + "name": "Small", + "configuration_value": "300px" + } + ], + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeatureInvalidStatusOverride", + "enabled": true, + "variants": [ + { + "name": "Small", + "configuration_value": "300px", + "status_override": "InvalidValue" + } + ], + "allocation": { + "default_when_enabled": "Small" + } + }, + { + "id": "VariantFeatureInvalidFromTo", + "enabled": true, + "variants": [ + { + "name": "Small", + "configuration_value": "300px" + } + ], + "allocation": { + "percentile": [ + { + "variant": "Small", + "from": "Invalid", + "to": "Invalid" + } + ] + } + } + ] + } +}; diff --git a/sdk/feature-management/test/targetingFilter.test.ts b/src/feature-management/test/targetingFilter.test.ts similarity index 80% rename from sdk/feature-management/test/targetingFilter.test.ts rename to src/feature-management/test/targetingFilter.test.ts index c752f67..6da25f6 100644 --- a/sdk/feature-management/test/targetingFilter.test.ts +++ b/src/feature-management/test/targetingFilter.test.ts @@ -83,10 +83,10 @@ describe("targeting filter", () => { const featureManager = new FeatureManager(provider); return Promise.all([ - expect(featureManager.isEnabled("InvalidTargeting1", {})).eventually.rejectedWith("Audience.DefaultRolloutPercentage must be a number between 0 and 100."), - expect(featureManager.isEnabled("InvalidTargeting2", {})).eventually.rejectedWith("Audience.DefaultRolloutPercentage must be a number between 0 and 100."), - expect(featureManager.isEnabled("InvalidTargeting3", {})).eventually.rejectedWith("RolloutPercentage of group Stage1 must be a number between 0 and 100."), - expect(featureManager.isEnabled("InvalidTargeting4", {})).eventually.rejectedWith("RolloutPercentage of group Stage1 must be a number between 0 and 100."), + expect(featureManager.isEnabled("InvalidTargeting1", {})).eventually.rejectedWith("Invalid feature flag: InvalidTargeting1. Audience.DefaultRolloutPercentage must be a number between 0 and 100."), + expect(featureManager.isEnabled("InvalidTargeting2", {})).eventually.rejectedWith("Invalid feature flag: InvalidTargeting2. Audience.DefaultRolloutPercentage must be a number between 0 and 100."), + expect(featureManager.isEnabled("InvalidTargeting3", {})).eventually.rejectedWith("Invalid feature flag: InvalidTargeting3. RolloutPercentage of group Stage1 must be a number between 0 and 100."), + expect(featureManager.isEnabled("InvalidTargeting4", {})).eventually.rejectedWith("Invalid feature flag: InvalidTargeting4. RolloutPercentage of group Stage1 must be a number between 0 and 100."), ]); }); @@ -131,15 +131,31 @@ describe("targeting filter", () => { ]); }); - it("should throw error if app context is not provided", () => { + it("should evaluate feature with targeting filter with targeting context accessor", async () => { const dataSource = new Map(); dataSource.set("feature_management", { feature_flags: [complexTargetingFeature] }); + let userId = ""; + let groups: string[] = []; + const testTargetingContextAccessor = { + getTargetingContext: () => { + return { userId: userId, groups: groups }; + } + }; const provider = new ConfigurationMapFeatureFlagProvider(dataSource); - const featureManager = new FeatureManager(provider); - - return expect(featureManager.isEnabled("ComplexTargeting")).eventually.rejectedWith("The app context is required for targeting filter."); + const featureManager = new FeatureManager(provider, {targetingContextAccessor: testTargetingContextAccessor}); + + userId = "Aiden"; + expect(await featureManager.isEnabled("ComplexTargeting")).to.eq(false); + userId = "Blossom"; + expect(await featureManager.isEnabled("ComplexTargeting")).to.eq(true); + expect(await featureManager.isEnabled("ComplexTargeting", {userId: "Aiden"})).to.eq(false); // targeting id will be overridden + userId = "Aiden"; + groups = ["Stage2"]; + expect(await featureManager.isEnabled("ComplexTargeting")).to.eq(true); + userId = "Chris"; + expect(await featureManager.isEnabled("ComplexTargeting")).to.eq(false); }); }); diff --git a/sdk/feature-management/test/variant.test.ts b/src/feature-management/test/variant.test.ts similarity index 52% rename from sdk/feature-management/test/variant.test.ts rename to src/feature-management/test/variant.test.ts index ddfd90f..edff9a0 100644 --- a/sdk/feature-management/test/variant.test.ts +++ b/src/feature-management/test/variant.test.ts @@ -4,7 +4,7 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "../"; -import { Features, featureFlagsConfigurationObject } from "./sampleFeatureFlags.js"; +import { Features, featureFlagsConfigurationObject } from "./sampleVariantFeatureFlags.js"; chai.use(chaiAsPromised); const expect = chai.expect; @@ -20,35 +20,35 @@ describe("feature variant", () => { describe("valid scenarios", () => { const context = { userId: "Marsha", groups: ["Group1"] }; - it("default allocation with disabled feature", async () => { + it("should perform default allocation with disabled feature", async () => { const variant = await featureManager.getVariant(Features.VariantFeatureDefaultDisabled, context); expect(variant).not.to.be.undefined; expect(variant?.name).eq("Small"); expect(variant?.configuration).eq("300px"); }); - it("default allocation with enabled feature", async () => { + it("should perform default allocation with enabled feature", async () => { const variant = await featureManager.getVariant(Features.VariantFeatureDefaultEnabled, context); expect(variant).not.to.be.undefined; expect(variant?.name).eq("Medium"); expect(variant?.configuration).deep.eq({ Size: "450px", Color: "Purple" }); }); - it("user allocation", async () => { + it("should perform user allocation", async () => { const variant = await featureManager.getVariant(Features.VariantFeatureUser, context); expect(variant).not.to.be.undefined; expect(variant?.name).eq("Small"); expect(variant?.configuration).eq("300px"); }); - it("group allocation", async () => { + it("should perform group allocation", async () => { const variant = await featureManager.getVariant(Features.VariantFeatureGroup, context); expect(variant).not.to.be.undefined; expect(variant?.name).eq("Small"); expect(variant?.configuration).eq("300px"); }); - it("percentile allocation with seed", async () => { + it("should perform percentile allocation with seed", async () => { const variant = await featureManager.getVariant(Features.VariantFeaturePercentileOn, context); expect(variant).not.to.be.undefined; expect(variant?.name).eq("Big"); @@ -57,7 +57,7 @@ describe("feature variant", () => { expect(variant2).to.be.undefined; }); - it("overwrite enabled status", async () => { + it("should overwrite enabled status", async () => { const enabledStatus = await featureManager.isEnabled(Features.VariantFeaturePercentileOn, context); expect(enabledStatus).to.be.false; // featureFlag.enabled = true, overridden to false by variant `Big`. }); @@ -67,28 +67,55 @@ describe("feature variant", () => { describe("invalid scenarios", () => { const context = { userId: "Jeff" }; - it("return undefined when no variants are specified", async () => { + it("should return undefined when no variants are specified", async () => { const variant = await featureManager.getVariant(Features.VariantFeatureNoVariants, context); expect(variant).to.be.undefined; }); - it("return undefined when no allocation is specified", async () => { + it("should return undefined when no allocation is specified", async () => { const variant = await featureManager.getVariant(Features.VariantFeatureNoAllocation, context); expect(variant).to.be.undefined; }); - it("only support configuration value", async () => { - const variant = await featureManager.getVariant(Features.VariantFeatureBothConfigurations, context); - expect(variant).not.to.be.undefined; - expect(variant?.configuration).eq("600px"); - }); - // requires IFeatureFlagProvider to throw an exception on validation - it("throw exception for invalid StatusOverride value"); + it("should throw exception for invalid StatusOverride value", async () => { + await expect(featureManager.getVariant(Features.VariantFeatureInvalidStatusOverride, context)) + .eventually.rejectedWith("Invalid feature flag: VariantFeatureInvalidStatusOverride. Variant 'status_override' must be 'None', 'Enabled', or 'Disabled'."); + }); // requires IFeatureFlagProvider to throw an exception on validation - it("throw exception for invalid doubles From and To in the Percentile section"); + it("should throw exception for invalid doubles From and To in the Percentile section", async () => { + await expect(featureManager.getVariant(Features.VariantFeatureInvalidFromTo, context)) + .eventually.rejectedWith("Invalid feature flag: VariantFeatureInvalidFromTo. Percentile allocation 'from' must be a number between 0 and 100."); + }); }); +}); +describe("variant assignment with targeting context accessor", () => { + it("should assign variant based on targeting context accessor", async () => { + let userId = ""; + let groups: string[] = []; + const testTargetingContextAccessor = { + getTargetingContext: () => { + return { userId: userId, groups: groups }; + } + }; + const provider = new ConfigurationObjectFeatureFlagProvider(featureFlagsConfigurationObject); + const featureManager = new FeatureManager(provider, {targetingContextAccessor: testTargetingContextAccessor}); + userId = "Marsha"; + let variant = await featureManager.getVariant(Features.VariantFeatureUser); + expect(variant).not.to.be.undefined; + expect(variant?.name).eq("Small"); + userId = "Jeff"; + variant = await featureManager.getVariant(Features.VariantFeatureUser); + expect(variant).to.be.undefined; + variant = await featureManager.getVariant(Features.VariantFeatureUser, {userId: "Marsha"}); // targeting id will be overridden + expect(variant).not.to.be.undefined; + expect(variant?.name).eq("Small"); + groups = ["Group1"]; + variant = await featureManager.getVariant(Features.VariantFeatureGroup); + expect(variant).not.to.be.undefined; + expect(variant?.name).eq("Small"); + }); }); diff --git a/src/feature-management/tsconfig.json b/src/feature-management/tsconfig.json new file mode 100644 index 0000000..d71e705 --- /dev/null +++ b/src/feature-management/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "lib": [ + "DOM", + "WebWorker", + "ESNext" + ], + "skipDefaultLibCheck": true, + "module": "ESNext", + "moduleResolution": "Node", + "target": "ES2022", + "strictNullChecks": true, + "strictFunctionTypes": true, + "sourceMap": true, + "inlineSources": true + }, + "exclude": [ + "node_modules", + "**/node_modules/*" + ] +} \ No newline at end of file diff --git a/sdk/feature-management/tsconfig.test.json b/src/feature-management/tsconfig.test.json similarity index 100% rename from sdk/feature-management/tsconfig.test.json rename to src/feature-management/tsconfig.test.json pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy