Skip to content

Commit

Permalink
Add feature flags to ParcelOptions (#9551)
Browse files Browse the repository at this point in the history
Implements a Feature Flag system for Parcel, available to both core code and plugins. 

This allows for early merges of not-ready-for-general-use features that can be enabled selectively by Parcel users.
  • Loading branch information
marcins authored Mar 1, 2024
1 parent 61a89cf commit 56e5f40
Show file tree
Hide file tree
Showing 12 changed files with 157 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/core/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@parcel/cache": "2.12.0",
"@parcel/diagnostic": "2.12.0",
"@parcel/events": "2.12.0",
"@parcel/feature-flags": "2.12.0",
"@parcel/fs": "2.12.0",
"@parcel/graph": "3.2.0",
"@parcel/logger": "2.12.0",
Expand Down
5 changes: 5 additions & 0 deletions packages/core/core/src/public/PluginOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
import type {FileSystem} from '@parcel/fs';
import type {PackageManager} from '@parcel/package-manager';
import type {ParcelOptions} from '../types';
import {type FeatureFlags} from '@parcel/feature-flags';

let parcelOptionsToPluginOptions: WeakMap<ParcelOptions, PluginOptions> =
new WeakMap();
Expand Down Expand Up @@ -87,4 +88,8 @@ export default class PluginOptions implements IPluginOptions {
get detailedReport(): ?DetailedReportOptions {
return this.#options.detailedReport;
}

get featureFlags(): FeatureFlags {
return this.#options.featureFlags;
}
}
3 changes: 3 additions & 0 deletions packages/core/core/src/resolveOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import loadDotEnv from './loadDotEnv';
import {toProjectPath} from './projectPath';
import {getResolveFrom} from './requests/ParcelConfigRequest';

import {DEFAULT_FEATURE_FLAGS} from '@parcel/feature-flags';

// Default cache directory name
const DEFAULT_CACHE_DIRNAME = '.parcel-cache';
const LOCK_FILE_NAMES = ['yarn.lock', 'package-lock.json', 'pnpm-lock.yaml'];
Expand Down Expand Up @@ -218,6 +220,7 @@ export default async function resolveOptions(
outputFormat: initialOptions?.defaultTargetOptions?.outputFormat,
isLibrary: initialOptions?.defaultTargetOptions?.isLibrary,
},
featureFlags: {...DEFAULT_FEATURE_FLAGS, ...initialOptions?.featureFlags},
};
}

Expand Down
3 changes: 3 additions & 0 deletions packages/core/core/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {Cache} from '@parcel/cache';
import type {PackageManager} from '@parcel/package-manager';
import type {ProjectPath} from './projectPath';
import type {EventType} from '@parcel/watcher';
import type {FeatureFlags} from '@parcel/feature-flags';

export type ParcelPluginNode = {|
packageName: PackageName,
Expand Down Expand Up @@ -316,6 +317,8 @@ export type ParcelOptions = {|
+outputFormat?: OutputFormat,
+isLibrary?: boolean,
|},

+featureFlags: FeatureFlags,
|};

export type AssetNode = {|
Expand Down
3 changes: 3 additions & 0 deletions packages/core/core/test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export const DEFAULT_OPTIONS: ParcelOptions = {
distDir: undefined,
sourceMaps: false,
},
featureFlags: {
exampleFeature: false,
},
};

