From c541c29ea4339f1f7c78a245ec66a741d87df40c Mon Sep 17 00:00:00 2001 From: Yan Zhang <2351748+Eskibear@users.noreply.github.com> Date: Thu, 23 May 2024 10:46:23 +0800 Subject: [PATCH 01/16] fix evaluation: FeatureFlag.enabled defaults to false (#11) --- src/featureManager.ts | 13 ++++++-- test/noFilters.test.ts | 70 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 test/noFilters.test.ts diff --git a/src/featureManager.ts b/src/featureManager.ts index 99b7700..d113f74 100644 --- a/src/featureManager.ts +++ b/src/featureManager.ts @@ -36,8 +36,11 @@ export class FeatureManager { return false; } - if (featureFlag.enabled === false) { - // If the feature is explicitly disabled, then it is disabled. + // Ensure that the feature flag is in the correct format. Feature providers should validate the feature flags, but we do it here as a safeguard. + validateFeatureFlagFormat(featureFlag); + + if (featureFlag.enabled !== true) { + // If the feature is not explicitly enabled, then it is disabled by default. return false; } @@ -77,3 +80,9 @@ export class FeatureManager { interface FeatureManagerOptions { customFilters?: IFeatureFilter[]; } + +function validateFeatureFlagFormat(featureFlag: any): void { + if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== "boolean") { + throw new Error(`Feature flag ${featureFlag.id} has an invalid 'enabled' value.`); + } +} diff --git a/test/noFilters.test.ts b/test/noFilters.test.ts new file mode 100644 index 0000000..a303748 --- /dev/null +++ b/test/noFilters.test.ts @@ -0,0 +1,70 @@ +// 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 } from "./exportedApi"; + +const featureFlagsDataObject = { + "feature_management": { + "feature_flags": [ + { + "id": "BooleanTrue", + "description": "A feature flag with no Filters, that returns true.", + "enabled": true, + "conditions": { + "client_filters": [] + } + }, + { + "id": "BooleanFalse", + "description": "A feature flag with no Filters, that returns false.", + "enabled": false, + "conditions": { + "client_filters": [] + } + }, + { + "id": "InvalidEnabled", + "description": "A feature flag with an invalid 'enabled' value, that throws an exception.", + "enabled": "invalid", + "conditions": { + "client_filters": [] + } + }, + { + "id": "Minimal", + "enabled": true + }, + { + "id": "NoEnabled" + }, + { + "id": "EmptyConditions", + "description": "A feature flag with no values in conditions, that returns true.", + "enabled": true, + "conditions": { + } + } + ] + } +}; + +describe("feature flags with no filters", () => { + it("should validate feature flags without filters", () => { + const provider = new ConfigurationObjectFeatureFlagProvider(featureFlagsDataObject); + const featureManager = new FeatureManager(provider); + + 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("Minimal")).eventually.eq(true), + expect(featureManager.isEnabled("NoEnabled")).eventually.eq(false), + expect(featureManager.isEnabled("EmptyConditions")).eventually.eq(true) + ]); + }); +}); From b70bbd341107b4093632d7272418884b5138de16 Mon Sep 17 00:00:00 2001 From: Yan Zhang <2351748+Eskibear@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:06:00 +0800 Subject: [PATCH 02/16] update FeatureFlag type to match v2 schema (#12) Co-authored-by: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> --- src/featureManager.ts | 4 +- src/model.ts | 157 +++++++++++++++++++++++++++++++++++------- 2 files changed, 136 insertions(+), 25 deletions(-) diff --git a/src/featureManager.ts b/src/featureManager.ts index d113f74..bce1325 100644 --- a/src/featureManager.ts +++ b/src/featureManager.ts @@ -50,14 +50,14 @@ export class FeatureManager { return true; } - const requirementType = featureFlag.conditions?.requirement_type ?? RequirementType.Any; // default to any. + const requirementType: RequirementType = featureFlag.conditions?.requirement_type ?? "Any"; // default to any. /** * While iterating through the client filters, we short-circuit the evaluation based on the requirement type. * - When requirement type is "All", the feature is enabled if all client filters are matched. If any client filter is not matched, the feature is disabled, otherwise it is enabled. `shortCircuitEvaluationResult` is false. * - When requirement type is "Any", the feature is enabled if any client filter is matched. If any client filter is matched, the feature is enabled, otherwise it is disabled. `shortCircuitEvaluationResult` is true. */ - const shortCircuitEvaluationResult: boolean = requirementType === RequirementType.Any; + const shortCircuitEvaluationResult: boolean = requirementType === "Any"; for (const clientFilter of clientFilters) { const matchedFeatureFilter = this.#featureFilters.get(clientFilter.name); diff --git a/src/model.ts b/src/model.ts index 40c5404..fc6fa19 100644 --- a/src/model.ts +++ b/src/model.ts @@ -1,61 +1,172 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureFlag.v1.1.0.schema.json +// 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 - + id: string; /** * A description of the feature. */ - description?: string - + description?: string; /** * A display name for the feature to use for display rather than the ID. */ - display_name?: string - + 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 + 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?: Variant[]; + /** + * 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 { /** - * The declaration of conditions used to dynamically enable features. + * Determines whether any or all registered client filters must be evaluated as true for the feature to be considered enabled. */ - conditions?: FeatureEnablementConditions + 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 enum RequirementType { - Any = "Any", - All = "All" +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 FeatureEnablementConditions { +interface Variant { /** - * Determines whether any or all registered client filters must be evaluated as true for the feature to be considered enabled. + * The name used to refer to a feature variant. */ - requirement_type?: RequirementType + name: string; + /** + * The configuration value for this feature variant. + */ + configuration_value?: unknown; + /** + * The path to a configuration section used as the configuration value for this feature variant. + */ + configuration_reference?: string; + /** + * 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 { /** - * Filters that must run on the client and be evaluated as true for the feature to be considered enabled. + * 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. */ - client_filters?: ClientFilter[] + to: number; } -export interface ClientFilter { +/** +* The declaration of options used to configure telemetry for this feature. +*/ +interface TelemetryOptions { /** - * The name used to refer to and require a client filter. + * Indicates if telemetry is enabled. */ - name: string + enabled?: boolean; /** - * Custom parameters for a given client filter. A client filter can require any set of parameters of any type. + * A container for metadata that should be bundled with flag telemetry. */ - parameters?: unknown + metadata?: Record; } // Feature Management Section fed into feature manager. From 1dacdd8831e20696040693cb51c27a4e64845d88 Mon Sep 17 00:00:00 2001 From: Yan Zhang <2351748+Eskibear@users.noreply.github.com> Date: Wed, 31 Jul 2024 19:42:02 +0800 Subject: [PATCH 03/16] Setup CI with GitHub Actions (#15) --- .github/workflows/ci.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4174865 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +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] + + 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' + - run: npm ci + - run: npm run lint + - run: npm run build + - run: npm test \ No newline at end of file From 53806beded147b05c15728cdeff74e6ce7675c19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:00:20 +0800 Subject: [PATCH 04/16] Bump braces from 3.0.2 to 3.0.3 (#16) Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9636ee3..8b061cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1069,12 +1069,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1578,9 +1578,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" From 81b3f67737d807bd9ec7230136baab750d163c25 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:58:40 +0800 Subject: [PATCH 05/16] add package badge (#18) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4b86de7..54aa97a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Microsoft Feature Management for JavaScript +[![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. From 1b97687cf713b85d76d53588d1ae8c774fc11bdb Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:27:34 +0800 Subject: [PATCH 06/16] Update linting rule (#21) * update linting rule * update --- .eslintrc | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 9af943a..f2852f8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -45,6 +45,17 @@ "error", "always" ], - "no-trailing-spaces": "warn" + "no-trailing-spaces": "error", + "space-before-blocks": [ + "error", + "always" + ], + "no-multi-spaces": "error", + "no-multiple-empty-lines": [ + "error", + { + "max": 1 + } + ] } } \ No newline at end of file From 524fb11272a971d473a06985ef5bb91afb5447d0 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:27:00 +0800 Subject: [PATCH 07/16] small fix (#26) --- package.json | 4 +--- test/featureManager.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0d917bb..ea96126 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "chai": "^4.4.0", + "chai-as-promised": "^7.1.1", "eslint": "^8.56.0", "mocha": "^10.2.0", "rimraf": "^5.0.5", @@ -50,8 +51,5 @@ "rollup-plugin-dts": "^6.1.0", "tslib": "^2.6.2", "typescript": "^5.3.3" - }, - "dependencies": { - "chai-as-promised": "^7.1.1" } } diff --git a/test/featureManager.test.ts b/test/featureManager.test.ts index b22484b..2fd9699 100644 --- a/test/featureManager.test.ts +++ b/test/featureManager.test.ts @@ -72,6 +72,6 @@ describe("feature manager", () => { ]); }); - it("shoud evaluate features with conditions"); - it("shoud override default filters with custom filters"); + it("should evaluate features with conditions"); + it("should override default filters with custom filters"); }); From 09ddb6dd8ceea1eaf2ec352d5efd47fd7fdc48b3 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:16:16 +0800 Subject: [PATCH 08/16] enforce to use semicolon (#28) --- .eslintrc | 3 ++- src/filter/TargetingFilter.ts | 2 +- src/model.ts | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.eslintrc b/.eslintrc index f2852f8..27a13cc 100644 --- a/.eslintrc +++ b/.eslintrc @@ -56,6 +56,7 @@ { "max": 1 } - ] + ], + "semi": ["error", "always"] } } \ No newline at end of file diff --git a/src/filter/TargetingFilter.ts b/src/filter/TargetingFilter.ts index 1f55901..2e1e2fe 100644 --- a/src/filter/TargetingFilter.ts +++ b/src/filter/TargetingFilter.ts @@ -127,7 +127,7 @@ function constructAudienceContextId(featureName: string, userId: string | undefi if (groupName !== undefined) { contextId += `\n${groupName}`; } - return contextId + return contextId; } function stringToUint32(str: string): number { diff --git a/src/model.ts b/src/model.ts index fc6fa19..7ee9cc4 100644 --- a/src/model.ts +++ b/src/model.ts @@ -172,8 +172,8 @@ interface TelemetryOptions { // 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 const FEATURE_MANAGEMENT_KEY = "feature_management"; +export const FEATURE_FLAGS_KEY = "feature_flags"; export interface FeatureManagementConfiguration { feature_management: FeatureManagement From 122bb9b4e32153e5f28cbf0e4f5053301e53a2c0 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Wed, 21 Aug 2024 10:15:57 +0800 Subject: [PATCH 09/16] expose IFeatureFilter (#29) --- src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index cea5cb6..509b92e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,4 +2,5 @@ // Licensed under the MIT license. export { FeatureManager } from "./featureManager"; -export { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider, IFeatureFlagProvider } from "./featureProvider"; +export { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider, IFeatureFlagProvider } from "./featureProvider"; +export { IFeatureFilter } from "./filter/FeatureFilter"; From 8711f6a898e9a693dd456faa8c9d9ef0e594aa39 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Fri, 23 Aug 2024 23:17:54 +0800 Subject: [PATCH 10/16] add .js extension (#30) --- src/featureManager.ts | 10 +++++----- src/featureProvider.ts | 4 ++-- src/filter/TargetingFilter.ts | 2 +- src/filter/TimeWindowFilter.ts | 2 +- src/index.ts | 6 +++--- test/exportedApi.ts | 2 +- test/featureManager.test.ts | 2 +- test/noFilters.test.ts | 2 +- test/targetingFilter.test.ts | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/featureManager.ts b/src/featureManager.ts index bce1325..402510f 100644 --- a/src/featureManager.ts +++ b/src/featureManager.ts @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { TimeWindowFilter } from "./filter/TimeWindowFilter"; -import { IFeatureFilter } from "./filter/FeatureFilter"; -import { RequirementType } from "./model"; -import { IFeatureFlagProvider } from "./featureProvider"; -import { TargetingFilter } from "./filter/TargetingFilter"; +import { TimeWindowFilter } from "./filter/TimeWindowFilter.js"; +import { IFeatureFilter } from "./filter/FeatureFilter.js"; +import { RequirementType } from "./model.js"; +import { IFeatureFlagProvider } from "./featureProvider.js"; +import { TargetingFilter } from "./filter/TargetingFilter.js"; export class FeatureManager { #provider: IFeatureFlagProvider; diff --git a/src/featureProvider.ts b/src/featureProvider.ts index 779b913..9b63876 100644 --- a/src/featureProvider.ts +++ b/src/featureProvider.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { IGettable } from "./gettable"; -import { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./model"; +import { IGettable } from "./gettable.js"; +import { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./model.js"; export interface IFeatureFlagProvider { /** diff --git a/src/filter/TargetingFilter.ts b/src/filter/TargetingFilter.ts index 2e1e2fe..3c0d9ca 100644 --- a/src/filter/TargetingFilter.ts +++ b/src/filter/TargetingFilter.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { IFeatureFilter } from "./FeatureFilter"; +import { IFeatureFilter } from "./FeatureFilter.js"; import { createHash } from "crypto"; type TargetingFilterParameters = { diff --git a/src/filter/TimeWindowFilter.ts b/src/filter/TimeWindowFilter.ts index 92d1f24..e1442c8 100644 --- a/src/filter/TimeWindowFilter.ts +++ b/src/filter/TimeWindowFilter.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { IFeatureFilter } from "./FeatureFilter"; +import { IFeatureFilter } from "./FeatureFilter.js"; // [Start, End) type TimeWindowParameters = { diff --git a/src/index.ts b/src/index.ts index 509b92e..7e99672 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export { FeatureManager } from "./featureManager"; -export { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider, IFeatureFlagProvider } from "./featureProvider"; -export { IFeatureFilter } from "./filter/FeatureFilter"; +export { FeatureManager } from "./featureManager.js"; +export { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider, IFeatureFlagProvider } from "./featureProvider.js"; +export { IFeatureFilter } from "./filter/FeatureFilter.js"; diff --git a/test/exportedApi.ts b/test/exportedApi.ts index f447b46..848ae85 100644 --- a/test/exportedApi.ts +++ b/test/exportedApi.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export * from "../src"; +export * from "../src/index.js"; diff --git a/test/featureManager.test.ts b/test/featureManager.test.ts index 2fd9699..9c1cc42 100644 --- a/test/featureManager.test.ts +++ b/test/featureManager.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { FeatureManager, ConfigurationObjectFeatureFlagProvider, ConfigurationMapFeatureFlagProvider } from "./exportedApi"; +import { FeatureManager, ConfigurationObjectFeatureFlagProvider, ConfigurationMapFeatureFlagProvider } from "./exportedApi.js"; describe("feature manager", () => { it("should load from json string", () => { diff --git a/test/noFilters.test.ts b/test/noFilters.test.ts index a303748..2963b86 100644 --- a/test/noFilters.test.ts +++ b/test/noFilters.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "./exportedApi"; +import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "./exportedApi.js"; const featureFlagsDataObject = { "feature_management": { diff --git a/test/targetingFilter.test.ts b/test/targetingFilter.test.ts index 6d4fb15..40e9b09 100644 --- a/test/targetingFilter.test.ts +++ b/test/targetingFilter.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { FeatureManager, ConfigurationMapFeatureFlagProvider } from "./exportedApi"; +import { FeatureManager, ConfigurationMapFeatureFlagProvider } from "./exportedApi.js"; const complexTargetingFeature = { "id": "ComplexTargeting", From 85db328dd1035ec1833c043efb5661515ce3bb91 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:55:33 +0800 Subject: [PATCH 11/16] Use web api crypto instead of node:crypto for targeting evaluation (#25) * use web crypto instead of node:crypto * fix lint * make code compatible with nodejs & browser * fix linting * add error message * fix lint * add browser test * add complex targeting testcase for browser * install chromium only * handle esm when fallback to native node.js crypto * mark crypto as external * fix lint --- .github/workflows/ci.yml | 3 +- .gitignore | 4 +- package-lock.json | 73 +++++++++++++++++++++++++++++++++-- package.json | 29 +++++++------- playwright.config.ts | 15 +++++++ rollup.config.mjs | 25 ++++++++++-- src/filter/TargetingFilter.ts | 55 +++++++++++++++++++------- test/browser/browser.test.ts | 22 +++++++++++ test/browser/index.html | 27 +++++++++++++ test/browser/testcases.js | 63 ++++++++++++++++++++++++++++++ tsconfig.base.json | 3 -- tsconfig.json | 10 ----- tsconfig.test.json | 2 +- 13 files changed, 279 insertions(+), 52 deletions(-) create mode 100644 playwright.config.ts create mode 100644 test/browser/browser.test.ts create mode 100644 test/browser/index.html create mode 100644 test/browser/testcases.js delete mode 100644 tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4174865..1493d90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,4 +30,5 @@ jobs: - run: npm ci - run: npm run lint - run: npm run build - - run: npm test \ No newline at end of file + - run: npm run test + - run: npm run test-browser \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1ccd7b9..5fd795e 100644 --- a/.gitignore +++ b/.gitignore @@ -399,7 +399,6 @@ FodyWeavers.xsd # bundled folder dist/ -dist-esm/ out/ types/ @@ -411,3 +410,6 @@ types/ # examples examples/package-lock.json + +# playwright test result +test-results diff --git a/package-lock.json b/package-lock.json index 8b061cc..0988ed1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,15 @@ "name": "@microsoft/feature-management", "version": "1.0.0-preview", "license": "MIT", - "dependencies": { - "chai-as-promised": "^7.1.1" - }, "devDependencies": { + "@playwright/test": "^1.46.1", "@rollup/plugin-typescript": "^11.1.5", "@types/mocha": "^10.0.6", "@types/node": "^20.10.7", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "chai": "^4.4.0", + "chai-as-promised": "^7.1.1", "eslint": "^8.56.0", "mocha": "^10.2.0", "rimraf": "^5.0.5", @@ -467,6 +466,21 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz", + "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==", + "dev": true, + "dependencies": { + "playwright": "1.46.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/plugin-typescript": { "version": "11.1.5", "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.5.tgz", @@ -1039,6 +1053,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, "engines": { "node": "*" } @@ -1111,6 +1126,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.0.tgz", "integrity": "sha512-x9cHNq1uvkCdU+5xTkNh5WtgD4e4yDFCsp9jVc7N7qVeKeftv3gO/ZrviX5d+3ZfxdYnZXZYujjRInu1RogU6A==", + "dev": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -1128,6 +1144,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, "dependencies": { "check-error": "^1.0.2" }, @@ -1155,6 +1172,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, "dependencies": { "get-func-name": "^2.0.2" }, @@ -1283,6 +1301,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, "dependencies": { "type-detect": "^4.0.0" }, @@ -1727,6 +1746,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, "engines": { "node": "*" } @@ -2127,6 +2147,7 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, "dependencies": { "get-func-name": "^2.0.1" } @@ -2484,6 +2505,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, "engines": { "node": "*" } @@ -2500,6 +2522,50 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", + "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==", + "dev": true, + "dependencies": { + "playwright-core": "1.46.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz", + "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2927,6 +2993,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, "engines": { "node": ">=4" } diff --git a/package.json b/package.json index ea96126..8c9c012 100644 --- a/package.json +++ b/package.json @@ -2,30 +2,24 @@ "name": "@microsoft/feature-management", "version": "1.0.0-preview", "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/index.js", - "module": "./dist-esm/index.js", + "main": "./dist/commonjs/index.js", + "module": "./dist/esm/index.js", "types": "types/index.d.ts", "files": [ - "dist/**/*.js", - "dist/**/*.map", - "dist/**/*.d.ts", - "dist-esm/**/*.js", - "dist-esm/**/*.map", - "dist-esm/**/*.d.ts", - "types/**/*.d.ts", + "dist/", + "types/", "LICENSE", "README.md" ], "scripts": { - "build": "npm run clean && npm run build-cjs && npm run build-esm && npm run build-test", - "build-cjs": "rollup --config", - "build-esm": "tsc -p ./tsconfig.json", + "build": "npm run clean && rollup --config && npm run build-test", "build-test": "tsc -p ./tsconfig.test.json", - "clean": "rimraf dist dist-esm out types", + "clean": "rimraf dist out types", "dev": "rollup --config --watch", - "lint": "eslint src/ test/", - "fix-lint": "eslint src/ test/ --fix", - "test": "mocha out/test/*.test.{js,cjs,mjs} --parallel" + "lint": "eslint src/ test/ --ignore-pattern test/browser/testcases.js", + "fix-lint": "eslint src/ test/ --fix --ignore-pattern test/browser/testcases.js", + "test": "mocha out/test/*.test.{js,cjs,mjs} --parallel", + "test-browser": "npx playwright install chromium && npx playwright test" }, "repository": { "type": "git", @@ -42,6 +36,7 @@ "@types/node": "^20.10.7", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", + "@playwright/test": "^1.46.1", "chai": "^4.4.0", "chai-as-promised": "^7.1.1", "eslint": "^8.56.0", @@ -51,5 +46,7 @@ "rollup-plugin-dts": "^6.1.0", "tslib": "^2.6.2", "typescript": "^5.3.3" + }, + "dependencies": { } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..e379b86 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,15 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './test/browser', + fullyParallel: true, + + retries: 0, + reporter: 'list', + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + } + ], +}); diff --git a/rollup.config.mjs b/rollup.config.mjs index b91fd90..42018b0 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -4,13 +4,27 @@ import dts from "rollup-plugin-dts"; export default [ { + external: ["crypto"], input: "src/index.ts", output: [ { - file: "dist/index.js", + dir: "dist/commonjs/", format: "cjs", - sourcemap: true + sourcemap: true, + preserveModules: true, + }, + { + dir: "dist/esm/", + format: "esm", + sourcemap: true, + preserveModules: true, }, + { + file: "dist/umd/index.js", + format: "umd", + name: 'FeatureManagement', + sourcemap: true + } ], plugins: [ typescript({ @@ -28,13 +42,16 @@ export default [ "strictFunctionTypes": true, "sourceMap": true, "inlineSources": true - } + }, + "exclude": [ + "test/**/*" + ] }) ], }, { input: "src/index.ts", - output: [{ file: "types/index.d.ts", format: "es" }], + output: [{ file: "types/index.d.ts", format: "esm" }], plugins: [dts()], }, ]; diff --git a/src/filter/TargetingFilter.ts b/src/filter/TargetingFilter.ts index 3c0d9ca..406d846 100644 --- a/src/filter/TargetingFilter.ts +++ b/src/filter/TargetingFilter.ts @@ -2,7 +2,6 @@ // Licensed under the MIT license. import { IFeatureFilter } from "./FeatureFilter.js"; -import { createHash } from "crypto"; type TargetingFilterParameters = { Audience: { @@ -32,7 +31,7 @@ type TargetingFilterAppContext = { export class TargetingFilter implements IFeatureFilter { name: string = "Microsoft.Targeting"; - evaluate(context: TargetingFilterEvaluationContext, appContext?: TargetingFilterAppContext): boolean { + async evaluate(context: TargetingFilterEvaluationContext, appContext?: TargetingFilterAppContext): Promise { const { featureName, parameters } = context; TargetingFilter.#validateParameters(parameters); @@ -72,7 +71,7 @@ export class TargetingFilter implements IFeatureFilter { if (appContext.groups.includes(group.Name)) { const audienceContextId = constructAudienceContextId(featureName, appContext.userId, group.Name); const rolloutPercentage = group.RolloutPercentage; - if (TargetingFilter.#isTargeted(audienceContextId, rolloutPercentage)) { + if (await TargetingFilter.#isTargeted(audienceContextId, rolloutPercentage)) { return true; } } @@ -84,12 +83,12 @@ export class TargetingFilter implements IFeatureFilter { return TargetingFilter.#isTargeted(defaultContextId, parameters.Audience.DefaultRolloutPercentage); } - static #isTargeted(audienceContextId: string, rolloutPercentage: number): boolean { + static async #isTargeted(audienceContextId: string, rolloutPercentage: number): Promise { if (rolloutPercentage === 100) { return true; } // Cryptographic hashing algorithms ensure adequate entropy across hash values. - const contextMarker = stringToUint32(audienceContextId); + const contextMarker = await stringToUint32(audienceContextId); const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100; return contextPercentage < rolloutPercentage; } @@ -130,14 +129,44 @@ function constructAudienceContextId(featureName: string, userId: string | undefi return contextId; } -function stringToUint32(str: string): number { - // Create a SHA-256 hash of the string - const hash = createHash("sha256").update(str).digest(); +async function stringToUint32(str: string): Promise { + let crypto; - // Get the first 4 bytes of the hash - const first4Bytes = hash.subarray(0, 4); + // Check for browser environment + if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) { + crypto = window.crypto; + } + // Check for Node.js environment + else if (typeof global !== "undefined" && global.crypto) { + crypto = global.crypto; + } + // Fallback to native Node.js crypto module + else { + try { + if (typeof module !== "undefined" && module.exports) { + crypto = require("crypto"); + } + else { + crypto = await import("crypto"); + } + } catch (error) { + console.error("Failed to load the crypto module:", error.message); + throw error; + } + } - // Convert the 4 bytes to a uint32 with little-endian encoding - const uint32 = first4Bytes.readUInt32LE(0); - return uint32; + // In the browser, use crypto.subtle.digest + if (crypto.subtle) { + const data = new TextEncoder().encode(str); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const dataView = new DataView(hashBuffer); + const uint32 = dataView.getUint32(0, true); + return uint32; + } + // In Node.js, use the crypto module's hash function + else { + const hash = crypto.createHash("sha256").update(str).digest(); + const uint32 = hash.readUInt32LE(0); + return uint32; + } } diff --git a/test/browser/browser.test.ts b/test/browser/browser.test.ts new file mode 100644 index 0000000..e6b137c --- /dev/null +++ b/test/browser/browser.test.ts @@ -0,0 +1,22 @@ +import { test } from "@playwright/test"; +import chai from "chai"; +import path from "path"; + +test("Testcase can pass in browser environment", async ({ page }) => { + + const filePath = path.join(__dirname, "index.html"); + + let hasPageError = false; + + page.on("pageerror", (err) => { + hasPageError = true; + console.log(`Page Error: ${err.message}`); + }); + + await page.goto(`file:${filePath}`); + + const failures = await page.evaluate(() => (window as any).mochaFailures); + + chai.expect(failures).to.equal(0); + chai.expect(hasPageError).to.be.false; +}); diff --git a/test/browser/index.html b/test/browser/index.html new file mode 100644 index 0000000..57df7fd --- /dev/null +++ b/test/browser/index.html @@ -0,0 +1,27 @@ + + + + + Mocha Tests + + + + +
+ + + + + + + + + + \ No newline at end of file diff --git a/test/browser/testcases.js b/test/browser/testcases.js new file mode 100644 index 0000000..ba865ed --- /dev/null +++ b/test/browser/testcases.js @@ -0,0 +1,63 @@ +const ConfigurationObjectFeatureFlagProvider = FeatureManagement.ConfigurationObjectFeatureFlagProvider; +const FeatureManager = FeatureManagement.FeatureManager; + +describe("feature manager", () => { + it("should load from json string", + async () => { + const jsonObject = { + "feature_management": { + "feature_flags": [ + { + "id": "ComplexTargeting", + "description": "A feature flag using a targeting filter, that will return true for Alice, Stage1, and 50% of Stage2. Dave and Stage3 are excluded. The default rollout percentage is 25%.", + "enabled": true, + "conditions": { + "client_filters": [ + { + "name": "Microsoft.Targeting", + "parameters": { + "Audience": { + "Users": [ + "Alice" + ], + "Groups": [ + { + "Name": "Stage1", + "RolloutPercentage": 100 + }, + { + "Name": "Stage2", + "RolloutPercentage": 50 + } + ], + "DefaultRolloutPercentage": 25, + "Exclusion": { + "Users": ["Dave"], + "Groups": ["Stage3"] + } + } + } + } + ] + } + } + ] + } + }; + + const provider = new ConfigurationObjectFeatureFlagProvider(jsonObject); + const featureManager = new FeatureManager(provider); + chai.expect(await featureManager.isEnabled("ComplexTargeting", { userId: "Aiden" })).to.eq(false); + chai.expect(await featureManager.isEnabled("ComplexTargeting", { userId: "Blossom" })).to.eq(true); + chai.expect(await featureManager.isEnabled("ComplexTargeting", { userId: "Alice" })).to.eq(true); + chai.expect(await featureManager.isEnabled("ComplexTargeting", { userId: "Aiden", groups: ["Stage1"] })).to.eq(true); + chai.expect(await featureManager.isEnabled("ComplexTargeting", { groups: ["Stage2"] })).to.eq(false); + chai.expect(await featureManager.isEnabled("ComplexTargeting", { userId: "Aiden", groups: ["Stage2"] })).to.eq(true); + chai.expect(await featureManager.isEnabled("ComplexTargeting", { userId: "Chris", groups: ["Stage2"] })).to.eq(false); + chai.expect(await featureManager.isEnabled("ComplexTargeting", { groups: ["Stage3"] })).to.eq(false), + chai.expect(await featureManager.isEnabled("ComplexTargeting", { userId: "Alice", groups: ["Stage3"] })).to.eq(false); + chai.expect(await featureManager.isEnabled("ComplexTargeting", { userId: "Blossom", groups: ["Stage3"] })).to.eq(false); + chai.expect(await featureManager.isEnabled("ComplexTargeting", { userId: "Dave", groups: ["Stage1"] })).to.eq(false); + } + ); +}); diff --git a/tsconfig.base.json b/tsconfig.base.json index 0f96a19..358df9e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -14,9 +14,6 @@ "sourceMap": true, "inlineSources": true }, - "include": [ - "src/**/*" - ], "exclude": [ "node_modules", "**/node_modules/*" diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index f2fc0e9..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "module": "ESNext", - "outDir": "./dist-esm" - }, - "include": [ - "src/**/*" - ] -} \ No newline at end of file diff --git a/tsconfig.test.json b/tsconfig.test.json index 3cbd3c0..8a81b80 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -5,6 +5,6 @@ "outDir": "./out" }, "include": [ - "test/**/*" + "test/*" ] } \ No newline at end of file From ea7ec832bd8d8a00d9a03c87ebb430b36ea94578 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Mon, 2 Sep 2024 17:08:25 +0800 Subject: [PATCH 12/16] use dist for test (#33) --- package.json | 2 +- test/exportedApi.ts | 4 ---- test/featureManager.test.ts | 2 +- test/noFilters.test.ts | 2 +- test/targetingFilter.test.ts | 2 +- 5 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 test/exportedApi.ts diff --git a/package.json b/package.json index 8c9c012..2b3a7cd 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "dev": "rollup --config --watch", "lint": "eslint src/ test/ --ignore-pattern test/browser/testcases.js", "fix-lint": "eslint src/ test/ --fix --ignore-pattern test/browser/testcases.js", - "test": "mocha out/test/*.test.{js,cjs,mjs} --parallel", + "test": "mocha out/*.test.{js,cjs,mjs} --parallel", "test-browser": "npx playwright install chromium && npx playwright test" }, "repository": { diff --git a/test/exportedApi.ts b/test/exportedApi.ts deleted file mode 100644 index 848ae85..0000000 --- a/test/exportedApi.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -export * from "../src/index.js"; diff --git a/test/featureManager.test.ts b/test/featureManager.test.ts index 9c1cc42..4c57a1e 100644 --- a/test/featureManager.test.ts +++ b/test/featureManager.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { FeatureManager, ConfigurationObjectFeatureFlagProvider, ConfigurationMapFeatureFlagProvider } from "./exportedApi.js"; +import { FeatureManager, ConfigurationObjectFeatureFlagProvider, ConfigurationMapFeatureFlagProvider } from "../"; describe("feature manager", () => { it("should load from json string", () => { diff --git a/test/noFilters.test.ts b/test/noFilters.test.ts index 2963b86..55639f7 100644 --- a/test/noFilters.test.ts +++ b/test/noFilters.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "./exportedApi.js"; +import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "../"; const featureFlagsDataObject = { "feature_management": { diff --git a/test/targetingFilter.test.ts b/test/targetingFilter.test.ts index 40e9b09..ac33f88 100644 --- a/test/targetingFilter.test.ts +++ b/test/targetingFilter.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { FeatureManager, ConfigurationMapFeatureFlagProvider } from "./exportedApi.js"; +import { FeatureManager, ConfigurationMapFeatureFlagProvider } from "../"; const complexTargetingFeature = { "id": "ComplexTargeting", From 98ce468e38deba6d1db26dfe6f738fdd29cbdcc8 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:30:28 +0800 Subject: [PATCH 13/16] Last one wins flag providers (#40) * last feature flag wins * fix lint --- src/featureProvider.ts | 4 ++-- test/featureManager.test.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/featureProvider.ts b/src/featureProvider.ts index 9b63876..c5a1aac 100644 --- a/src/featureProvider.ts +++ b/src/featureProvider.ts @@ -28,7 +28,7 @@ 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); + return featureConfig?.[FEATURE_FLAGS_KEY]?.findLast((feature) => feature.id === featureName); } async getFeatureFlags(): Promise { @@ -49,7 +49,7 @@ 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); + return featureFlags?.findLast((feature: FeatureFlag) => feature.id === featureName); } async getFeatureFlags(): Promise { diff --git a/test/featureManager.test.ts b/test/featureManager.test.ts index 4c57a1e..ea9c787 100644 --- a/test/featureManager.test.ts +++ b/test/featureManager.test.ts @@ -72,6 +72,36 @@ describe("feature manager", () => { ]); }); + 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 features with conditions"); it("should override default filters with custom filters"); }); From 72a80fcb635278a9c4bdd13c84b69774b4fe331e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 18:22:45 +0800 Subject: [PATCH 14/16] Bump rollup from 4.9.4 to 4.22.4 (#41) Bumps [rollup](https://github.com/rollup/rollup) from 4.9.4 to 4.22.4. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v4.9.4...v4.22.4) --- updated-dependencies: - dependency-name: rollup dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 152 +++++++++++++++++++++++++++++----------------- 1 file changed, 97 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0988ed1..f140bcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -530,9 +530,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.4.tgz", - "integrity": "sha512-ub/SN3yWqIv5CWiAZPHVS1DloyZsJbtXmX4HxUTIpS0BHm9pW5iYBo2mIZi+hE3AeiTzHz33blwSnhdUo+9NpA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", "cpu": [ "arm" ], @@ -543,9 +543,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.4.tgz", - "integrity": "sha512-ehcBrOR5XTl0W0t2WxfTyHCR/3Cq2jfb+I4W+Ch8Y9b5G+vbAecVv0Fx/J1QKktOrgUYsIKxWAKgIpvw56IFNA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", "cpu": [ "arm64" ], @@ -556,9 +556,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.4.tgz", - "integrity": "sha512-1fzh1lWExwSTWy8vJPnNbNM02WZDS8AW3McEOb7wW+nPChLKf3WG2aG7fhaUmfX5FKw9zhsF5+MBwArGyNM7NA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", "cpu": [ "arm64" ], @@ -569,9 +569,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.4.tgz", - "integrity": "sha512-Gc6cukkF38RcYQ6uPdiXi70JB0f29CwcQ7+r4QpfNpQFVHXRd0DfWFidoGxjSx1DwOETM97JPz1RXL5ISSB0pA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", "cpu": [ "x64" ], @@ -582,9 +582,22 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.4.tgz", - "integrity": "sha512-g21RTeFzoTl8GxosHbnQZ0/JkuFIB13C3T7Y0HtKzOXmoHhewLbVTFBQZu+z5m9STH6FZ7L/oPgU4Nm5ErN2fw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", "cpu": [ "arm" ], @@ -595,9 +608,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.4.tgz", - "integrity": "sha512-TVYVWD/SYwWzGGnbfTkrNpdE4HON46orgMNHCivlXmlsSGQOx/OHHYiQcMIOx38/GWgwr/po2LBn7wypkWw/Mg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", "cpu": [ "arm64" ], @@ -608,9 +621,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.4.tgz", - "integrity": "sha512-XcKvuendwizYYhFxpvQ3xVpzje2HHImzg33wL9zvxtj77HvPStbSGI9czrdbfrf8DGMcNNReH9pVZv8qejAQ5A==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", "cpu": [ "arm64" ], @@ -620,10 +633,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.4.tgz", - "integrity": "sha512-LFHS/8Q+I9YA0yVETyjonMJ3UA+DczeBd/MqNEzsGSTdNvSJa1OJZcSH8GiXLvcizgp9AlHs2walqRcqzjOi3A==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", "cpu": [ "riscv64" ], @@ -633,10 +659,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.4.tgz", - "integrity": "sha512-dIYgo+j1+yfy81i0YVU5KnQrIJZE8ERomx17ReU4GREjGtDW4X+nvkBak2xAUpyqLs4eleDSj3RrV72fQos7zw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", "cpu": [ "x64" ], @@ -647,9 +686,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.4.tgz", - "integrity": "sha512-RoaYxjdHQ5TPjaPrLsfKqR3pakMr3JGqZ+jZM0zP2IkDtsGa4CqYaWSfQmZVgFUCgLrTnzX+cnHS3nfl+kB6ZQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", "cpu": [ "x64" ], @@ -660,9 +699,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.4.tgz", - "integrity": "sha512-T8Q3XHV+Jjf5e49B4EAaLKV74BbX7/qYBRQ8Wop/+TyyU0k+vSjiLVSHNWdVd1goMjZcbhDmYZUYW5RFqkBNHQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", "cpu": [ "arm64" ], @@ -673,9 +712,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.4.tgz", - "integrity": "sha512-z+JQ7JirDUHAsMecVydnBPWLwJjbppU+7LZjffGf+Jvrxq+dVjIE7By163Sc9DKc3ADSU50qPVw0KonBS+a+HQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", "cpu": [ "ia32" ], @@ -686,9 +725,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.4.tgz", - "integrity": "sha512-LfdGXCV9rdEify1oxlN9eamvDSjv9md9ZVMAbNHA87xqIfFCxImxan9qZ8+Un54iK2nnqPlbnSi4R54ONtbWBw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", "cpu": [ "x64" ], @@ -2689,9 +2728,9 @@ } }, "node_modules/rollup": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.4.tgz", - "integrity": "sha512-2ztU7pY/lrQyXSCnnoU4ICjT/tCG9cdH3/G25ERqE3Lst6vl2BCM5hL2Nw+sslAvAf+ccKsAq1SkKQALyqhR7g==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -2704,19 +2743,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.4", - "@rollup/rollup-android-arm64": "4.9.4", - "@rollup/rollup-darwin-arm64": "4.9.4", - "@rollup/rollup-darwin-x64": "4.9.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.4", - "@rollup/rollup-linux-arm64-gnu": "4.9.4", - "@rollup/rollup-linux-arm64-musl": "4.9.4", - "@rollup/rollup-linux-riscv64-gnu": "4.9.4", - "@rollup/rollup-linux-x64-gnu": "4.9.4", - "@rollup/rollup-linux-x64-musl": "4.9.4", - "@rollup/rollup-win32-arm64-msvc": "4.9.4", - "@rollup/rollup-win32-ia32-msvc": "4.9.4", - "@rollup/rollup-win32-x64-msvc": "4.9.4", + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", "fsevents": "~2.3.2" } }, From 54360117bba83729d021a43543b697460235d147 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:06:13 +0800 Subject: [PATCH 15/16] update rollup version (#42) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b3a7cd..8c672dd 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "eslint": "^8.56.0", "mocha": "^10.2.0", "rimraf": "^5.0.5", - "rollup": "^4.9.4", + "rollup": "^4.22.4", "rollup-plugin-dts": "^6.1.0", "tslib": "^2.6.2", "typescript": "^5.3.3" From 4e711c9fb0315e6e80c31e3490d3347a67e8a2d0 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:11:21 +0800 Subject: [PATCH 16/16] Version bump 1.0.0 stable (#43) * version bump 1.0.0 stable * add version file * fix lint --- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 src/version.ts diff --git a/package-lock.json b/package-lock.json index f140bcf..0bc8262 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@microsoft/feature-management", - "version": "1.0.0-preview", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@microsoft/feature-management", - "version": "1.0.0-preview", + "version": "1.0.0", "license": "MIT", "devDependencies": { "@playwright/test": "^1.46.1", diff --git a/package.json b/package.json index 8c672dd..f7e5e01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/feature-management", - "version": "1.0.0-preview", + "version": "1.0.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", diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..dcc0b29 --- /dev/null +++ b/src/version.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export const VERSION = "1.0.0";