|
1 | | -import { Plugin, Compiler } from 'webpack' |
| 1 | +import { Expression, CallExpression, Identifier, SpreadElement } from 'estree' |
| 2 | +import { Compilation, Compiler, javascript } from 'webpack' |
2 | 3 |
|
3 | | -const featureFlagsFunctionName = 'isFeatureEnabled' |
| 4 | +type PluginOptions = { |
| 5 | + /** |
| 6 | + * Function name for match |
| 7 | + * |
| 8 | + * @default 'isFeatureEnabled' |
| 9 | + */ |
| 10 | + isFeatureEnabledFnName: string |
| 11 | + /** |
| 12 | + * Object with flags |
| 13 | + */ |
| 14 | + flags: Record<string, any> |
| 15 | +} |
4 | 16 |
|
5 | | -function isFeatureFlagFunction(expression: any) { |
6 | | - if (expression.callee.property) { |
7 | | - return expression.callee.property.name === featureFlagsFunctionName |
| 17 | +class FeatureFlagsWebpackPlugin { |
| 18 | + constructor(public options: PluginOptions) { |
| 19 | + Object.assign(this.options, { isFeatureEnabledFnName: 'isFeatureEnabled' }) |
8 | 20 | } |
9 | | - return expression.callee.name === featureFlagsFunctionName |
10 | | -} |
11 | 21 |
|
12 | | -function getFeatureProperties(expression: any) { |
13 | | - if (!expression || !expression.arguments.length) return [] |
14 | | - return expression.arguments[0].properties |
15 | | -} |
| 22 | + apply(compiler: Compiler) { |
| 23 | + const { isFeatureEnabledFnName, flags } = this.options |
16 | 24 |
|
17 | | -function getFeatureFlag(properties: any) { |
18 | | - return properties.reduce((result: any, property: any) => { |
19 | | - const key = property.key.name |
20 | | - const value = property.value.value |
21 | | - result[key] = value |
22 | | - return result |
23 | | - }, {}) |
24 | | -} |
| 25 | + function onCallExpression(expression: Expression, parser: javascript.JavascriptParser) { |
| 26 | + if (isFeatureFlagFunction(expression, isFeatureEnabledFnName)) { |
| 27 | + const argument = expression.arguments[0] |
| 28 | + if (isIdentifier(argument)) { |
| 29 | + const isEnabled = flags[argument.name] !== undefined |
| 30 | + const result = isEnabled ? parser.evaluate(true) : parser.evaluate(false) |
| 31 | + if (result !== undefined) { |
| 32 | + result.setRange(expression.range) |
| 33 | + } |
| 34 | + return result |
| 35 | + } |
| 36 | + } |
| 37 | + return undefined |
| 38 | + } |
25 | 39 |
|
26 | | -function isEnabledFlag(flag: any, options: any) { |
27 | | - return options.some( |
28 | | - (option: any) => option.value === flag.value && option.component === flag.component, |
29 | | - ) |
30 | | -} |
| 40 | + function onParseJavascript(parser: javascript.JavascriptParser) { |
| 41 | + parser.hooks.evaluate |
| 42 | + .for('CallExpression') |
| 43 | + .tap('FeatureFlagsWebpackPlugin', (expression) => onCallExpression(expression, parser)) |
| 44 | + } |
31 | 45 |
|
32 | | -class FeatureFlagsWebpackPlugin extends Plugin { |
33 | | - options: any[] |
| 46 | + function onThisCompilation(_compilation: Compilation, compilationParams: any) { |
| 47 | + compilationParams.normalModuleFactory.hooks.parser |
| 48 | + .for('javascript/auto') |
| 49 | + .tap('FeatureFlagsWebpackPlugin', onParseJavascript) |
| 50 | + } |
34 | 51 |
|
35 | | - constructor(options: any[]) { |
36 | | - super() |
37 | | - this.options = options || [] |
| 52 | + compiler.hooks.thisCompilation.tap('FeatureFlagsWebpackPlugin', onThisCompilation) |
38 | 53 | } |
| 54 | +} |
39 | 55 |
|
40 | | - apply(compiler: Compiler) { |
41 | | - const options = this.options |
42 | | - |
43 | | - compiler.hooks.thisCompilation.tap( |
44 | | - 'FeatureFlagsWebpackPlugin', |
45 | | - (_, { normalModuleFactory }) => { |
46 | | - normalModuleFactory.hooks.parser |
47 | | - .for('javascript/auto') |
48 | | - .tap('FeatureFlagsWebpackPlugin', (parser) => { |
49 | | - // @ts-expect-error |
50 | | - const getTrue = () => parser.evaluate(true) |
51 | | - // @ts-expect-error |
52 | | - const getFalse = () => parser.evaluate(false) |
53 | | - |
54 | | - parser.hooks.evaluate |
55 | | - .for('CallExpression') |
56 | | - .tap('FeatureFlagsWebpackPlugin', (expression) => { |
57 | | - if (!isFeatureFlagFunction(expression)) return |
| 56 | +function isFeatureFlagFunction( |
| 57 | + expression: Expression, |
| 58 | + isFeatureEnabledFnName: string, |
| 59 | +): expression is CallExpression { |
| 60 | + // prettier-ignore |
| 61 | + return ( |
| 62 | + expression.type === 'CallExpression' |
| 63 | + && expression.arguments.length > 0 |
| 64 | + && expression.callee.type === 'Identifier' |
| 65 | + && expression.callee.name === isFeatureEnabledFnName |
| 66 | + ) |
| 67 | +} |
58 | 68 |
|
59 | | - const properties = getFeatureProperties(expression) |
60 | | - const flag = getFeatureFlag(properties) |
61 | | - const isEnabled = isEnabledFlag(flag, options) |
62 | | - const result = isEnabled ? getTrue() : getFalse() |
63 | | - result.setRange(expression.range) |
64 | | - return result |
65 | | - }) |
66 | | - }) |
67 | | - }, |
68 | | - ) |
69 | | - } |
| 69 | +function isIdentifier(expression: Expression | SpreadElement): expression is Identifier { |
| 70 | + return expression.type === 'Identifier' |
70 | 71 | } |
71 | 72 |
|
72 | 73 | module.exports = FeatureFlagsWebpackPlugin |
0 commit comments