1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

Prevent new JS files in shared folder (#17737)

* Prevent new JS files in shared folder

* migrate to typescript

* fix types

* cleanup
This commit is contained in:
Pedro Figueiredo 2023-04-24 15:44:42 +01:00 committed by GitHub
parent 3e520214c9
commit 632ae0b7c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 467 additions and 191 deletions

View File

@ -24,5 +24,5 @@ jobs:
run: |
# git fetch origin $HEAD_REF
# git fetch origin $BASE_REF
# git diff origin/$BASE_REF origin/$HEAD_REF -- . ':(exclude)development/fitness-functions/*' > diff
# git diff origin/$BASE_REF origin/$HEAD_REF -- . > diff
# npm run fitness-functions -- "ci" "./diff"

View File

@ -1,44 +0,0 @@
const {
EXCLUDE_E2E_TESTS_REGEX,
filterDiffAdditions,
filterDiffByFilePath,
hasNumberOfCodeBlocksIncreased,
} = require('./shared');
function checkMochaSyntax(diff) {
const ruleHeading = 'favor-jest-instead-of-mocha';
const codeBlocks = [
"import { strict as assert } from 'assert';",
'assert.deepEqual',
'assert.equal',
'assert.rejects',
'assert.strictEqual',
'sinon.',
];
console.log(`\nChecking ${ruleHeading}...`);
const diffByFilePath = filterDiffByFilePath(diff, EXCLUDE_E2E_TESTS_REGEX);
const diffAdditions = filterDiffAdditions(diffByFilePath);
const hashmap = hasNumberOfCodeBlocksIncreased(diffAdditions, codeBlocks);
Object.keys(hashmap).forEach((key) => {
if (hashmap[key]) {
console.error(`Number of occurences of "${key}" have increased.`);
}
});
if (Object.values(hashmap).includes(true)) {
console.error(
`...changes have not been accepted by the fitness function.\nFor more info, see: https://github.com/MetaMask/metamask-extension/blob/develop/docs/testing.md#${ruleHeading}`,
);
process.exit(1);
} else {
console.log(
`...number of occurences has not increased for any code block.`,
);
process.exit(0);
}
}
module.exports = { checkMochaSyntax };

View File

@ -0,0 +1,105 @@
import { EXCLUDE_E2E_TESTS_REGEX, SHARED_FOLDER_JS_REGEX } from './constants';
describe('Regular Expressions used in Fitness Functions', (): void => {
describe(`EXCLUDE_E2E_TESTS_REGEX "${EXCLUDE_E2E_TESTS_REGEX}"`, (): void => {
const PATHS_IT_SHOULD_MATCH = [
'file.js',
'path/file.js',
'much/longer/path/file.js',
'file.ts',
'path/file.ts',
'much/longer/path/file.ts',
'file.jsx',
'path/file.jsx',
'much/longer/path/file.jsx',
];
const PATHS_IT_SHOULD_NOT_MATCH = [
// any without JS, TS, JSX or TSX extension
'file',
'file.extension',
'path/file.extension',
'much/longer/path/file.extension',
// any in the test/e2e directory
'test/e2e/file',
'test/e2e/file.extension',
'test/e2e/path/file.extension',
'test/e2e/much/longer/path/file.extension',
'test/e2e/file.js',
'test/e2e/path/file.ts',
'test/e2e/much/longer/path/file.jsx',
'test/e2e/much/longer/path/file.tsx',
// any in the development/fitness-functions directory
'development/fitness-functions/file',
'development/fitness-functions/file.extension',
'development/fitness-functions/path/file.extension',
'development/fitness-functions/much/longer/path/file.extension',
'development/fitness-functions/file.js',
'development/fitness-functions/path/file.ts',
'development/fitness-functions/much/longer/path/file.jsx',
'development/fitness-functions/much/longer/path/file.tsx',
];
describe('included paths', (): void => {
PATHS_IT_SHOULD_MATCH.forEach((path: string): void => {
it(`should match "${path}"`, (): void => {
const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path);
expect(result).toStrictEqual(true);
});
});
});
describe('excluded paths', (): void => {
PATHS_IT_SHOULD_NOT_MATCH.forEach((path: string): void => {
it(`should not match "${path}"`, (): void => {
const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path);
expect(result).toStrictEqual(false);
});
});
});
});
describe(`SHARED_FOLDER_JS_REGEX "${SHARED_FOLDER_JS_REGEX}"`, (): void => {
const PATHS_IT_SHOULD_MATCH = [
'shared/file.js',
'shared/path/file.js',
'shared/much/longer/path/file.js',
'shared/file.jsx',
'shared/path/file.jsx',
'shared/much/longer/path/file.jsx',
];
const PATHS_IT_SHOULD_NOT_MATCH = [
// any without JS or JSX extension
'file',
'file.extension',
'path/file.extension',
'much/longer/path/file.extension',
'file.ts',
'path/file.ts',
'much/longer/path/file.tsx',
];
describe('included paths', (): void => {
PATHS_IT_SHOULD_MATCH.forEach((path: string): void => {
it(`should match "${path}"`, (): void => {
const result = new RegExp(SHARED_FOLDER_JS_REGEX, 'u').test(path);
expect(result).toStrictEqual(true);
});
});
});
describe('excluded paths', (): void => {
PATHS_IT_SHOULD_NOT_MATCH.forEach((path: string): void => {
it(`should not match "${path}"`, (): void => {
const result = new RegExp(SHARED_FOLDER_JS_REGEX, 'u').test(path);
expect(result).toStrictEqual(false);
});
});
});
});
});

