From 1d1679bdc4a2b97cdf5017bb101608d5d5e2942f Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Sat, 17 Aug 2024 20:17:15 +0800 Subject: [PATCH 01/12] use web crypto instead of node:crypto --- src/filter/TargetingFilter.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/filter/TargetingFilter.ts b/src/filter/TargetingFilter.ts index 1f55901..0ebadd5 100644 --- a/src/filter/TargetingFilter.ts +++ b/src/filter/TargetingFilter.ts @@ -2,7 +2,6 @@ // Licensed under the MIT license. import { IFeatureFilter } from "./FeatureFilter"; -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,14 @@ 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 { + const bytes = new TextEncoder().encode(str); - // Get the first 4 bytes of the hash - const first4Bytes = hash.subarray(0, 4); + const hashBuffer = await crypto.subtle.digest('SHA-256', bytes); - // Convert the 4 bytes to a uint32 with little-endian encoding - const uint32 = first4Bytes.readUInt32LE(0); + const dataView = new DataView(hashBuffer); + + // Convert the first 4 bytes to a uint32 with little-endian encoding + const uint32 = dataView.getUint32(0, true); return uint32; } From 525172db5a4999cbe80eec35059cc5020619412f Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Sat, 17 Aug 2024 20:39:32 +0800 Subject: [PATCH 02/12] fix lint --- src/filter/TargetingFilter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filter/TargetingFilter.ts b/src/filter/TargetingFilter.ts index 0ebadd5..e610140 100644 --- a/src/filter/TargetingFilter.ts +++ b/src/filter/TargetingFilter.ts @@ -132,7 +132,7 @@ function constructAudienceContextId(featureName: string, userId: string | undefi async function stringToUint32(str: string): Promise { const bytes = new TextEncoder().encode(str); - const hashBuffer = await crypto.subtle.digest('SHA-256', bytes); + const hashBuffer = await crypto.subtle.digest("SHA-256", bytes); const dataView = new DataView(hashBuffer); From e9b6e85af6661590d349129e79f78f778d0db50b Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 20 Aug 2024 14:37:24 +0800 Subject: [PATCH 03/12] make code compatible with nodejs & browser --- src/filter/TargetingFilter.ts | 38 ++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/filter/TargetingFilter.ts b/src/filter/TargetingFilter.ts index e610140..80fb98b 100644 --- a/src/filter/TargetingFilter.ts +++ b/src/filter/TargetingFilter.ts @@ -130,13 +130,33 @@ function constructAudienceContextId(featureName: string, userId: string | undefi } async function stringToUint32(str: string): Promise { - const bytes = new TextEncoder().encode(str); - - const hashBuffer = await crypto.subtle.digest("SHA-256", bytes); - - const dataView = new DataView(hashBuffer); + let crypto; + + // 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 { + crypto = require('crypto'); // maybe wrap with try-catch in case of uncovered runtimes... or you maybe want to fail the program because there's no way to calc hash then + } - // Convert the first 4 bytes to a uint32 with little-endian encoding - const uint32 = dataView.getUint32(0, true); - 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; + } +} \ No newline at end of file From 2b684c7a53197f8e6d019aad123c67de90cf2ed6 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 20 Aug 2024 14:39:18 +0800 Subject: [PATCH 04/12] fix linting --- src/filter/TargetingFilter.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/filter/TargetingFilter.ts b/src/filter/TargetingFilter.ts index 80fb98b..feed4e5 100644 --- a/src/filter/TargetingFilter.ts +++ b/src/filter/TargetingFilter.ts @@ -133,16 +133,16 @@ async function stringToUint32(str: string): Promise { let crypto; // Check for browser environment - if (typeof window !== 'undefined' && window.crypto && window.crypto.subtle) { + if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) { crypto = window.crypto; - } + } // Check for Node.js environment - else if (typeof global !== 'undefined' && global.crypto) { + else if (typeof global !== "undefined" && global.crypto) { crypto = global.crypto; - } + } // Fallback to native Node.js crypto module else { - crypto = require('crypto'); // maybe wrap with try-catch in case of uncovered runtimes... or you maybe want to fail the program because there's no way to calc hash then + crypto = require("crypto"); // maybe wrap with try-catch in case of uncovered runtimes... or you maybe want to fail the program because there's no way to calc hash then } // In the browser, use crypto.subtle.digest @@ -152,11 +152,11 @@ async function stringToUint32(str: string): Promise { 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; } -} \ No newline at end of file +} From ec8905c74b0a87e164c5a8960997c9c9544b98df Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 20 Aug 2024 16:14:20 +0800 Subject: [PATCH 05/12] add error message --- src/filter/TargetingFilter.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/filter/TargetingFilter.ts b/src/filter/TargetingFilter.ts index feed4e5..7f1fed4 100644 --- a/src/filter/TargetingFilter.ts +++ b/src/filter/TargetingFilter.ts @@ -142,7 +142,12 @@ async function stringToUint32(str: string): Promise { } // Fallback to native Node.js crypto module else { - crypto = require("crypto"); // maybe wrap with try-catch in case of uncovered runtimes... or you maybe want to fail the program because there's no way to calc hash then + try { + crypto = require('crypto'); + } catch (error) { + console.error('Failed to load the crypto module:', error.message); + throw error; + } } // In the browser, use crypto.subtle.digest @@ -159,4 +164,4 @@ async function stringToUint32(str: string): Promise { const uint32 = hash.readUInt32LE(0); return uint32; } -} +} From cbc8e4da6f09173a62d13ad4ea6a95e8146b274b Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 20 Aug 2024 16:15:56 +0800 Subject: [PATCH 06/12] fix lint --- src/filter/TargetingFilter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/filter/TargetingFilter.ts b/src/filter/TargetingFilter.ts index 7f1fed4..d88c741 100644 --- a/src/filter/TargetingFilter.ts +++ b/src/filter/TargetingFilter.ts @@ -143,9 +143,9 @@ async function stringToUint32(str: string): Promise { // Fallback to native Node.js crypto module else { try { - crypto = require('crypto'); + crypto = require("crypto"); } catch (error) { - console.error('Failed to load the crypto module:', error.message); + console.error("Failed to load the crypto module:", error.message); throw error; } } From 0e702322ebe7ca758af873d19905079264e53ffb Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 22 Aug 2024 15:07:20 +0800 Subject: [PATCH 07/12] add browser test --- .github/workflows/ci.yml | 3 +- .gitignore | 4 +- package-lock.json | 73 ++++++++++++++++++++++++-- package.json | 29 +++++----- playwright.config.ts | 15 ++++++ rollup.config.mjs | 24 +++++++-- test/browser/featureManagement.test.ts | 22 ++++++++ test/browser/index.html | 27 ++++++++++ test/browser/testcases.js | 19 +++++++ tsconfig.base.json | 3 -- tsconfig.json | 10 ---- tsconfig.test.json | 2 +- 12 files changed, 191 insertions(+), 40 deletions(-) create mode 100644 playwright.config.ts create mode 100644 test/browser/featureManagement.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 0d917bb..4382d1e 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 && npx playwright test" }, "repository": { "type": "git", @@ -42,7 +36,9 @@ "@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", "mocha": "^10.2.0", "rimraf": "^5.0.5", @@ -52,6 +48,5 @@ "typescript": "^5.3.3" }, "dependencies": { - "chai-as-promised": "^7.1.1" } } 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..c0aab5e 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -7,10 +7,23 @@ export default [ 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 +41,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/test/browser/featureManagement.test.ts b/test/browser/featureManagement.test.ts new file mode 100644 index 0000000..e6b137c --- /dev/null +++ b/test/browser/featureManagement.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..1b04ba6 --- /dev/null +++ b/test/browser/testcases.js @@ -0,0 +1,19 @@ +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": "Alpha", "description": "", "enabled": true } + ] + } + }; + const provider = new ConfigurationObjectFeatureFlagProvider(jsonObject); + const featureManager = new FeatureManager(provider); + return chai.expect(await featureManager.isEnabled("Alpha")).to.eq(true); + } + ); +}); 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 fad2c00c6aa891dfa5c68807afca4bf30c04256b Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 22 Aug 2024 15:27:35 +0800 Subject: [PATCH 08/12] add complex targeting testcase for browser --- ...tureManagement.test.ts => browser.test.ts} | 0 test/browser/testcases.js | 48 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) rename test/browser/{featureManagement.test.ts => browser.test.ts} (100%) diff --git a/test/browser/featureManagement.test.ts b/test/browser/browser.test.ts similarity index 100% rename from test/browser/featureManagement.test.ts rename to test/browser/browser.test.ts diff --git a/test/browser/testcases.js b/test/browser/testcases.js index 1b04ba6..ba865ed 100644 --- a/test/browser/testcases.js +++ b/test/browser/testcases.js @@ -7,13 +7,57 @@ describe("feature manager", () => { const jsonObject = { "feature_management": { "feature_flags": [ - { "id": "Alpha", "description": "", "enabled": true } + { + "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); - return chai.expect(await featureManager.isEnabled("Alpha")).to.eq(true); + 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); } ); }); From 07d88670302a537806b43be75eaf21725ecc4973 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 23 Aug 2024 14:10:58 +0800 Subject: [PATCH 09/12] install chromium only --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4382d1e..8c9c012 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "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 && npx playwright test" + "test-browser": "npx playwright install chromium && npx playwright test" }, "repository": { "type": "git", From a24fdf29f77f3945902a5dddfab29a08a59587b9 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Sun, 25 Aug 2024 11:58:49 +0800 Subject: [PATCH 10/12] handle esm when fallback to native node.js crypto --- src/filter/TargetingFilter.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/filter/TargetingFilter.ts b/src/filter/TargetingFilter.ts index c7a1128..506754b 100644 --- a/src/filter/TargetingFilter.ts +++ b/src/filter/TargetingFilter.ts @@ -143,7 +143,12 @@ async function stringToUint32(str: string): Promise { // Fallback to native Node.js crypto module else { try { - crypto = require("crypto"); + 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; From 710e067d7657aef8daf59f27b79e7441bbeb4537 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Sun, 25 Aug 2024 12:03:18 +0800 Subject: [PATCH 11/12] mark crypto as external --- rollup.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/rollup.config.mjs b/rollup.config.mjs index c0aab5e..42018b0 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -4,6 +4,7 @@ import dts from "rollup-plugin-dts"; export default [ { + external: ["crypto"], input: "src/index.ts", output: [ { From db91271e9ac68ccc7c750c2841fe75d2929f05bb Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Sun, 25 Aug 2024 17:08:35 +0800 Subject: [PATCH 12/12] fix lint --- src/filter/TargetingFilter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/filter/TargetingFilter.ts b/src/filter/TargetingFilter.ts index 506754b..406d846 100644 --- a/src/filter/TargetingFilter.ts +++ b/src/filter/TargetingFilter.ts @@ -143,11 +143,11 @@ async function stringToUint32(str: string): Promise { // Fallback to native Node.js crypto module else { try { - if (typeof module !== 'undefined' && module.exports) { - crypto = require('crypto'); + if (typeof module !== "undefined" && module.exports) { + crypto = require("crypto"); } else { - crypto = await import('crypto'); + crypto = await import("crypto"); } } catch (error) { console.error("Failed to load the crypto module:", error.message);