Skip to content

Commit

Permalink
feat: add toggle features script for components
Browse files Browse the repository at this point in the history
  • Loading branch information
TomatoVan committed Apr 9, 2024
1 parent 4441bbd commit 259bd01
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 37 deletions.
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/clear-cache.js"
"postinstall": "node ./scripts/clear-cache.js",
"remove-feature": "ts-node ./scripts/remove-feature.ts"
},
"lint-staged": {
"**/*.{ts,tsx}": [
Expand Down
120 changes: 91 additions & 29 deletions scripts/remove-feature.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Node, Project, SyntaxKind } from 'ts-morph';
import { JsxAttribute, Node, Project, SyntaxKind } from 'ts-morph';

const removedFeatureName = process.argv[2]; // example isArticleEnabled
const featureState = process.argv[3]; // example off\on

const toggleFunctionName = 'toggleFeatures';
const toggleComponentName = 'ToggleFeatures';

if (!removedFeatureName) {
throw new Error('Enter a feature name');
}
Expand All @@ -28,7 +31,7 @@ function isToggleFunction(node: Node) {
node.forEachChild((child) => {
if (
child.isKind(SyntaxKind.Identifier) &&
child.getText() === 'toggleFeatures'
child.getText() === toggleFunctionName
) {
isToggleFeatures = true;
}
Expand All @@ -37,40 +40,99 @@ function isToggleFunction(node: Node) {
return isToggleFeatures;
}

files.forEach((sourceFile) => {
sourceFile.forEachDescendant((node) => {
if (node.isKind(SyntaxKind.CallExpression) && isToggleFunction(node)) {
const objectOptions = node.getFirstDescendantByKind(
SyntaxKind.ObjectLiteralExpression,
);
function isToggleComponent(node: Node) {
const identifier = node.getFirstDescendantByKind(SyntaxKind.Identifier);

return identifier?.getText() === toggleComponentName;
}

const replaceToggleFunction = (node: Node) => {
const objectOptions = node.getFirstDescendantByKind(
SyntaxKind.ObjectLiteralExpression,
);

if (!objectOptions) return;

const offFunctionProperty = objectOptions.getProperty('off');
const onFunctionProperty = objectOptions.getProperty('on');

const featureNameProperty = objectOptions.getProperty('name');

const onFunction = onFunctionProperty?.getFirstDescendantByKind(
SyntaxKind.ArrowFunction,
);
const offFunction = offFunctionProperty?.getFirstDescendantByKind(
SyntaxKind.ArrowFunction,
);
const featureName = featureNameProperty
?.getFirstDescendantByKind(SyntaxKind.StringLiteral)
?.getText()
.slice(1, -1);

if (featureName !== removedFeatureName) return;

if (featureState === 'on') {
node.replaceWithText(onFunction?.getBody().getText() ?? '');
}

if (!objectOptions) return;
if (featureState === 'off') {
node.replaceWithText(offFunction?.getBody().getText() ?? '');
}
};

const offFunctionProperty = objectOptions.getProperty('off');
const onFunctionProperty = objectOptions.getProperty('on');
const getAttributeNodeByName = (jsxAttributes: JsxAttribute[], name: string) =>
jsxAttributes.find((node) => node.getName() === name);

const featureNameProperty = objectOptions.getProperty('name');
const getReplacedComponent = (attribute?: JsxAttribute) => {
const value = attribute
?.getFirstDescendantByKind(SyntaxKind.JsxExpression)
?.getExpression()
?.getText();

const onFunction = onFunctionProperty?.getFirstDescendantByKind(
SyntaxKind.ArrowFunction,
);
const offFunction = offFunctionProperty?.getFirstDescendantByKind(
SyntaxKind.ArrowFunction,
);
const featureName = featureNameProperty
?.getFirstDescendantByKind(SyntaxKind.StringLiteral)
?.getText()
.slice(1, -1);
if (value?.startsWith('(')) {
return value.slice(1, -1);
}

if (featureName !== removedFeatureName) return;
return value;
};

if (featureState === 'on') {
node.replaceWithText(onFunction?.getBody().getText() ?? '');
}
const replaceComponent = (node: Node) => {
const attributes = node.getDescendantsOfKind(SyntaxKind.JsxAttribute);

if (featureState === 'off') {
node.replaceWithText(offFunction?.getBody().getText() ?? '');
}
const onAttribute = getAttributeNodeByName(attributes, 'on');
const offAttribute = getAttributeNodeByName(attributes, 'off');

const featureNameAttribute = getAttributeNodeByName(attributes, 'feature');
const featureName = featureNameAttribute
?.getFirstDescendantByKind(SyntaxKind.StringLiteral)
?.getText()
?.slice(1, -1);

if (featureName !== removedFeatureName) return;

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

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

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

files.forEach((sourceFile) => {
sourceFile.forEachDescendant((node) => {
if (node.isKind(SyntaxKind.CallExpression) && isToggleFunction(node)) {
replaceToggleFunction(node);
}

if (
node.isKind(SyntaxKind.JsxSelfClosingElement) &&
isToggleComponent(node)
) {
replaceComponent(node);
}
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { articleDetailsPageReducer } from '../../model/slices';
import { ArticleDetails } from '../../../../entities/Article';
import { ArticleRating } from '@/features/articleRating';
import { Page } from '@/widgets/Page';
import { toggleFeatures } from '@/shared/lib/features';
import { ToggleFeatures } from '@/shared/lib/features';
import { Card } from '@/shared/ui/Card';

const reducers: ReducersList = {
Expand All @@ -29,19 +29,24 @@ const ArticlesDetailsPage = (props: any) => {
return null;
}

const articleRatingCard = toggleFeatures({
name: 'isArticleRatingEnabled',
on: () => <ArticleRating articleId={id} />,
off: () => <Card>{t('Article ratings coming soon!')}</Card>,
});
// usage example for func
// const articleRatingCard = toggleFeatures({
// name: 'isArticleRatingEnabled',
// on: () => todo1(),
// off: () => todo2(),
// });

return (
<DynamicModuleLoader reducers={reducers}>
<Page>
<VStack gap="16" max>
<ArticlesDetailsPageHeader />
<ArticleDetails id={id} />
{articleRatingCard}
<ToggleFeatures
feature="isArticleRatingEnabled"
on={<ArticleRating articleId={id} />}
off={<Card>{t('Article ratings coming soon!')}</Card>}
/>
<ArticleRecommendationsList />
<ArticleDetailsComments id={id} />
</VStack>
Expand Down
19 changes: 19 additions & 0 deletions src/shared/lib/features/ToggleFeatures/ToggleFeatures.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ReactElement } from 'react';
import { FeatureFlags } from '@/shared/types/featureFlags';
import { getFeatureFlag } from '../setGetFeatures';

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

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

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

return off;
};
1 change: 1 addition & 0 deletions src/shared/lib/features/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { setFeatureFlags, getFeatureFlag } from './setGetFeatures';
export { toggleFeatures } from './toggleFeatures';
export { ToggleFeatures } from './toggleFeatures/ToggleFeatures';

0 comments on commit 259bd01

Please sign in to comment.