View File

@ -0,0 +1,15 @@
// include JS, TS, JSX, TSX files only excluding files in the e2e tests and
// fitness functions directories
const EXCLUDE_E2E_TESTS_REGEX =
'^(?!test/e2e)(?!development/fitness).*.(js|ts|jsx|tsx)$';
// include JS and JSX files in the shared directory only
const SHARED_FOLDER_JS_REGEX = '^(shared).*.(js|jsx)$';
enum AUTOMATION_TYPE {
CI = 'ci',
PRE_COMMIT_HOOK = 'pre-commit-hook',
PRE_PUSH_HOOK = 'pre-push-hook',
}
export { EXCLUDE_E2E_TESTS_REGEX, SHARED_FOLDER_JS_REGEX, AUTOMATION_TYPE };

View File

@ -0,0 +1,54 @@
import { execSync } from 'child_process';
import fs from 'fs';
import { AUTOMATION_TYPE } from './constants';
function getDiffByAutomationType(
automationType: AUTOMATION_TYPE,
): string | undefined {
if (!Object.values(AUTOMATION_TYPE).includes(automationType)) {
console.error('Invalid automation type.');
process.exit(1);
}
let diff;
if (automationType === AUTOMATION_TYPE.CI) {
const optionalArguments = process.argv.slice(3);
if (optionalArguments.length !== 1) {
console.error('Invalid number of arguments.');
process.exit(1);
}
diff = getCIDiff(optionalArguments[0]);
} else if (automationType === AUTOMATION_TYPE.PRE_COMMIT_HOOK) {
diff = getPreCommitHookDiff();
} else if (automationType === AUTOMATION_TYPE.PRE_PUSH_HOOK) {
diff = getPrePushHookDiff();
}
return diff;
}
function getCIDiff(path: string): string {
return fs.readFileSync(path, {
encoding: 'utf8',
flag: 'r',
});
}
function getPreCommitHookDiff(): string {
return execSync(`git diff --cached HEAD`).toString().trim();
}
function getPrePushHookDiff(): string {
const currentBranch = execSync(`git rev-parse --abbrev-ref HEAD`)
.toString()
.trim();
return execSync(
`git diff ${currentBranch} origin/${currentBranch} -- . ':(exclude)development/fitness-functions/'`,
)
.toString()
.trim();
}
export { getDiffByAutomationType };

View File

