Skip to content

Instantly share code, notes, and snippets.

@Ligalaiz
Created November 8, 2021 12:41
Show Gist options
  • Save Ligalaiz/5a1fb37c74518bd2293c811d3c1770aa to your computer and use it in GitHub Desktop.
Save Ligalaiz/5a1fb37c74518bd2293c811d3c1770aa to your computer and use it in GitHub Desktop.
instead of CRA
last 2 version
> 0.25%
not dead
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
[*.{js,ts}]
quote_type = single
[*.md]
trim_trailing_whitespace = false
[*.json]
insert_final_newline = false
.vscode
build/*.js
*.config.js
dist
src/assets/animations
coverage
.history
storybook-static
module.exports = {
parser: '@typescript-eslint/parser',
env: {
browser: true,
es2021: true,
jest: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jest/recommended',
'airbnb',
'prettier',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2021,
sourceType: 'module',
},
plugins: ['react', '@typescript-eslint', 'import', 'jest'],
rules: {
'import/no-unresolved': 'error',
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'@emotion/jsx-import': 0,
'no-restricted-syntax': [
'error',
{
selector: 'ExportDefaultDeclaration',
message: 'Restricted default export, prefer named exports!',
},
],
'no-console': [
'error',
{
allow: ['warn', 'error'],
},
],
'import/prefer-default-export': 'off',
'no-duplicate-imports': ['error', { includeExports: true }],
'no-use-before-define': 'off',
'no-param-reassign': 'off',
'@typescript-eslint/explicit-function-return-type': 0,
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-var-requires': 0,
'react/jsx-filename-extension': [1, { extensions: ['.tsx', '.ts'] }],
'react/jsx-props-no-spreading': 'off',
'react/prop-types': 0,
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'react/display-name': 0,
},
settings: {
react: {
createClass: 'createReactClass', // Regex for Component Factory to use,
// default to "createReactClass"
pragma: 'React', // Pragma to use, default to "React"
fragment: 'Fragment', // Fragment to use (may be a property of <pragma>),
// default to "Fragment"
version: 'detect', // React version. "detect" automatically picks the
// version you have installed.
// You can also use `16.0`, `16.3`, etc, if you want to override the
// detected value.
// default to latest and warns if missing
// It will default to "detect" in the future
flowVersion: '0.53', // Flow version
},
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
typescript: {
alwaysTryTypes: true, // always try to resolve types under
// `<root>@types` directory even it
// doesn't contain any source code, like `@types/unist`
// Choose from one of the "project" configs below or omit to use
// <root>/tsconfig.json by default
// use <root>/path/to/folder/tsconfig.json
project: './tsconfig.json',
},
},
propWrapperFunctions: [
// The names of any function used to wrap propTypes, e.g.
// `forbidExtraProps`. If this isn't set,
// any propTypes wrapped in a function will be skipped.
'forbidExtraProps',
{ property: 'freeze', object: 'Object' },
{ property: 'myFavoriteWrapper' },
],
linkComponents: [
// Components used as alternatives to <a> for linking, eg.
// <Link to={ url } />
'Hyperlink',
{ name: 'Link', linkAttribute: 'to' },
],
},
overrides: [
{
files: ['*.stories.tsx'],
rules: {
'no-restricted-syntax': ['off'],
},
},
],
};
*.js text eol=lf
*.*~
.vscode
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Build results
dist
.env
coverage
.husky
.history
storybook-static
.vscode
node_modules
dist
.history
storybook-static
module.exports = {
presets: [
'@babel/preset-env',
[
'@babel/preset-react',
{ runtime: 'automatic', importSource: '@emotion/react' },
],
'@babel/preset-typescript',
],
plugins: [
['@emotion/babel-plugin'],
['@babel/plugin-proposal-class-properties', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
'@babel/plugin-transform-runtime',
],
};
import '@testing-library/jest-dom/extend-expect';
module.exports = {
clearMocks: true,
coverageDirectory: 'coverage',
testEnvironment: 'jsdom',
testPathIgnorePatterns: ['<rootDir>/dist/'],
moduleFileExtensions: ['ts', 'js', 'tsx', 'jsx', 'json', 'node'],
collectCoverageFrom: [
'src/**/*.tsx',
'!**/node_modules/**',
'!src/utils/index.jsx',
'!src/store/rootReducer.jsx',
'!src/store/index.jsx',
],
resetMocks: false,
setupFiles: ['jest-localstorage-mock'],
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/tests/mocks/FileTransformer.js',
},
moduleNameMapper: {
'^@src(.*)$': '<rootDir>/src$1',
'^@components(.*)$': '<rootDir>/src/components$1',
'^@shared(.*)$': '<rootDir>/src/components/shared$1',
'^@utils(.*)$': '<rootDir>/src/utils$1',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/tests/mocks/fileMock.js',
'\\.(css|scss)$': '<rootDir>/tests/mocks/styleMock.js',
},
coverageThreshold: {
global: {
branches: 60,
functions: 60,
lines: 60,
statements: -12,
},
},
};
{
"name": "react",
"version": "1.0.0",
"description": "React",
"private": true,
"scripts": {
"dev": "webpack serve --config webpack.dev.js",
"build": "webpack --config webpack.prod.js",
"lint": "eslint ./src --cache && echo \"No linting errors\"",
"lint:fix": "eslint ./src --fix",
"clean": "rm -rf dist",
"loki:update": "npm run build-storybook && npx loki update --requireReference --reactUri file:./storybook-static",
"loki:approve": "npx loki approve",
"test": "jest",
"test:coverage": "jest --coverage",
"test:loki": "npm run build-storybook && npx loki --requireReference --reactUri file:./storybook-static",
"check": "npm run lint && npm test && npm run test:loki",
"prepare": "husky install",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Ligalaiz/react.git"
},
"keywords": [
"react",
"components"
],
"author": "Ligalaiz",
"license": "MIT",
"bugs": {
"url": "https://github.com/Ligalaiz/react/issues"
},
"homepage": "https://github.com/Ligalaiz/react#readme",
"devDependencies": {
"@babel/cli": "^7.15.7",
"@babel/core": "^7.14.8",
"@babel/eslint-parser": "^7.14.7",
"@babel/eslint-plugin": "^7.14.5",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-proposal-private-methods": "^7.14.5",
"@babel/plugin-proposal-private-property-in-object": "^7.14.5",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.15.8",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.14.8",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@emotion/babel-plugin": "^11.3.0",
"@emotion/eslint-plugin": "^11.5.0",
"@emotion/jest": "^11.5.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
"@react-mock/localstorage": "^0.1.2",
"@reduxjs/toolkit": "^1.6.1",
"@storybook/addon-docs": "*",
"@storybook/addon-essentials": "^6.4.0-beta.22",
"@storybook/addon-knobs": "^6.3.1",
"@storybook/addon-links": "^6.4.0-beta.22",
"@storybook/addon-postcss": "^2.0.0",
"@storybook/addon-storysource": "^6.4.0-beta.22",
"@storybook/builder-webpack5": "^6.4.0-beta.21",
"@storybook/manager-webpack5": "^6.4.0-beta.21",
"@storybook/node-logger": "^6.3.12",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/html-webpack-plugin": "^3.2.6",
"@types/jest": "^27.0.2",
"@types/lodash": "^4.14.176",
"@types/node": "^16.11.4",
"@types/react": "^17.0.31",
"@types/react-dom": "^17.0.10",
"@types/react-redux": "^7.1.20",
"@types/react-router-dom": "^5.3.1",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0",
"autoprefixer": "^9.8.6",
"axios": "^0.21.4",
"babel-jest": "^27.0.6",
"babel-loader": "^8.2.2",
"classnames": "^2.3.1",
"clean-webpack-plugin": "*",
"copy-webpack-plugin": "^9.0.1",
"css-loader": "^6.2.0",
"cssnano": "^5.0.7",
"dotenv-webpack": "^7.0.3",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-jest": "^25.2.3",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-webpack-plugin": "^3.0.1",
"html-loader": "^2.1.2",
"html-webpack-plugin": "^5.3.2",
"husky": "^7.0.4",
"image-minimizer-webpack-plugin": "^2.2.0",
"imagemin-gifsicle": "^7.0.0",
"imagemin-jpegtran": "^7.0.0",
"imagemin-optipng": "^8.0.0",
"imagemin-svgo": "^9.0.0",
"jest": "^27.0.6",
"jest-localstorage-mock": "^2.4.17",
"lint-staged": "^11.2.3",
"lodash": "^4.17.21",
"loki": "^0.28.1",
"mini-css-extract-plugin": "^2.4.3",
"postcss": "^8.3.6",
"postcss-import": "^14.0.2",
"postcss-loader": "^6.1.1",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.3.2",
"react-docgen-typescript-loader": "^3.7.2",
"react-refresh": "^0.9.0",
"react-test-renderer": "^17.0.2",
"react-transition-group": "^4.4.2",
"redux-devtools-extension": "^2.13.9",
"redux-mock-store": "^1.5.4",
"sass": "^1.36.0",
"sass-loader": "^12.1.0",
"storybook-css-modules-preset": "^1.1.1",
"sugarss": "^4.0.1",
"svgo": "^2.3.1",
"ts-loader": "^9.2.6",
"typescript": "^4.4.4",
"webpack": "^5.47.0",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"@emotion/babel-preset-css-prop": "^11.2.0",
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@storybook/addon-actions": "^6.4.0-beta.22",
"@storybook/react": "^6.4.0-beta.22",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.0",
"redux": "^4.1.1",
"redux-thunk": "^2.3.0",
"uuid": "^3.4.0"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint:fix && npm test"
}
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix"
]
},
"loki": {
"configurations": {
"chrome.laptop": {
"target": "chrome.app",
"width": 1366,
"height": 768,
"deviceScaleFactor": 1,
"mobile": false
},
"chrome.iphone7": {
"target": "chrome.app",
"preset": "iPhone 7"
}
}
},
"_id": "react@1.0.0"
}
module.exports = ({ file, options, env }) => ({
parser: file.extname === '.sss' ? 'sugarss' : false,
plugins: {
'postcss-import': {},
autoprefixer: {},
'postcss-preset-env': {},
cssnano: env === 'production' ? {} : false,
},
});
module.exports = {
semi: true,
tabWidth: 2,
useTabs: false,
printWidth: 80,
endOfLine: 'auto',
singleQuote: true,
trailingComma: 'all',
bracketSpacing: true,
arrowParens: 'always',
jsxBracketSameLine: false
};
{
"compileOnSave": true,
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
"lib": [
"ESNext",
"DOM"
] /* Specify library files to be included in the compilation. */,
"allowJs": true /* Allow javascript files to be compiled. */,
"checkJs": true /* Report errors in .js files. */,
"jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
"declaration": false /* Generates corresponding '.d.ts' file. */,
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true /* Generates corresponding '.map' file. */,
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist" /* Redirect output structure to the directory. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
"removeComments": true /* Do not emit comments to output. */,
// "noEmit": true /* Do not emit outputs. */,
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */,
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true /* Report errors on unused locals. */,
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
/* Module Resolution Options */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
"baseUrl": "./" /* Base directory to resolve non-absolute module names. */,
"paths": {
"@src/*": ["src/*"],
"@utils/*": ["src/utils/*"],
"@components/*": ["src/components/*"],
"@shared/*": ["src/components/shared/*"]
} /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */,
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": [
"node_modules/@types"
] /* List of folders to include type definitions from. */,
// "types": ["jest"], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
"resolveJsonModule": true,
},
"include": ["src/*", "typings/*", "src/interfaces/interfaces.ts"],
"exclude": ["node_modules"]
}
const path = require('path');
const webpack = require('webpack');
const Dotenv = require('dotenv-webpack');
const CopyPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
target: 'web',
entry: ['@babel/polyfill', path.resolve(__dirname, './src/index.tsx')],
output: {
path: path.resolve(__dirname, 'dist'),
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
module: {
rules: [
{
test: /\.html$/,
include: path.resolve(__dirname, 'src'),
use: 'html-loader',
},
{
test: /\.(?:ico|gif|png|jpe?g|svg)$/i,
type: 'asset',
generator: {
filename: 'assets/images/[name].[contenthash:10].[ext]',
},
},
{
test: /\.(woff(2)?|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'assets/fonts/[name].[contenthash:10].[ext]',
},
},
],
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
alias: {
'@src': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@shared': path.resolve(__dirname, 'src/components/shared'),
'@utils': path.resolve(__dirname, 'src/utils'),
},
},
plugins: [
new webpack.ProgressPlugin(),
new HtmlWebpackPlugin({
template: path.join('./src', 'index.html'),
filename: './index.html',
inject: true,
}),
new Dotenv({
systemvars: true,
}),
new CleanWebpackPlugin({
cleanStaleWebpackAssets: false,
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, 'src/favicon.ico'),
to: path.resolve(__dirname, 'dist'),
},
],
}),
],
};
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const path = require('path');
const { merge } = require('webpack-merge');
const common = require('./webpack.common');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map',
output: {
filename: '[name].[hash:10].js',
chunkFilename: '[name].[hash:10].js',
assetModuleFilename: 'assets/[name].[hash:10].[ext]',
publicPath: '/',
},
devServer: {
hot: true,
open: 'true',
port: 3000,
historyApiFallback: true,
contentBase: path.resolve(__dirname, './dist'),
},
plugins: [new BundleAnalyzerPlugin()],
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
},
},
{
test: /\.module\.(sa|sc|c)ss$/i,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
options: { sourceMap: true },
},
{
loader: 'postcss-loader',
options: { sourceMap: true },
},
{
loader: 'sass-loader',
options: { sourceMap: true },
},
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'styles/[name].[contenthash:10].css',
chunkFilename: '[name].[contenthash:10].css',
}),
new ReactRefreshWebpackPlugin(),
],
stats: {
errorDetails: true,
},
});
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { merge } = require('webpack-merge');
const ESLintPlugin = require('eslint-webpack-plugin');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const { extendDefaultPlugins } = require('svgo');
const common = require('./webpack.common');
module.exports = merge(common, {
mode: 'production',
target: 'browserslist',
output: {
filename: '[name].[chunkhash:10].js',
chunkFilename: '[name].[chunkhash:10].js',
assetModuleFilename: 'assets/[name].[chunkhash:10].[ext]',
},
performance: {
hints: false,
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: 'ts-loader',
},
{
test: /\.module\.(sa|sc|c)ss$/i,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
},
'postcss-loader',
'sass-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'styles/[name].[contenthash:10].css',
chunkFilename: '[name].[contenthash:10].css',
}),
new ESLintPlugin({
extensions: ['js', 'jsx', '.ts', '.tsx'],
fix: false,
failOnError: true,
}),
new ImageMinimizerPlugin({
minimizerOptions: {
plugins: [
['gifsicle', { interlaced: true }],
['jpegtran', { progressive: true }],
['optipng', { optimizationLevel: 5 }],
[
'svgo',
{
plugins: extendDefaultPlugins([
{
name: 'removeViewBox',
active: false,
},
{
name: 'addAttributesToSVGElement',
params: {
attributes: [{ xmlns: 'http://www.w3.org/2000/svg' }],
},
},
]),
},
],
],
},
}),
],
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment