Skip to content

Commit

Permalink
upgrade feature flag functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
roman-adamchik-sw committed Sep 9, 2023
1 parent f2da6bf commit 78eff23
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 50 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ module.exports = {
'gap',
'role',
'as',
'borderRadius'
'borderRadius',
'feature'
],
}],
'max-len': [
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,23 @@ For enabling async reducers we use [DynamicModuleLoader](/src/shared/lib/compone
Working with feature flag is possible on the project. You should use the template below:

```
// For plain ts/js
toggleFeature({
name: "feature-name",
on: () => ..., // arrow function or component if feature is on
off: () => ... // arrow function or component if feature is off
on: () => ..., // arrow function if feature is on
off: () => ... // arrow function if feature is off
})
// For tsx
<ToggleFeatures
feature="feature-name"
on={<Component_if_feature_on/>}
off={<Component_if_feature_off/>}
/>
```

To remove feature from the code base you can use script from `./scripts/refactoring/removeFeature.ts`
Use the command `npx ts-node ./scripts/refactoring/removeFeature.ts feature-name on/off` to run the script
Use the command `npm run remove-feature feature-name on/off` to run the script

---

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"storybook": "start-storybook -p 6006 -c ./config/storybook",
"storybook:build": "build-storybook -c ./config/storybook",
"generate:slice": "node ./scripts/createSlice/index.js",
"postinstall": "node ./scripts/clearCache/index.js"
"postinstall": "node ./scripts/clearCache/index.js",
"remove-feature": "npx ts-node ./scripts/refactoring/removeFeature.ts"
},
"lint-staged": {
"\"**/*.{ts,tsx}\"": [
Expand Down
136 changes: 100 additions & 36 deletions scripts/refactoring/removeFeature.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Node, Project, SyntaxKind } from 'ts-morph';
import { JsxAttribute, Node, Project, SyntaxKind } from 'ts-morph';

const project = new Project({});

Expand All @@ -7,20 +7,31 @@ project.addSourceFilesAtPaths('src/**/*.tsx');

const files = project.getSourceFiles();

const isToggleFeatures = (node: Node) => {
const featureToApply = process.argv[2];
const featureState = process.argv[3]; // 'on' | 'off'

const TOGGLE_FUNC_NAME = 'toggleFeatures';
const TOGGLE_COMP_NAME = 'ToggleFeatures';

const isToggleFunc = (node: Node) => {
let isToggleFeatures = false;

node.forEachChild(child => {
if (child.asKind(SyntaxKind.Identifier) && child.getText() === 'toggleFeatures') {
if (child.asKind(SyntaxKind.Identifier) && child.getText() === TOGGLE_FUNC_NAME) {
isToggleFeatures = true;
};
});

return isToggleFeatures;
};

const featureToApply = process.argv[2];
const featureState = process.argv[3]; // 'on' | 'off'
const isToggleComp = (node: Node) => {
const identifier = node.getFirstDescendantByKind(SyntaxKind.Identifier);

if (!identifier) return;

return identifier.getText() === TOGGLE_COMP_NAME;
};

if (!featureToApply) {
throw new Error('You need to pass feature name to apply as a first argument');
Expand All @@ -34,39 +45,92 @@ if (featureState !== 'on' && featureState !== 'off') {
throw new Error('Second argument must be on or off only');
}

const removeFunctions = (node: Node) => {
const propsObject = node.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
if (!propsObject) return;

const onFunctionProperty = propsObject.getProperty('on')
?.getFirstDescendantByKind(SyntaxKind.ArrowFunction)
?.getBody()
.getText() || '';
const offFunctionProperty = propsObject.getProperty('off')
?.getFirstDescendantByKind(SyntaxKind.ArrowFunction)
?.getBody()
.getText() || '';
const featureName = propsObject
.getProperty('name')
?.getFirstDescendantByKind(SyntaxKind.StringLiteral)
?.getText()
.slice(1, -1);

if (featureToApply !== featureName) {
return;
}

// TODO consider refactor it to make it possible to work with arrow functions with multiline body.

if (featureState === 'on') {
node.replaceWithText(onFunctionProperty);
}

if (featureState === 'off') {
node.replaceWithText(offFunctionProperty);
}
};

const getAttributeByNodeName = (
jsxAttributes: JsxAttribute[],
name: string,
) => {
return jsxAttributes.find(node=> node.getName() === name);
};

const getReplaceComponent = (attribute: JsxAttribute) => {
const value = attribute
?.getFirstDescendantByKind(SyntaxKind.JsxExpression)
?.getExpression()
?.getText();

if (value?.startsWith('(')) {
return value.slice(1, -1);
}

return value;
};

const removeComponents = (node: Node) => {
const attributes = node.getDescendantsOfKind(SyntaxKind.JsxAttribute);

const onAttribute = getAttributeByNodeName(attributes, 'on');
const offAttribute = getAttributeByNodeName(attributes, 'off');
const featureNameAttribute = getAttributeByNodeName(attributes, 'feature');
const featureName = featureNameAttribute
?.getFirstDescendantByKind(SyntaxKind.StringLiteral)
?.getText()
?.slice(1,-1);

if (featureToApply !== featureName || !onAttribute || !offAttribute) return;

const onValue = getReplaceComponent(onAttribute);
const offValue = getReplaceComponent(offAttribute);

if (featureState === 'on' && onValue) {
node.replaceWithText(onValue);
}

if (featureState === 'off' && offValue) {
node.replaceWithText(offValue);
}
};

files.forEach((file) => {
file.forEachDescendant(node => {
if (node.asKind(SyntaxKind.CallExpression) && isToggleFeatures(node)) {
const propsObject = node.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
if (!propsObject) return;

const onFunctionProperty = propsObject.getProperty('on')
?.getFirstDescendantByKind(SyntaxKind.ArrowFunction)
?.getBody()
.getText() || '';
const offFunctionProperty = propsObject.getProperty('off')
?.getFirstDescendantByKind(SyntaxKind.ArrowFunction)
?.getBody()
.getText() || '';
const featureName = propsObject
.getProperty('name')
?.getFirstDescendantByKind(SyntaxKind.StringLiteral)
?.getText()
.slice(1, -1);

if (featureToApply !== featureName) {
return;
}

// TODO consider refactor it to make it possible to work with arrow functions with multiline body.

if (featureState === 'on') {
node.replaceWithText(onFunctionProperty);
}

if (featureState === 'off') {
node.replaceWithText(offFunctionProperty);
}
if (node.asKind(SyntaxKind.CallExpression) && isToggleFunc(node)) {
removeFunctions(node);
}

if (node.asKind(SyntaxKind.JsxSelfClosingElement) && isToggleComp(node)) {
removeComponents(node);
}
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ArticleDetailsPageHeader } from '../ArticleDetailsPageHeader/ArticleDet
import { VStack } from '@/shared/ui/Stack';
import { ArticleDetailsComments } from '../ArticleDetailsComments/ArticleDetailsComments';
import { ArticleRating } from '@/features/ArticleRating';
import { toggleFeatures } from '@/shared/lib/features';
import { ToggleFeatures } from '@/shared/lib/features';
import { Card } from '@/shared/ui/Card';
import { useTranslation } from 'react-i18next';

Expand All @@ -23,18 +23,16 @@ const ArticleDetailsPage: FC<ArticleDetailsPageProps> = memo((props) => {

if (!id) return null;

const articleRating = toggleFeatures({
name: 'isArticleRatingEnabled',
on: () => <ArticleRating articleId={id} />,
off: () => <Card>{t('Article rating coming soon...')}</Card>,
});

return (
<Page className={classNames('', {}, [className])}>
<VStack gap="16" align="stretch">
<ArticleDetailsPageHeader />
<ArticleDetails id={id} />
{articleRating}
<ToggleFeatures
feature="isArticleRatingEnabled"
on={<ArticleRating articleId={id} />}
off={<Card>{t('Article rating coming soon...')}</Card>}
/>
<ArticleRecommendationsList />
<ArticleDetailsComments id={id} />
</VStack>
Expand Down
21 changes: 21 additions & 0 deletions src/shared/lib/features/ToggleFeatures/ToggleFeatures.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { FeatureFlags } from '@/shared/types/featureFlags';
import { ReactElement } from 'react';
import { getFeatureFlags } from '../features';

interface ToggleFeaturesProps {
feature: keyof FeatureFlags;
on: ReactElement;
off: ReactElement;
}

export const ToggleFeatures = (props: ToggleFeaturesProps) => {
const { feature, on, off } = props;

if (getFeatureFlags(feature)) {
return on;
}

return off;
};

ToggleFeatures.displayName = 'ToggleFeatures';
3 changes: 2 additions & 1 deletion src/shared/lib/features/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ToggleFeatures } from './ToggleFeatures/ToggleFeatures';
import { getFeatureFlags, setFeatureFlags } from "./features";
import {toggleFeatures} from "./toggleFeatures";

export {setFeatureFlags, getFeatureFlags, toggleFeatures};
export {setFeatureFlags, getFeatureFlags, toggleFeatures, ToggleFeatures};

0 comments on commit 78eff23

Please sign in to comment.