@ -1,46 +1,48 @@
const {
EXCLUDE_E2E_TESTS_REGEX,
filterDiffAdditions,
import {
filterDiffLineAdditions,
hasNumberOfCodeBlocksIncreased,
filterDiffByFilePath,
} = require('./shared');
filterDiffFileCreations,
} from './shared';
import { generateCreateFileDiff, generateModifyFilesDiff } from './test-data';
const generateCreateFileDiff = (filePath, content) => `
diff --git a/${filePath} b/${filePath}
new file mode 100644
index 000000000..30d74d258
--- /dev/null
+++ b/${filePath}
@@ -0,0 +1 @@
+${content}
`;
const generateModifyFilesDiff = (filePath, addition, removal) => `
diff --git a/${filePath} b/${filePath}
index 57d5de75c..808d8ba37 100644
--- a/${filePath}
+++ b/${filePath}
@@ -1,3 +1,8 @@
+${addition}
@@ -34,33 +39,4 @@
-${removal}
`;
describe('filterDiffAdditions()', () => {
it('should return code additions in the diff', () => {
describe('filterDiffLineAdditions()', (): void => {
it('should return code additions in the diff', (): void => {
const testFilePath = 'new-file.js';
const testAddition = 'foo';
const testFileDiff = generateCreateFileDiff(testFilePath, testAddition);
const actualResult = filterDiffAdditions(testFileDiff);
const actualResult = filterDiffLineAdditions(testFileDiff);
const expectedResult = `+${testAddition}`;
expect(actualResult).toStrictEqual(expectedResult);
});
});
describe('hasNumberOfCodeBlocksIncreased()', () => {
it('should show which code blocks have increased', () => {
describe('filterDiffFileCreations()', (): void => {
it('should return code additions in the diff', (): void => {
const testFileDiff = [
generateModifyFilesDiff('new-file.ts', 'foo', 'bar'),
generateCreateFileDiff('old-file.js', 'ping'),
generateModifyFilesDiff('old-file.jsx', 'yin', 'yang'),
].join('');
const actualResult = filterDiffFileCreations(testFileDiff);
expect(actualResult).toMatchInlineSnapshot(`
"diff --git a/old-file.js b/old-file.js
new file mode 100644
index 000000000..30d74d258
--- /dev/null
+++ b/old-file.js
@@ -0,0 +1 @@
+ping"
`);
});
});
describe('hasNumberOfCodeBlocksIncreased()', (): void => {
it('should show which code blocks have increased', (): void => {
const testDiffFragment = `
+foo
+bar
@ -57,14 +59,14 @@ describe('hasNumberOfCodeBlocksIncreased()', () => {
});
});
describe('filterDiffByFilePath()', () => {
describe('filterDiffByFilePath()', (): void => {
const testFileDiff = [
generateModifyFilesDiff('new-file.ts', 'foo', 'bar'),
generateModifyFilesDiff('old-file.js', 'ping', 'pong'),
generateModifyFilesDiff('old-file.jsx', 'yin', 'yang'),
].join('');
it('should return the right diff for a generic matcher', () => {
it('should return the right diff for a generic matcher', (): void => {
const actualResult = filterDiffByFilePath(
testFileDiff,
'.*/.*.(js|ts)$|.*.(js|ts)$',
@ -90,7 +92,7 @@ describe('filterDiffByFilePath()', () => {
`);
});
it('should return the right diff for a specific file in any dir matcher', () => {
it('should return the right diff for a specific file in any dir matcher', (): void => {
const actualResult = filterDiffByFilePath(testFileDiff, '.*old-file.js$');
expect(actualResult).toMatchInlineSnapshot(`
@ -105,7 +107,7 @@ describe('filterDiffByFilePath()', () => {
`);
});
it('should return the right diff for a multiple file extension (OR) matcher', () => {
it('should return the right diff for a multiple file extension (OR) matcher', (): void => {
const actualResult = filterDiffByFilePath(
testFileDiff,
'^(./)*old-file.(js|ts|jsx)$',
@ -131,7 +133,7 @@ describe('filterDiffByFilePath()', () => {
`);
});
it('should return the right diff for a file name negation matcher', () => {
it('should return the right diff for a file name negation matcher', (): void => {
const actualResult = filterDiffByFilePath(
testFileDiff,
'^(?!.*old-file.js$).*.[a-zA-Z]+$',
@ -157,51 +159,3 @@ describe('filterDiffByFilePath()', () => {
`);
});
});
describe(`EXCLUDE_E2E_TESTS_REGEX "${EXCLUDE_E2E_TESTS_REGEX}"`, () => {
const PATHS_IT_SHOULD_MATCH = [
'file.js',
'path/file.js',
'much/longer/path/file.js',
'file.ts',
'path/file.ts',
'much/longer/path/file.ts',
'file.jsx',
'path/file.jsx',
'much/longer/path/file.jsx',
];
const PATHS_IT_SHOULD_NOT_MATCH = [
'test/e2e/file',
'test/e2e/file.extension',
'test/e2e/path/file.extension',
'test/e2e/much/longer/path/file.extension',
'test/e2e/file.js',
'test/e2e/path/file.ts',
'test/e2e/much/longer/path/file.jsx',
'file',
'file.extension',
'path/file.extension',
'much/longer/path/file.extension',
];
describe('included paths', () => {
PATHS_IT_SHOULD_MATCH.forEach((path) => {
it(`should match "${path}"`, () => {
const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path);
expect(result).toStrictEqual(true);
});
});
});
describe('excluded paths', () => {
PATHS_IT_SHOULD_NOT_MATCH.forEach((path) => {
it(`should not match "${path}"`, () => {
const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path);
expect(result).toStrictEqual(false);
});
});
});
});

View File

@ -1,6 +1,4 @@
const EXCLUDE_E2E_TESTS_REGEX = '^(?!test/e2e/).*.(js|ts|jsx)$';
function filterDiffByFilePath(diff, regex) {
function filterDiffByFilePath(diff: string, regex: string): string {
// split by `diff --git` and remove the first element which is empty
const diffBlocks = diff.split(`diff --git`).slice(1);
@ -34,7 +32,7 @@ function filterDiffByFilePath(diff, regex) {
return filteredDiff;
}
function filterDiffAdditions(diff) {
function filterDiffLineAdditions(diff: string): string {
const diffLines = diff.split('\n');
const diffAdditionLines = diffLines.filter((line) => {
@ -46,10 +44,36 @@ function filterDiffAdditions(diff) {
return diffAdditionLines.join('/n');
}
function hasNumberOfCodeBlocksIncreased(diffFragment, codeBlocks) {
function filterDiffFileCreations(diff: string): string {
// split by `diff --git` and remove the first element which is empty
const diffBlocks = diff.split(`diff --git`).slice(1);
const filteredDiff = diffBlocks
.map((block) => block.trim())
.filter((block) => {
const isFileCreationLine =
block
// get the second line of the block which has the file mode
.split('\n')[1]
.trim()
.substring(0, 13) === 'new file mode';
return isFileCreationLine;
})
// prepend `git --diff` to each block
.map((block) => `diff --git ${block}`)
.join('\n');
return filteredDiff;
}
function hasNumberOfCodeBlocksIncreased(
diffFragment: string,
codeBlocks: string[],
): { [codeBlock: string]: boolean } {
const diffLines = diffFragment.split('\n');
const codeBlockFound = {};
const codeBlockFound: { [codeBlock: string]: boolean } = {};
for (const codeBlock of codeBlocks) {
codeBlockFound[codeBlock] = false;
@ -65,9 +89,9 @@ function hasNumberOfCodeBlocksIncreased(diffFragment, codeBlocks) {
return codeBlockFound;
}
module.exports = {
EXCLUDE_E2E_TESTS_REGEX,
export {
filterDiffByFilePath,
filterDiffAdditions,
filterDiffLineAdditions,
filterDiffFileCreations,
hasNumberOfCodeBlocksIncreased,
};

View File

@ -0,0 +1,40 @@
const generateCreateFileDiff = (
filePath = 'file.txt',
content = 'Lorem ipsum',
): string => `
diff --git a/${filePath} b/${filePath}
new file mode 100644
index 000000000..30d74d258
--- /dev/null
+++ b/${filePath}
@@ -0,0 +1 @@
+${content}
`;
const generateModifyFilesDiff = (
filePath = 'file.txt',
addition = 'Lorem ipsum',
removal = '',
): string => {
const additionBlock = addition
? `
@@ -1,3 +1,8 @@
+${addition}`.trim()
: '';
const removalBlock = removal
? `
@@ -34,33 +39,4 @@
-${removal}`.trim()
: '';
return `
diff --git a/${filePath} b/${filePath}
index 57d5de75c..808d8ba37 100644
--- a/${filePath}
+++ b/${filePath}
${additionBlock}
${removalBlock}`;
};
export { generateCreateFileDiff, generateModifyFilesDiff };

View File

@ -1,53 +0,0 @@
const fs = require('fs');
const { execSync } = require('child_process');
const { checkMochaSyntax } = require('./check-mocha-syntax');
const AUTOMATION_TYPE = Object.freeze({
CI: 'ci',
PRE_COMMIT_HOOK: 'pre-commit-hook',
PRE_PUSH_HOOK: 'pre-push-hook',
});
const automationType = process.argv[2];
if (automationType === AUTOMATION_TYPE.CI) {
const optionalArguments = process.argv.slice(3);
if (optionalArguments.length !== 1) {
console.error('Invalid number of arguments.');
process.exit(1);
}
const diff = fs.readFileSync(optionalArguments[0], {
encoding: 'utf8',
flag: 'r',
});
checkMochaSyntax(diff);
} else if (automationType === AUTOMATION_TYPE.PRE_COMMIT_HOOK) {
const diff = getPreCommitHookDiff();
checkMochaSyntax(diff);
} else if (automationType === AUTOMATION_TYPE.PRE_PUSH_HOOK) {
const diff = getPrePushHookDiff();
checkMochaSyntax(diff);
} else {
console.error('Invalid automation type.');
process.exit(1);
}
function getPreCommitHookDiff() {
return execSync(`git diff --cached HEAD`).toString().trim();
}
function getPrePushHookDiff() {
const currentBranch = execSync(`git rev-parse --abbrev-ref HEAD`)
.toString()
.trim();
return execSync(
`git diff ${currentBranch} origin/${currentBranch} -- . ':(exclude)development/fitness-functions/'`,
)
.toString()
.trim();
}

View File

@ -0,0 +1,11 @@
import { AUTOMATION_TYPE } from './common/constants';
import { getDiffByAutomationType } from './common/get-diff';
import { IRule, RULES, runFitnessFunctionRule } from './rules';
const automationType: AUTOMATION_TYPE = process.argv[2] as AUTOMATION_TYPE;
const diff = getDiffByAutomationType(automationType);
if (typeof diff === 'string') {
RULES.forEach((rule: IRule): void => runFitnessFunctionRule(rule, diff));
}

View File

@ -0,0 +1,42 @@
import { preventSinonAssertSyntax } from './sinon-assert-syntax';
import { preventJavaScriptFileAdditions } from './javascript-additions';
const RULES: IRule[] = [
{
name: "Don't use `sinon` or `assert` in unit tests",
fn: preventSinonAssertSyntax,
docURL:
'https://github.com/MetaMask/metamask-extension/blob/develop/docs/testing.md#favor-jest-instead-of-mocha',
},
{
name: "Don't add JS or JSX files to the `shared` directory",
fn: preventJavaScriptFileAdditions,
},
];
interface IRule {
name: string;
fn: (diff: string) => boolean;
docURL?: string;
}
function runFitnessFunctionRule(rule: IRule, diff: string): void {
const { name, fn, docURL } = rule;
console.log(`Checking rule "${name}"...`);
const hasRulePassed: boolean = fn(diff) as boolean;
if (hasRulePassed === true) {
console.log(`...OK`);
} else {
console.log(`...FAILED. Changes not accepted by the fitness function.`);
if (docURL) {
console.log(`For more info: ${docURL}.`);
}
process.exit(1);
}
}
export { RULES, runFitnessFunctionRule };
export type { IRule };

View File

@ -0,0 +1,51 @@
import {
generateModifyFilesDiff,
generateCreateFileDiff,
} from '../common/test-data';
import { preventJavaScriptFileAdditions } from './javascript-additions';
describe('preventJavaScriptFileAdditions()', (): void => {
it('should pass when receiving an empty diff', (): void => {
const testDiff = '';
const hasRulePassed = preventJavaScriptFileAdditions(testDiff);
expect(hasRulePassed).toBe(true);
});
it('should pass when receiving a diff with a new TS file on the shared folder', (): void => {
const testDiff = [
generateModifyFilesDiff('new-file.ts', 'foo', 'bar'),
generateModifyFilesDiff('old-file.js', undefined, 'pong'),
generateCreateFileDiff('shared/test.ts', 'yada yada yada yada'),
].join('');
const hasRulePassed = preventJavaScriptFileAdditions(testDiff);
expect(hasRulePassed).toBe(true);
});
it('should not pass when receiving a diff with a new JS file on the shared folder', (): void => {
const testDiff = [
generateModifyFilesDiff('new-file.ts', 'foo', 'bar'),
generateModifyFilesDiff('old-file.js', undefined, 'pong'),
generateCreateFileDiff('shared/test.js', 'yada yada yada yada'),
].join('');
const hasRulePassed = preventJavaScriptFileAdditions(testDiff);
expect(hasRulePassed).toBe(false);
});
it('should not pass when receiving a diff with a new JSX file on the shared folder', (): void => {
const testDiff = [
generateModifyFilesDiff('new-file.ts', 'foo', 'bar'),
generateModifyFilesDiff('old-file.js', undefined, 'pong'),
generateCreateFileDiff('shared/test.jsx', 'yada yada yada yada'),
].join('');
const hasRulePassed = preventJavaScriptFileAdditions(testDiff);
expect(hasRulePassed).toBe(false);
});
});

View File

@ -0,0 +1,18 @@
import { SHARED_FOLDER_JS_REGEX } from '../common/constants';
import {
filterDiffByFilePath,
filterDiffFileCreations,
} from '../common/shared';
function preventJavaScriptFileAdditions(diff: string): boolean {
const sharedFolderDiff = filterDiffByFilePath(diff, SHARED_FOLDER_JS_REGEX);
const sharedFolderCreationDiff = filterDiffFileCreations(sharedFolderDiff);
const hasCreatedAtLeastOneJSFileInShared = sharedFolderCreationDiff !== '';
if (hasCreatedAtLeastOneJSFileInShared) {
return false;
}
return true;
}
export { preventJavaScriptFileAdditions };

View File

@ -0,0 +1,29 @@
import { generateModifyFilesDiff } from '../common/test-data';
import { preventSinonAssertSyntax } from './sinon-assert-syntax';
describe('preventSinonAssertSyntax()', (): void => {
it('should pass when receiving an empty diff', (): void => {
const testDiff = '';
const hasRulePassed = preventSinonAssertSyntax(testDiff);
expect(hasRulePassed).toBe(true);
});
it('should not pass when receiving a diff with one of the blocked expressions', (): void => {
const infringingExpression = 'assert.equal';
const testDiff = [
generateModifyFilesDiff('new-file.ts', 'foo', 'bar'),
generateModifyFilesDiff('old-file.js', undefined, 'pong'),
generateModifyFilesDiff(
'test.js',
`yada yada ${infringingExpression} yada yada`,
undefined,
),
].join('');
const hasRulePassed = preventSinonAssertSyntax(testDiff);
expect(hasRulePassed).toBe(false);
});
});

View File

@ -0,0 +1,30 @@
import { EXCLUDE_E2E_TESTS_REGEX } from '../common/constants';
import {
filterDiffLineAdditions,
filterDiffByFilePath,
hasNumberOfCodeBlocksIncreased,
} from '../common/shared';
const codeBlocks = [
"import { strict as assert } from 'assert';",
'assert.deepEqual',
'assert.equal',
'assert.rejects',
'assert.strictEqual',
'sinon.',
];
function preventSinonAssertSyntax(diff: string): boolean {
const diffByFilePath = filterDiffByFilePath(diff, EXCLUDE_E2E_TESTS_REGEX);
const diffAdditions = filterDiffLineAdditions(diffByFilePath);
const hashmap = hasNumberOfCodeBlocksIncreased(diffAdditions, codeBlocks);
const haveOccurencesOfAtLeastOneCodeBlockIncreased =
Object.values(hashmap).includes(true);
if (haveOccurencesOfAtLeastOneCodeBlockIncreased) {
return false;
}
return true;
}
export { preventSinonAssertSyntax };

View File

@ -91,7 +91,7 @@
"test-storybook": "test-storybook -c .storybook",
"test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn storybook:build && npx http-server storybook-build --port 6006 \" \"wait-on tcp:6006 && yarn test-storybook --maxWorkers=2\"",
"githooks:install": "husky install",
"fitness-functions": "node development/fitness-functions/index.js",
"fitness-functions": "ts-node development/fitness-functions/index.ts",
"generate-beta-commit": "node ./development/generate-beta-commit.js"
},
"resolutions": {