export const DEFAULT_ENV: Environment = createEnvironment({
Expand Down
23 changes: 23 additions & 0 deletions packages/core/feature-flags/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@parcel/feature-flags",
"version": "2.12.0",
"description": "Blazing fast, zero configuration web application bundler",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"repository": {
"type": "git",
"url": "https://github.com/parcel-bundler/parcel.git"
},
"main": "lib/index.js",
"source": "src/index.js",
"types": "index.d.ts",
"engines": {
"node": ">= 16.0.0"
}
}
10 changes: 10 additions & 0 deletions packages/core/feature-flags/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// @flow strict

export type FeatureFlags = {|
// This feature flag mostly exists to test the feature flag system, and doesn't have any build/runtime effect
+exampleFeature: boolean,
|};

export const DEFAULT_FEATURE_FLAGS: FeatureFlags = {
exampleFeature: false,
};
101 changes: 101 additions & 0 deletions packages/core/integration-tests/test/feature-flags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import assert from 'assert';
import path from 'node:path';
import {rimraf} from 'rimraf';
import {bundle, run, overlayFS, fsFixture} from '@parcel/test-utils';

describe('feature flags', () => {
let dir = path.join(__dirname, 'feature-flags-fixture');
beforeEach(async () => {
await rimraf(dir);
await overlayFS.mkdirp(dir);
await fsFixture(overlayFS, dir)`
yarn.lock:
// required for .parcelrc to work
package.json:
{
"name": "feature-flags-fixture",
"version": "1.0.0"
}
index.js:
module.exports = "MARKER";
.parcelrc:
{
extends: "@parcel/config-default",
transformers: {
'*.js': ['./transformer.js', '...']
},
}
transformer.js:
const {Transformer} = require('@parcel/plugin');
module.exports = new Transformer({
async transform({asset, options}) {
const code = await asset.getCode();
if (code.includes('MARKER') && options.featureFlags.exampleFeature) {
asset.setCode(code.replace('MARKER', 'REPLACED'));
}
return [asset];
}
});
`;
});

it('flag should be available in plugins and set from options', async () => {
await overlayFS.mkdirp(dir);

const b = await bundle(path.join(dir, 'index.js'), {
inputFS: overlayFS,
featureFlags: {exampleFeature: true},
});
const output = await run(b);

assert(
output.includes('REPLACED'),
`Expected ${output} to contain 'REPLACED'`,
);
});

it('flag defaults should be available in plugins if not set from options', async () => {
await overlayFS.mkdirp(dir);

const b = await bundle(path.join(dir, 'index.js'), {
inputFS: overlayFS,
});
const output = await run(b);

assert(
!output.includes('REPLACED'),
`Expected ${output} to NOT contain 'REPLACED'`,
);
});

it('cache should invalidate on flag switch', async () => {
await overlayFS.mkdirp(dir);

const b = await bundle(path.join(dir, 'index.js'), {
inputFS: overlayFS,
shouldDisableCache: false,
featureFlags: {exampleFeature: true},
});
const output = await run(b);

assert(
output.includes('REPLACED'),
`Expected ${output} to contain 'REPLACED'`,
);

const b2 = await bundle(path.join(dir, 'index.js'), {
inputFS: overlayFS,
shouldDisableCache: false,
featureFlags: {exampleFeature: false},
});
const output2 = await run(b2);
assert(
!output2.includes('REPLACED'),
`Expected ${output} to NOT contain 'REPLACED'`,
);
});
});
4 changes: 4 additions & 0 deletions packages/core/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {Cache} from '@parcel/cache';
import type {AST as _AST, ConfigResult as _ConfigResult} from './unsafe';
import type {TraceMeasurement} from '@parcel/profiler';
import type {EventType} from '@parcel/watcher';
import type {FeatureFlags} from '@parcel/feature-flags';

/** Plugin-specific AST, <code>any</code> */
export type AST = _AST;
Expand Down Expand Up @@ -333,6 +334,8 @@ export type InitialParcelOptions = {|
resolveFrom: FilePath,
|}>,

+featureFlags?: FeatureFlags,

// throwErrors
// global?
|};
Expand All @@ -359,6 +362,7 @@ export interface PluginOptions {
+packageManager: PackageManager;
+instanceId: string;
+detailedReport: ?DetailedReportOptions;
+featureFlags: FeatureFlags;
}

export type ServerOptions = {|
Expand Down
1 change: 1 addition & 0 deletions packages/core/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dependencies": {
"@parcel/cache": "2.12.0",
"@parcel/diagnostic": "2.12.0",
"@parcel/feature-flags": "2.12.0",
"@parcel/fs": "2.12.0",
"@parcel/package-manager": "2.12.0",
"@parcel/source-map": "^2.1.1",
Expand Down
1 change: 1 addition & 0 deletions packages/reporters/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"term-size": "^2.2.1"
},
"devDependencies": {
"@parcel/feature-flags": "2.12.0",
"filesize": "^6.1.0",
"nullthrows": "^1.1.1",
"ora": "^5.2.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/reporters/cli/test/CLIReporter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {NodePackageManager} from '@parcel/package-manager';
import stripAnsi from 'strip-ansi';
import * as bundleReport from '../src/bundleReport';
import * as render from '../src/render';
import {DEFAULT_FEATURE_FLAGS} from '@parcel/feature-flags';

const EMPTY_OPTIONS = {
cacheDir: '.parcel-cache',
Expand All @@ -36,6 +37,7 @@ const EMPTY_OPTIONS = {
detailedReport: {
assetsPerBundle: 10,
},
featureFlags: DEFAULT_FEATURE_FLAGS,
};

describe('CLIReporter', () => {
Expand Down

0 comments on commit 56e5f40

Please sign in to comment.