From a24a30d2492b329a076841ed512184dc5dc16bc9 Mon Sep 17 00:00:00 2001 From: Marcin Szczepanski Date: Thu, 4 Apr 2024 14:58:08 +1100 Subject: [PATCH] Implements a `getFeatureFlag` to avoid option drilling (#9618) * Implement a simple feature-flag client to avoid option drilling * Ensure feature flag client is initialised in workers * Rename featureFlags -> featureFlagValues * Avoid depending on ephemeral flag in test --- packages/core/core/src/Parcel.js | 3 ++ packages/core/core/src/worker.js | 4 ++ packages/core/feature-flags/src/index.js | 10 +++++ .../feature-flags/test/feature-flags.test.js | 21 ++++++++++ .../integration-tests/test/feature-flags.js | 39 ++++++++++++++++++- 5 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 packages/core/feature-flags/test/feature-flags.test.js diff --git a/packages/core/core/src/Parcel.js b/packages/core/core/src/Parcel.js index 4b5a6709cd7..df14f4fc8ce 100644 --- a/packages/core/core/src/Parcel.js +++ b/packages/core/core/src/Parcel.js @@ -53,6 +53,7 @@ import { fromProjectPathRelative, } from './projectPath'; import {tracer} from '@parcel/profiler'; +import {setFeatureFlags} from '@parcel/feature-flags'; registerCoreWithSerializer(); @@ -108,6 +109,8 @@ export default class Parcel { let {config} = await loadParcelConfig(resolvedOptions); this.#config = new ParcelConfig(config, resolvedOptions); + setFeatureFlags(resolvedOptions.featureFlags); + if (this.#initialOptions.workerFarm) { if (this.#initialOptions.workerFarm.ending) { throw new Error('Supplied WorkerFarm is ending'); diff --git a/packages/core/core/src/worker.js b/packages/core/core/src/worker.js index 863b27137e1..23750220321 100644 --- a/packages/core/core/src/worker.js +++ b/packages/core/core/src/worker.js @@ -26,6 +26,7 @@ import {clearBuildCaches} from './buildCache'; import {init as initSourcemaps} from '@parcel/source-map'; import {init as initRust} from '@parcel/rust'; import WorkerFarm from '@parcel/workers'; +import {setFeatureFlags} from '@parcel/feature-flags'; import '@parcel/cache'; // register with serializer import '@parcel/package-manager'; @@ -78,6 +79,9 @@ async function loadConfig(cachePath, options) { ); config = new ParcelConfig(processedConfig, options); parcelConfigCache.set(cachePath, config); + + setFeatureFlags(options.featureFlags); + return config; } diff --git a/packages/core/feature-flags/src/index.js b/packages/core/feature-flags/src/index.js index 5d5f388b7cd..c66b372d0dd 100644 --- a/packages/core/feature-flags/src/index.js +++ b/packages/core/feature-flags/src/index.js @@ -9,3 +9,13 @@ export const DEFAULT_FEATURE_FLAGS: FeatureFlags = { exampleFeature: false, configKeyInvalidation: false, }; + +let featureFlagValues: FeatureFlags = {...DEFAULT_FEATURE_FLAGS}; + +export function setFeatureFlags(flags: FeatureFlags) { + featureFlagValues = flags; +} + +export function getFeatureFlag(flagName: $Keys): boolean { + return featureFlagValues[flagName]; +} diff --git a/packages/core/feature-flags/test/feature-flags.test.js b/packages/core/feature-flags/test/feature-flags.test.js new file mode 100644 index 00000000000..536929e0d13 --- /dev/null +++ b/packages/core/feature-flags/test/feature-flags.test.js @@ -0,0 +1,21 @@ +// @flow strict +import assert from 'assert'; +import {getFeatureFlag, DEFAULT_FEATURE_FLAGS, setFeatureFlags} from '../src'; + +describe('feature-flag test', () => { + beforeEach(() => { + setFeatureFlags(DEFAULT_FEATURE_FLAGS); + }); + + it('has defaults', () => { + assert.equal( + getFeatureFlag('exampleFeature'), + DEFAULT_FEATURE_FLAGS.exampleFeature, + ); + }); + + it('can override', () => { + setFeatureFlags({...DEFAULT_FEATURE_FLAGS, exampleFeature: true}); + assert.equal(getFeatureFlag('exampleFeature'), true); + }); +}); diff --git a/packages/core/integration-tests/test/feature-flags.js b/packages/core/integration-tests/test/feature-flags.js index dc9cdc51e3a..e9ff1bdf0e8 100644 --- a/packages/core/integration-tests/test/feature-flags.js +++ b/packages/core/integration-tests/test/feature-flags.js @@ -28,7 +28,15 @@ describe('feature flags', () => { '*.js': ['./transformer.js', '...'] }, } - + + .parcelrc-2: + { + extends: "@parcel/config-default", + transformers: { + '*.js': ['./transformer-client.js', '...'] + }, + } + transformer.js: const {Transformer} = require('@parcel/plugin'); module.exports = new Transformer({ @@ -40,6 +48,19 @@ describe('feature flags', () => { return [asset]; } }); + + transformer-client.js: + const {Transformer} = require('@parcel/plugin'); + const {getFeatureFlag} = require('@parcel/feature-flags'); + module.exports = new Transformer({ + async transform({asset, options}) { + const code = await asset.getCode(); + if (code.includes('MARKER') && getFeatureFlag('exampleFeature')) { + asset.setCode(code.replace('MARKER', 'REPLACED')); + } + return [asset]; + } + }); `; }); @@ -98,4 +119,20 @@ describe('feature flags', () => { `Expected ${output} to NOT contain 'REPLACED'`, ); }); + + it('flag should be available in plugins via client', async () => { + await overlayFS.mkdirp(dir); + + const b = await bundle(path.join(dir, 'index.js'), { + inputFS: overlayFS, + featureFlags: {exampleFeature: true}, + config: path.join(dir, '.parcelrc-2'), + }); + const output = await run(b); + + assert( + output.includes('REPLACED'), + `Expected ${output} to contain 'REPLACED'`, + ); + }); });