Upgrade project's build infrastructure: Node 22, Webpack 5, TypeScript 5, Storybook 8

This commit is contained in:
ErickSkrauch committed 2026-07-13 04:08:57 +02:00
1 parent 5f12afc64d
commit a12db70dea
100 files changed
+7512 -10487

No files matched your search

+1
View File
@@ -1,5 +1,6 @@
[production]
> 0.25%
not iOS < 14
not dead
[development]
-1
View File
@@ -9,7 +9,6 @@ module.exports = {
'eslint:recommended',
'plugin:jsdoc/recommended',
'plugin:@typescript-eslint/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
],
+3 -5
View File
@@ -1,11 +1,9 @@
variables:
NODE_IMAGE: node:14-alpine
NODE_E2E_IMAGE: circleci/node:14-buster-browsers
NODE_IMAGE: node:22-alpine
DEPLOY_IMAGE: docksal/ssh-agent:1.3
# To cache both npm modules and Cypress binary we use environment variables
# to point at the folders we can list as paths in "cache" job settings
# To cache npm modules we use an environment variable
# to point at the folder we list as a path in "cache" job settings
YARN_CACHE_FOLDER: "$CI_PROJECT_DIR/cache/yarn"
CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/cache/cypress"
GIT_DEPTH: 10
-4
View File
@@ -1,4 +0,0 @@
import 'storybook-addon-intl/register';
import '@storybook/addon-actions/register';
import '@storybook/addon-links/register';
import '@storybook/addon-viewport/register';
-13
View File
@@ -1,13 +0,0 @@
import { configure, addDecorator } from '@storybook/react';
import storyDecorator from './storyDecorator';
const req = require.context('../packages/app', true, /\.story\.[tj]sx?$/);
function loadStories() {
req.keys().forEach((filename) => req(filename));
}
addDecorator(storyDecorator);
configure(loadStories, module);
+10 -27
View File
@@ -1,36 +1,19 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { Channel } from '@storybook/channels';
import { setIntlConfig } from 'storybook-addon-intl';
import { EVENT_SET_LOCALE_ID, EVENT_GET_LOCALE_ID } from 'storybook-addon-intl/dist/shared';
import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE } from 'app/services/i18n';
import { setLocale } from 'app/components/i18n/actions';
import { useReduxDispatch } from 'app/functions';
setIntlConfig({
locales: SUPPORTED_LANGUAGES,
defaultLocale: DEFAULT_LANGUAGE,
});
interface Props {
locale: string;
children: React.ReactNode;
}
const IntlDecorator: React.ComponentType<{
channel: Channel;
}> = ({ channel, children }) => {
const dispatch = useDispatch();
const IntlDecorator: React.ComponentType<Props> = ({ locale, children }) => {
const dispatch = useReduxDispatch();
React.useEffect(() => {
const onLocaleChange = (locale: string) => {
dispatch(setLocale(locale));
};
// Listen for change of locale
channel.on(EVENT_SET_LOCALE_ID, onLocaleChange);
// Request the current locale
channel.emit(EVENT_GET_LOCALE_ID);
return () => {
channel.removeListener(EVENT_SET_LOCALE_ID, onLocaleChange);
};
}, [channel]);
dispatch(setLocale(locale));
}, [dispatch, locale]);
return children as React.ReactElement;
};
+54
View File
@@ -0,0 +1,54 @@
import type { StorybookConfig } from '@storybook/react-webpack5';
import { ContextReplacementPlugin } from 'webpack';
import rootConfig from '../webpack.config';
const config: StorybookConfig = {
stories: ['../packages/app/**/*.story.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-actions',
'@storybook/addon-links',
'@storybook/addon-toolbars',
'@storybook/addon-viewport',
],
framework: {
name: '@storybook/react-webpack5',
options: {},
},
webpackFinal: (config) => ({
...config,
resolve: {
...config.resolve,
...rootConfig.resolve,
alias: {
// Storybook's own presets contribute aliases we must keep — e.g. @storybook/react-dom-shim's
// preset aliases itself to the React 16/17-compatible shim when it detects React < 18
// (see @storybook/react-dom-shim/dist/preset.js). Overwriting the whole `resolve` (and thus
// dropping this alias) makes webpack fall back to the React 18 shim, which imports the
// nonexistent (in React 17) `react-dom/client` module.
...config.resolve?.alias,
...rootConfig.resolve.alias,
},
},
module: {
...config.module,
rules: [
...rootConfig.module.rules,
// Our webpack config's rules above fully replace Storybook's own module.rules, which drops
// the fix Storybook normally applies for strict ESM resolution of .mjs files (see
// @storybook/preset-react-webpack's framework-preset-cra.js). Without it, webpack requires
// an explicit extension for every bare import inside real ESM files such as
// @storybook/react/dist/entry-preview.mjs, and fails to resolve its `react-dom/test-utils`
// import (react-dom doesn't declare an "exports" map to compensate). Restore it explicitly.
{ test: /\.m?js$/, resolve: { fullySpecified: false } },
],
},
resolveLoader: rootConfig.resolveLoader,
plugins: [
...(config.plugins || []),
...rootConfig.plugins.filter((plugin) => plugin instanceof ContextReplacementPlugin),
],
}),
};
export default config;
+34
View File
@@ -0,0 +1,34 @@
import type { Preview } from '@storybook/react';
import { DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES } from 'app/services/i18n';
import storyDecorator from './storyDecorator';
function getLanguageTitle(locale: string): string {
try {
return new Intl.DisplayNames(['en'], { type: 'language' }).of(locale) ?? locale;
} catch {
return locale;
}
}
const preview: Preview = {
decorators: [storyDecorator],
globalTypes: {
locale: {
name: 'Locale',
description: 'Application locale (react-intl)',
defaultValue: DEFAULT_LANGUAGE,
toolbar: {
icon: 'globe',
dynamicTitle: true,
items: SUPPORTED_LANGUAGES.map((locale) => ({
value: locale,
title: getLanguageTitle(locale),
})),
},
},
},
};
export default preview;
+8 -9
View File
@@ -1,5 +1,6 @@
import React from 'react';
import addons, { DecoratorFunction } from '@storybook/addons';
import type { Decorator } from '@storybook/react';
import { ContextProvider } from 'app/shell';
import { browserHistory } from 'app/services/history';
import storeFactory from 'app/storeFactory';
@@ -9,12 +10,10 @@ import { IntlDecorator } from './decorators';
const store = storeFactory();
export default ((story) => {
const channel = addons.getChannel();
const storyDecorator: Decorator = (story, context) => (
<ContextProvider store={store} history={browserHistory}>
<IntlDecorator locale={context.globals.locale}>{story()}</IntlDecorator>
</ContextProvider>
);
return (
<ContextProvider store={store} history={browserHistory}>
<IntlDecorator channel={channel}>{story()}</IntlDecorator>
</ContextProvider>
);
}) as DecoratorFunction<React.ReactElement>;
export default storyDecorator;
-23
View File
@@ -1,23 +0,0 @@
// @ts-nocheck
const { ContextReplacementPlugin } = require('webpack');
const rootConfig = require('../webpack.config');
module.exports = async ({ config }) => ({
...config,
resolve: rootConfig.resolve,
module: {
...config.module,
// our rules should satisfy all storybook needs,
// so replace all storybook defaults with our rules
rules: rootConfig.module.rules,
},
resolveLoader: rootConfig.resolveLoader,
plugins: [
...config.plugins,
...rootConfig.plugins.filter((plugin) => plugin instanceof ContextReplacementPlugin),
],
});
-43
View File
@@ -1,43 +0,0 @@
# test directories
__tests__
test
tests
powered-test
# asset directories
docs
doc
website
images
assets
# examples
example
examples
# code coverage directories
coverage
.nyc_output
# build scripts
Makefile
Gulpfile.js
Gruntfile.js
# configs
.tern-project
.gitattributes
.editorconfig
.*ignore
.eslintrc
.jshintrc
.flowconfig
.documentup.json
.yarn-metadata.json
.*.yml
*.yml
!.svgo.yml
# misc
*.gz
*.md
+2 -4
View File
@@ -3,6 +3,7 @@
module.exports = function (api) {
const env = api.env();
const isProduction = api.env((envName) => envName.includes('production'));
const { version: corejsVersion } = require('core-js/package.json');
api.cache.using(() => env);
@@ -16,7 +17,7 @@ module.exports = function (api) {
ignoreBrowserslistConfig: false,
modules: false,
useBuiltIns: 'usage', // or "entry"
corejs: 3,
corejs: corejsVersion,
include: ['proposal-class-properties'],
},
],
@@ -52,9 +53,6 @@ module.exports = function (api) {
],
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-proposal-function-bind',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-transform-runtime',
[
'react-intl-auto',
-21
View File
@@ -1,21 +0,0 @@
/* eslint-env node */
/* eslint-disable @typescript-eslint/no-var-requires */
require('dotenv').config();
const { env } = process;
module.exports = {
version: env.VERSION || env.NODE_ENV,
apiHost: env.API_HOST || 'https://account.dev.ely.by',
ga: env.GA_ID && { id: env.GA_ID },
sentryDSN: env.SENTRY_DSN,
crowdin: {
apiKey: env.CROWDIN_API_KEY,
projectId: 350687,
filePath: 'accounts/site.json',
sourceLang: 'en',
basePath: `${__dirname}/packages/app/i18n`,
minTranslated: 80, // Minimal ready percent before translation can be published
},
};
+53 -79
View File
@@ -16,14 +16,14 @@
"license": "Apache-2.0",
"repository": "https://github.com/elyby/accounts-frontend",
"engines": {
"node": ">=10.0.0"
"node": ">=22.0.0"
},
"workspaces": [
"packages/*",
"tests-e2e"
],
"scripts": {
"start": "yarn run clean && yarn run build:dll && webpack-dev-server --colors",
"start": "yarn run clean && webpack serve",
"clean": "rm -rf ./build && mkdir ./build",
"e2e": "yarn --cwd ./tests-e2e test",
"test": "jest",
@@ -40,13 +40,11 @@
"i18n:pull": "babel-node --extensions \".ts\" ./packages/scripts/i18n-crowdin.ts pull",
"build": "yarn run clean && yarn run build:webpack",
"build:install": "yarn install",
"build:webpack": "NODE_ENV=production webpack --colors -p --bail",
"build:webpack": "NODE_ENV=production webpack --bail",
"build:quiet": "yarn run clean && yarn run build:webpack --quiet",
"build:dll": "babel-node --extensions '.ts,.d.ts' ./packages/scripts/build-dll.ts",
"build:serve": "http-server --proxy https://account.dev.ely.by ./build",
"sb": "APP_ENV=storybook start-storybook -p 9009 --ci",
"sb:build": "APP_ENV=storybook build-storybook",
"postinstall": "node -e \"try{require('./postinstall.js')}catch(e){}\""
"sb": "APP_ENV=storybook storybook dev -p 9009",
"sb:build": "APP_ENV=storybook storybook build"
},
"lint-staged": {
"*.{json,scss,css,md}": [
@@ -60,6 +58,7 @@
"roots": [
"<rootDir>/packages/app"
],
"testEnvironment": "jsdom",
"setupFilesAfterEnv": [
"<rootDir>/jest/setupAfterEnv.js"
],
@@ -87,92 +86,67 @@
"react": "^17",
"react-dom": "^17",
"react-hot-loader": "^4",
"react-intl": "^4.5.7",
"regenerator-runtime": "^0.13.3"
"react-intl": "^4.5.7"
},
"devDependencies": {
"@babel/cli": "^7.10.5",
"@babel/core": "^7.10.5",
"@babel/node": "^7.10.5",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-proposal-do-expressions": "^7.10.4",
"@babel/plugin-proposal-export-default-from": "^7.10.4",
"@babel/plugin-proposal-export-namespace-from": "^7.10.4",
"@babel/plugin-proposal-function-bind": "^7.10.5",
"@babel/plugin-proposal-function-sent": "^7.10.4",
"@babel/plugin-proposal-json-strings": "^7.10.4",
"@babel/plugin-proposal-logical-assignment-operators": "^7.10.4",
"@babel/plugin-proposal-numeric-separator": "^7.10.4",
"@babel/plugin-proposal-optional-chaining": "^7.10.4",
"@babel/plugin-proposal-pipeline-operator": "^7.10.5",
"@babel/plugin-proposal-throw-expressions": "^7.10.4",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.10.5",
"@babel/preset-env": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@babel/preset-typescript": "^7.10.4",
"@babel/runtime-corejs3": "^7.10.5",
"@storybook/addon-actions": "^5.3.18",
"@storybook/addon-links": "^5.3.18",
"@storybook/addon-viewport": "^5.3.18",
"@storybook/addons": "^5.3.18",
"@storybook/react": "^5.3.18",
"@types/jest": "^25.2.3",
"@babel/cli": "^7",
"@babel/core": "^7",
"@babel/node": "^7",
"@babel/plugin-transform-runtime": "^7",
"@babel/preset-env": "^7",
"@babel/preset-react": "^7",
"@babel/preset-typescript": "^7",
"@babel/runtime-corejs3": "^7",
"@storybook/addon-actions": "^8.2.9",
"@storybook/addon-links": "^8.2.9",
"@storybook/addon-toolbars": "^8.2.9",
"@storybook/addon-viewport": "^8.2.9",
"@storybook/react": "^8.2.9",
"@storybook/react-webpack5": "^8.2.9",
"@types/jest": "^29.5.12",
"@types/sinon": "^9.0.3",
"@typescript-eslint/eslint-plugin": "^3.0.0",
"@typescript-eslint/parser": "^3.0.0",
"babel-loader": "^8.0.0",
"@types/webpack-env": "^1",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"babel-loader": "^10",
"babel-plugin-react-intl-auto": "https://github.com/elyby/babel-plugin-react-intl-auto.git#build",
"core-js": "3.6.5",
"csp-webpack-plugin": "^2.0.2",
"css-loader": "^3.5.3",
"core-js": "^3",
"css-loader": "^7.1.2",
"cssnano": "^4.1.10",
"dotenv": "^8.2.0",
"eager-imports-webpack-plugin": "^1.0.0",
"eslint": "^7.0.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-jsdoc": "^25.4.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jsdoc": "^46.10.1",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.20.0",
"exports-loader": "^0.7.0",
"extract-react-intl-messages": "^4.1.1",
"file-loader": "^6.0.0",
"file-replace-loader": "^1.2.0",
"html-loader": "^1.1.0",
"html-webpack-plugin": "^4.3.0",
"html-loader": "^5.1.0",
"html-webpack-plugin": "^5.6.0",
"identity-obj-proxy": "^3.0.0",
"imports-loader": "^0.8.0",
"jest": "^26.0.1",
"jest-watch-typeahead": "^0.6.0",
"json-loader": "^0.5.4",
"lint-staged": "^10.2.4",
"loader-utils": "^2.0.0",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^6.0.1",
"postcss": "^8.2.8",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-watch-typeahead": "^2.2.2",
"lint-staged": "^15.2.7",
"mini-css-extract-plugin": "^2.9.0",
"postcss": "^8",
"postcss-import": "^14.0.0",
"postcss-loader": "^4.2.0",
"postcss-loader": "^8.1.1",
"postcss-logical-properties-polyfill": "^0.1.0",
"postcss-scss": "^3.0.5",
"prettier": "^2.0.5",
"raw-loader": "^4.0.1",
"sass-loader": "^10.2.0",
"postcss-scss": "^4.0.9",
"prettier": "^3.3.3",
"sass": "^1.77.8",
"sass-loader": "^14.2.1",
"sinon": "^9.0.2",
"sitemap-webpack-plugin": "^0.8.0",
"speed-measure-webpack-plugin": "^1.3.3",
"storybook-addon-intl": "^2.4.1",
"style-loader": "~1.2.1",
"typescript": "^3.9.3",
"sitemap-webpack-plugin": "^1",
"storybook": "^8.2.9",
"style-loader": "^4.0.0",
"typescript": "^5.5.4",
"unexpected": "^11.14.0",
"unexpected-sinon": "^10.5.1",
"url-loader": "^4.1.0",
"wait-on": "^5.0.0",
"webpack": "^4.41.5",
"webpack-bundle-analyzer": "^3.8.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.10.1",
"webpackbar": "^4.0.0"
"webpack": "^5.94.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^7",
"webpack-dev-server": "^5"
}
}
@@ -169,24 +169,6 @@ describe('components/accounts/actions', () => {
});
});
describe('when user authenticated during oauth', () => {
beforeEach(() => {
(getState as any).returns({
accounts: {
available: [],
active: null,
},
user: {},
auth: {
oauth: {
clientId: 'ely.by',
prompt: [],
},
},
});
});
});
describe('when one account available', () => {
beforeEach(() => {
(getState as any).returns({
@@ -4,6 +4,11 @@ import { useHistory, useLocation, useRouteMatch } from 'react-router';
import { Factory } from './factory';
import PanelTransition from './PanelTransition';
export default {
title: 'Components/Auth',
excludeStories: /.*Presenter$/,
};
interface AuthPresenterProps {
factory: Factory;
}
@@ -1,8 +1,10 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { AuthPresenter } from 'app/components/auth/Auth.story';
import AcceptRules from './AcceptRules';
storiesOf('Components/Auth', module).add('AcceptRules', () => <AuthPresenter factory={AcceptRules} />);
export default { title: 'Components/Auth' };
export const AcceptRulesStory = () => <AuthPresenter factory={AcceptRules} />;
AcceptRulesStory.storyName = 'AcceptRules';
@@ -1,4 +1,4 @@
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/colors.scss' as *;
.descriptionText {
font-size: 15px;
@@ -1,9 +1,11 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { AuthPresenter } from 'app/components/auth/Auth.story';
import Activation from './Activation';
export default { title: 'Components/Auth' };
// TODO: add case with provided key
storiesOf('Components/Auth', module).add('Activation', () => <AuthPresenter factory={Activation} />);
export const ActivationStory = () => <AuthPresenter factory={Activation} />;
ActivationStory.storyName = 'Activation';
@@ -1,5 +1,5 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
.description {
}
@@ -1,5 +1,6 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use 'sass:color';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
.appInfo {
max-width: 270px;
@@ -43,7 +44,7 @@
margin-top: 7px;
a {
color: lighten($font-color, 10%);
color: color.adjust($font-color, $lightness: 10%, $space: hsl);
border-bottom-color: #666;
&:hover {
@@ -1,10 +1,12 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { AuthPresenter } from 'app/components/auth/Auth.story';
import ChooseAccount from './ChooseAccount';
export default { title: 'Components/Auth' };
// TODO: provide accounts list
// TODO: provide application name
storiesOf('Components/Auth', module).add('ChooseAccount', () => <AuthPresenter factory={ChooseAccount} />);
export const ChooseAccountStory = () => <AuthPresenter factory={ChooseAccount} />;
ChooseAccountStory.storyName = 'ChooseAccount';
@@ -1,5 +1,5 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
.accountSwitcher {
background: $black;
@@ -1,5 +1,5 @@
@import '~app/components/ui/panel.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/panel.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
.accountSwitcherContainer {
margin-left: -$bodyLeftRightPadding;
@@ -1,5 +1,5 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
.finishPage {
font-family: $font-family-title;
@@ -1,4 +1,4 @@
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/colors.scss' as *;
.descriptionText {
font-size: 15px;
@@ -1,4 +1,4 @@
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/fonts.scss' as *;
.avatar {
width: 90px;
@@ -1,5 +1,5 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
.authInfo {
// Отступы сверху и снизу разные т.к. мы ужимаем высоту линии строки с логином на 2 пикселя и из-за этого теряем отступ снизу
@@ -10,7 +10,7 @@
.authInfoAvatar {
$size: 30px;
float: start;
float: inline-start;
height: $size;
width: $size;
font-size: $size;
@@ -1,4 +1,4 @@
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/colors.scss' as *;
.descriptionText {
font-size: 15px;
@@ -1,4 +1,4 @@
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/fonts.scss' as *;
.description {
font-family: $font-family-title;
@@ -1,10 +1,11 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import ContactFormPopup from './ContactFormPopup';
storiesOf('Components/Popups', module).add('ContactFormPopup', () => (
export default { title: 'Components/Popups' };
export const ContactFormPopupStory = () => (
<ContactFormPopup
onSubmit={(params) => {
action('onSubmit')(params);
@@ -13,4 +14,5 @@ storiesOf('Components/Popups', module).add('ContactFormPopup', () => (
}}
onClose={action('onClose')}
/>
));
);
ContactFormPopupStory.storyName = 'ContactFormPopup';
@@ -1,9 +1,11 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import SuccessContactFormPopup from './SuccessContactFormPopup';
storiesOf('Components/Popups', module).add('SuccessContactFormPopup', () => (
export default { title: 'Components/Popups' };
export const SuccessContactFormPopupStory = () => (
<SuccessContactFormPopup email="email@ely.by" onClose={action('onClose')} />
));
);
SuccessContactFormPopupStory.storyName = 'SuccessContactFormPopup';
@@ -1,6 +1,6 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@import '~app/components/ui/popup/popup.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
@use '~app/components/ui/popup/popup.scss' as *;
/* Form state */
@@ -1,5 +1,4 @@
import React, { ComponentType, ComponentProps } from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import ApplicationsIndex from './ApplicationsIndex';
@@ -15,7 +14,7 @@ export const DevLayout: ComponentType = ({ children }) => (
</div>
);
export const sampleApp: OauthAppResponse = {
const sampleApp: OauthAppResponse = {
clientId: 'my-application',
clientSecret: 'cL1eNtS3cRE7xNJqfWQdqrMRKURfW1ssP4kiX6JDW0_szM-n-q',
type: TYPE_WEB_APPLICATION,
@@ -34,22 +33,32 @@ const commonProps: Omit<ComponentProps<typeof ApplicationsIndex>, 'isLoading' |
deleteApp: async (clientId) => action('deleteApp')(clientId),
};
storiesOf('Components/Dev/Apps/ApplicationsIndex', module)
.addDecorator((storyFn) => <DevLayout>{storyFn()}</DevLayout>)
.add('Guest', () => <ApplicationsIndex isLoading={false} displayForGuest applications={[]} {...commonProps} />)
.add('Loading', () => <ApplicationsIndex isLoading displayForGuest={false} applications={[]} {...commonProps} />)
.add('Empty', () => (
<ApplicationsIndex isLoading={false} displayForGuest={false} applications={[]} {...commonProps} />
))
.add('With apps', () => (
<ApplicationsIndex
isLoading={false}
displayForGuest={false}
applications={[
sampleApp,
{ ...sampleApp, clientId: 'my-application1', countUsers: 10 },
{ ...sampleApp, clientId: 'my-application2', countUsers: 1 },
]}
{...commonProps}
/>
));
export default {
title: 'Components/Dev/Apps/ApplicationsIndex',
decorators: [(storyFn: () => React.ReactElement) => <DevLayout>{storyFn()}</DevLayout>],
excludeStories: /.*Layout$/,
};
export const Guest = () => <ApplicationsIndex isLoading={false} displayForGuest applications={[]} {...commonProps} />;
export const Loading = () => (
<ApplicationsIndex isLoading displayForGuest={false} applications={[]} {...commonProps} />
);
export const Empty = () => (
<ApplicationsIndex isLoading={false} displayForGuest={false} applications={[]} {...commonProps} />
);
export const WithApps = () => (
<ApplicationsIndex
isLoading={false}
displayForGuest={false}
applications={[
sampleApp,
{ ...sampleApp, clientId: 'my-application1', countUsers: 10 },
{ ...sampleApp, clientId: 'my-application2', countUsers: 1 },
]}
{...commonProps}
/>
);
WithApps.storyName = 'With apps';
@@ -1,5 +1,4 @@
import React, { useState } from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { OauthAppResponse } from 'app/services/api/oauth';
@@ -28,78 +27,91 @@ const blankApp: OauthAppResponse = {
const onSubmit = async (form: FormModel) => action('onSubmit')(form);
storiesOf('Components/Dev/Apps/ApplicationForm', module)
.addDecorator((storyFn) => <DevLayout>{storyFn()}</DevLayout>)
.add('Create', () => {
const [currentType, setType] = useState<ApplicationType | null>(null);
export default {
title: 'Components/Dev/Apps/ApplicationForm',
decorators: [(storyFn: () => React.ReactElement) => <DevLayout>{storyFn()}</DevLayout>],
};
return (
<ApplicationForm
form={new FormModel()}
onSubmit={onSubmit}
displayTypeSwitcher
type={currentType}
setType={(type) => {
action('setType')(type);
setType(type);
}}
app={blankApp}
/>
);
})
.add('Create website', () => (
export const Create = () => {
const [currentType, setType] = useState<ApplicationType | null>(null);
return (
<ApplicationForm
form={new FormModel()}
onSubmit={onSubmit}
displayTypeSwitcher
type={TYPE_WEB_APPLICATION}
setType={action('setType')}
type={currentType}
setType={(type) => {
action('setType')(type);
setType(type);
}}
app={blankApp}
/>
))
.add('Create desktop application', () => (
<ApplicationForm
form={new FormModel()}
onSubmit={onSubmit}
displayTypeSwitcher
type={TYPE_DESKTOP_APPLICATION}
setType={action('setType')}
app={blankApp}
/>
))
.add('Create Minecraft server', () => (
<ApplicationForm
form={new FormModel()}
onSubmit={onSubmit}
displayTypeSwitcher
type={TYPE_MINECRAFT_SERVER}
setType={action('setType')}
app={{
...blankApp,
type: TYPE_MINECRAFT_SERVER,
}}
/>
))
.add('Update website', () => (
<ApplicationForm
form={new FormModel()}
onSubmit={onSubmit}
type={TYPE_WEB_APPLICATION}
app={{
...blankApp,
clientId: 'already-registered',
}}
/>
))
.add('Update Minecraft server', () => (
<ApplicationForm
form={new FormModel()}
onSubmit={onSubmit}
type={TYPE_MINECRAFT_SERVER}
app={{
...blankApp,
type: TYPE_MINECRAFT_SERVER,
clientId: 'already-registered',
}}
/>
));
);
};
export const CreateWebsite = () => (
<ApplicationForm
form={new FormModel()}
onSubmit={onSubmit}
displayTypeSwitcher
type={TYPE_WEB_APPLICATION}
setType={action('setType')}
app={blankApp}
/>
);
CreateWebsite.storyName = 'Create website';
export const CreateDesktopApplication = () => (
<ApplicationForm
form={new FormModel()}
onSubmit={onSubmit}
displayTypeSwitcher
type={TYPE_DESKTOP_APPLICATION}
setType={action('setType')}
app={blankApp}
/>
);
CreateDesktopApplication.storyName = 'Create desktop application';
export const CreateMinecraftServer = () => (
<ApplicationForm
form={new FormModel()}
onSubmit={onSubmit}
displayTypeSwitcher
type={TYPE_MINECRAFT_SERVER}
setType={action('setType')}
app={{
...blankApp,
type: TYPE_MINECRAFT_SERVER,
}}
/>
);
CreateMinecraftServer.storyName = 'Create Minecraft server';
export const UpdateWebsite = () => (
<ApplicationForm
form={new FormModel()}
onSubmit={onSubmit}
type={TYPE_WEB_APPLICATION}
app={{
...blankApp,
clientId: 'already-registered',
}}
/>
);
UpdateWebsite.storyName = 'Update website';
export const UpdateMinecraftServer = () => (
<ApplicationForm
form={new FormModel()}
onSubmit={onSubmit}
type={TYPE_MINECRAFT_SERVER}
app={{
...blankApp,
type: TYPE_MINECRAFT_SERVER,
clientId: 'already-registered',
}}
/>
);
UpdateMinecraftServer.storyName = 'Update Minecraft server';
@@ -1,5 +1,5 @@
@import '~app/components/ui/fonts.scss';
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/fonts.scss' as *;
@use '~app/components/ui/colors.scss' as *;
.container {
max-width: 500px;
@@ -114,7 +114,6 @@ export default class ApplicationItem extends React.Component<
key="publickOauthApplicationDescription"
defaultMessage="This is a public client and it can't have any secrets by design. To secure authorization flow, use the PKCE challenge code. <link>Read more</link>."
values={{
// @ts-expect-error those typings seems to be invalid in the current version of react-intl, but might be fixed later
link: (nodes) => (
<a href="https://www.oauth.com/oauth2-servers/pkce/" target="_blank">
{nodes}
@@ -1,5 +1,4 @@
import React, { ComponentType, CSSProperties } from 'react';
import { storiesOf } from '@storybook/react';
import FooterMenu from './FooterMenu';
@@ -7,7 +6,9 @@ const PreviewWrapper: ComponentType<{ style?: CSSProperties }> = ({ style, child
<div style={{ padding: '25px', width: 320, boxSizing: 'border-box', ...style }}>{children}</div>
);
storiesOf('Components', module).add('FooterMenu', () => (
export default { title: 'Components' };
export const FooterMenuStory = () => (
<div style={{ display: 'flex' }}>
<PreviewWrapper>
<FooterMenu />
@@ -16,4 +17,5 @@ storiesOf('Components', module).add('FooterMenu', () => (
<FooterMenu />
</PreviewWrapper>
</div>
));
);
FooterMenuStory.storyName = 'FooterMenu';
+8 -7
View File
@@ -22,18 +22,19 @@ export function getCountriesList(): string[] {
/**
* Возвращает для указанной локали её флаг с учётом всех нюансов загрузки флага
* и подбора соответствующего локали флага.
*
* @param {string} locale
*
* @returns {string}
*/
export function getLocaleIconUrl(locale: string): string {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require(`flag-icon-css/flags/4x3/${localeToCountryCode[locale] || locale}.svg`).default;
return require(`./flags/${locale}.svg`);
} catch (err) {
if (!err.message.startsWith('Cannot find module')) {
throw err;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require(`flag-icons/flags/4x3/${localeToCountryCode[locale] || locale}.svg`);
} catch (err) {
if (!err.message.startsWith('Cannot find module')) {
throw err;
}
}
}
@@ -1,10 +1,11 @@
import React, { useState } from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import LanguageSwitcherPopup from './LanguageSwitcherPopup';
storiesOf('Components/Popups', module).add('LanguageSwitcherPopup', () => {
export default { title: 'Components/Popups' };
export const LanguageSwitcherPopupStory = () => {
const [activeLocale, setActiveLocale] = useState('en');
return (
@@ -73,4 +74,5 @@ storiesOf('Components/Popups', module).add('LanguageSwitcherPopup', () => {
onClose={action('onClose')}
/>
);
});
};
LanguageSwitcherPopupStory.storyName = 'LanguageSwitcherPopup';
@@ -1,4 +1,4 @@
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/colors.scss' as *;
.languageLink {
position: relative;
@@ -28,7 +28,7 @@
display: inline-block;
margin-inline-end: 4px;
height: $height;
width: $height * 4 / 3;
width: calc($height * 4 / 3);
box-shadow: 0 0 1px rgba(#000, 0.2);
background-size: cover;
@@ -1,6 +1,6 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@import '~app/components/ui/popup/popup.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
@use '~app/components/ui/popup/popup.scss' as *;
@mixin hideFooter {
@media (max-height: 455px) {
@@ -1,12 +1,13 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { ProfileLayout } from 'app/components/profile/Profile.story';
import AccountDeleted from './AccountDeleted';
storiesOf('Components/Profile', module).add('AccountDeleted', () => (
export default { title: 'Components/Profile' };
export const AccountDeletedStory = () => (
<ProfileLayout>
<AccountDeleted
onRestore={() =>
@@ -17,4 +18,5 @@ storiesOf('Components/Profile', module).add('AccountDeleted', () => (
}
/>
</ProfileLayout>
));
);
AccountDeletedStory.storyName = 'AccountDeleted';
@@ -1,18 +1,22 @@
import React, { ComponentType } from 'react';
import { storiesOf } from '@storybook/react';
import React, { ComponentType, PropsWithChildren } from 'react';
import rootStyles from 'app/pages/root/root.scss';
import profileStyles from 'app/pages/profile/profile.scss';
import Profile from './Profile';
export const ProfileLayout: ComponentType = ({ children }) => (
<div className={rootStyles.wrapper}>
<div className={profileStyles.container}>{children}</div>
</div>
);
import Profile from './Profile';
export default {
title: 'Components/Profile',
excludeStories: /.*Layout$/,
};
storiesOf('Components/Profile', module).add('Profile', () => (
export const ProfileStory = () => (
<ProfileLayout>
<Profile
user={{
@@ -33,4 +37,5 @@ storiesOf('Components/Profile', module).add('Profile', () => (
activeLocale={'en'}
/>
</ProfileLayout>
));
);
ProfileStory.storyName = 'Profile';
@@ -1,69 +1,80 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { ProfileLayout } from 'app/components/profile/Profile.story';
import ChangeEmail from './ChangeEmail';
storiesOf('Components/Profile/ChangeEmail', module)
.addDecorator((story) => <ProfileLayout>{story()}</ProfileLayout>)
.add('First step', () => (
<ChangeEmail
step={0}
email="initial-email@ely.by"
onSubmit={(step, form) => {
action('onSubmit')(step, form);
export default {
title: 'Components/Profile/ChangeEmail',
decorators: [(story: () => React.ReactElement) => <ProfileLayout>{story()}</ProfileLayout>],
};
return Promise.resolve();
}}
onChangeStep={action('onChangeStep')}
/>
))
.add('Second step', () => (
<ChangeEmail
step={1}
email="email-from-prev-step@ely.by"
onSubmit={(step, form) => {
action('onSubmit')(step, form);
export const FirstStep = () => (
<ChangeEmail
step={0}
email="initial-email@ely.by"
onSubmit={(step, form) => {
action('onSubmit')(step, form);
return Promise.resolve();
}}
onChangeStep={action('onChangeStep')}
/>
))
.add('Second step with code', () => (
<ChangeEmail
step={1}
code="I7SP06BUTLLM8MA03O"
onSubmit={(step, form) => {
action('onSubmit')(step, form);
return Promise.resolve();
}}
onChangeStep={action('onChangeStep')}
/>
);
FirstStep.storyName = 'First step';
return Promise.resolve();
}}
onChangeStep={action('onChangeStep')}
/>
))
.add('Third step', () => (
<ChangeEmail
step={2}
onSubmit={(step, form) => {
action('onSubmit')(step, form);
export const SecondStep = () => (
<ChangeEmail
step={1}
email="email-from-prev-step@ely.by"
onSubmit={(step, form) => {
action('onSubmit')(step, form);
return Promise.resolve();
}}
onChangeStep={action('onChangeStep')}
/>
))
.add('Third step with code', () => (
<ChangeEmail
step={2}
code="I7SP06BUTLLM8MA03O"
onSubmit={(step, form) => {
action('onSubmit')(step, form);
return Promise.resolve();
}}
onChangeStep={action('onChangeStep')}
/>
);
SecondStep.storyName = 'Second step';
return Promise.resolve();
}}
onChangeStep={action('onChangeStep')}
/>
));
export const SecondStepWithCode = () => (
<ChangeEmail
step={1}
code="I7SP06BUTLLM8MA03O"
onSubmit={(step, form) => {
action('onSubmit')(step, form);
return Promise.resolve();
}}
onChangeStep={action('onChangeStep')}
/>
);
SecondStepWithCode.storyName = 'Second step with code';
export const ThirdStep = () => (
<ChangeEmail
step={2}
onSubmit={(step, form) => {
action('onSubmit')(step, form);
return Promise.resolve();
}}
onChangeStep={action('onChangeStep')}
/>
);
ThirdStep.storyName = 'Third step';
export const ThirdStepWithCode = () => (
<ChangeEmail
step={2}
code="I7SP06BUTLLM8MA03O"
onSubmit={(step, form) => {
action('onSubmit')(step, form);
return Promise.resolve();
}}
onChangeStep={action('onChangeStep')}
/>
);
ThirdStepWithCode.storyName = 'Third step with code';
@@ -1,5 +1,4 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { FormModel } from 'app/components/ui/form';
@@ -7,7 +6,9 @@ import { ProfileLayout } from 'app/components/profile/Profile.story';
import ChangePassword from './ChangePassword';
storiesOf('Components/Profile', module).add('ChangePassword', () => (
export default { title: 'Components/Profile' };
export const ChangePasswordStory = () => (
<ProfileLayout>
<ChangePassword
form={new FormModel()}
@@ -18,4 +19,5 @@ storiesOf('Components/Profile', module).add('ChangePassword', () => (
}}
/>
</ProfileLayout>
));
);
ChangePasswordStory.storyName = 'ChangePassword';
@@ -1,5 +1,4 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { FormModel } from 'app/components/ui/form';
@@ -7,7 +6,9 @@ import { ProfileLayout } from 'app/components/profile/Profile.story';
import ChangeUsername from './ChangeUsername';
storiesOf('Components/Profile', module).add('ChangeUsername', () => (
export default { title: 'Components/Profile' };
export const ChangeUsernameStory = () => (
<ProfileLayout>
<ChangeUsername
form={new FormModel()}
@@ -20,4 +21,5 @@ storiesOf('Components/Profile', module).add('ChangeUsername', () => (
}}
/>
</ProfileLayout>
));
);
ChangeUsernameStory.storyName = 'ChangeUsername';
@@ -1,12 +1,13 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { ProfileLayout } from 'app/components/profile/Profile.story';
import DeleteAccount from './DeleteAccount';
storiesOf('Components/Profile', module).add('DeleteAccount', () => (
export default { title: 'Components/Profile' };
export const DeleteAccountStory = () => (
<ProfileLayout>
<DeleteAccount
onSubmit={async () => {
@@ -14,4 +15,5 @@ storiesOf('Components/Profile', module).add('DeleteAccount', () => (
}}
/>
</ProfileLayout>
));
);
DeleteAccountStory.storyName = 'DeleteAccount';
@@ -1,4 +1,4 @@
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/fonts.scss' as *;
.removableDataRow {
composes: formRow from '~app/components/profile/profileForm.scss';
@@ -1,62 +1,70 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { ProfileLayout } from 'app/components/profile/Profile.story';
import MultiFactorAuth from './MultiFactorAuth';
storiesOf('Components/Profile/MultiFactorAuth', module)
.addDecorator((story) => <ProfileLayout>{story()}</ProfileLayout>)
.add('First step', () => (
<MultiFactorAuth
isMfaEnabled={false}
step={0}
onSubmit={(form, sendData) => {
action('onSubmit')(form, sendData);
export default {
title: 'Components/Profile/MultiFactorAuth',
decorators: [(story: () => React.ReactElement) => <ProfileLayout>{story()}</ProfileLayout>],
};
return Promise.resolve();
}}
onComplete={action('onComplete')}
onChangeStep={action('onChangeStep')}
/>
))
.add('Second step', () => (
<MultiFactorAuth
isMfaEnabled={false}
step={1}
onSubmit={(form, sendData) => {
action('onSubmit')(form, sendData);
export const FirstStep = () => (
<MultiFactorAuth
isMfaEnabled={false}
step={0}
onSubmit={(form, sendData) => {
action('onSubmit')(form, sendData);
return Promise.resolve();
}}
onComplete={action('onComplete')}
onChangeStep={action('onChangeStep')}
/>
))
.add('Third step', () => (
<MultiFactorAuth
isMfaEnabled={false}
step={2}
onSubmit={(form, sendData) => {
action('onSubmit')(form, sendData);
return Promise.resolve();
}}
onComplete={action('onComplete')}
onChangeStep={action('onChangeStep')}
/>
);
FirstStep.storyName = 'First step';
return Promise.resolve();
}}
onComplete={action('onComplete')}
onChangeStep={action('onChangeStep')}
/>
))
.add('Enabled', () => (
<MultiFactorAuth
isMfaEnabled
step={0}
onSubmit={(form, sendData) => {
action('onSubmit')(form, sendData);
export const SecondStep = () => (
<MultiFactorAuth
isMfaEnabled={false}
step={1}
onSubmit={(form, sendData) => {
action('onSubmit')(form, sendData);
return Promise.resolve();
}}
onComplete={action('onComplete')}
onChangeStep={action('onChangeStep')}
/>
));
return Promise.resolve();
}}
onComplete={action('onComplete')}
onChangeStep={action('onChangeStep')}
/>
);
SecondStep.storyName = 'Second step';
export const ThirdStep = () => (
<MultiFactorAuth
isMfaEnabled={false}
step={2}
onSubmit={(form, sendData) => {
action('onSubmit')(form, sendData);
return Promise.resolve();
}}
onComplete={action('onComplete')}
onChangeStep={action('onChangeStep')}
/>
);
ThirdStep.storyName = 'Third step';
export const Enabled = () => (
<MultiFactorAuth
isMfaEnabled
step={0}
onSubmit={(form, sendData) => {
action('onSubmit')(form, sendData);
return Promise.resolve();
}}
onComplete={action('onComplete')}
onChangeStep={action('onChangeStep')}
/>
);
@@ -1,4 +1,4 @@
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/fonts.scss' as *;
.instructionTitle {
font-family: $font-family-title;
@@ -1,5 +1,5 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
.mfaTitle {
composes: sectionTitle from '~app/components/profile/profileForm.scss';
@@ -1,17 +1,19 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { FormModel } from 'app/components/ui/form';
import PasswordRequestForm from './PasswordRequestForm';
storiesOf('Components/Popups/PasswordRequestForm', module)
.add('empty', () => <PasswordRequestForm form={new FormModel()} onSubmit={action('onSubmit')} />)
.add('with error', () => {
const form = new FormModel();
form.bindField('password');
form.errors['password'] = 'error.password_incorrect';
export default { title: 'Components/Popups/PasswordRequestForm' };
return <PasswordRequestForm form={form} onSubmit={action('onSubmit')} />;
});
export const Empty = () => <PasswordRequestForm form={new FormModel()} onSubmit={action('onSubmit')} />;
export const WithError = () => {
const form = new FormModel();
form.bindField('password');
form.errors['password'] = 'error.password_incorrect';
return <PasswordRequestForm form={form} onSubmit={action('onSubmit')} />;
};
WithError.storyName = 'with error';
@@ -1,4 +1,4 @@
@import '~app/components/ui/popup/popup.scss';
@use '~app/components/ui/popup/popup.scss' as *;
.boundings {
@include popupBounding(280px);
+2 -2
View File
@@ -1,5 +1,5 @@
@import '~app/components/ui/fonts.scss';
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/fonts.scss' as *;
@use '~app/components/ui/colors.scss' as *;
$formColumnWidth: 416px;
@@ -1,5 +1,5 @@
@import '~app/components/ui/fonts.scss';
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/fonts.scss' as *;
@use '~app/components/ui/colors.scss' as *;
.contentWithBackButton {
position: relative;
@@ -1,7 +1,9 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import SourceCodePopup from './SourceCodePopup';
storiesOf('Components/Popups', module).add('SourceCodePopup', () => <SourceCodePopup onClose={action('onClose')} />);
export default { title: 'Components/Popups' };
export const SourceCodePopupStory = () => <SourceCodePopup onClose={action('onClose')} />;
SourceCodePopupStory.storyName = 'SourceCodePopup';
@@ -1,6 +1,6 @@
@import '~app/components/ui/popup/popup.scss';
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/popup/popup.scss' as *;
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
.boundings {
@include popupBounding(420px);
@@ -1,6 +1,8 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import BSoD from './BSoD';
storiesOf('UI', module).add('BSoD', () => <BSoD />);
export default { title: 'UI' };
export const BSoDStory = () => <BSoD />;
BSoDStory.storyName = 'BSoD';
+1 -1
View File
@@ -1,4 +1,4 @@
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/colors.scss' as *;
$font-family-monospaced: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Roboto Mono', monospace;
+2 -2
View File
@@ -1,5 +1,5 @@
@import './colors.scss';
@import './fonts.scss';
@use './colors.scss' as *;
@use './fonts.scss' as *;
@mixin button-theme($themeName, $backgroundColor) {
.#{$themeName} {
@@ -1,4 +1,4 @@
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/colors.scss' as *;
.overflow {
overflow: hidden;
+9 -6
View File
@@ -1,3 +1,6 @@
@use 'sass:color';
@use 'sass:map';
$green: #207e5c;
$blue: #5b9aa9;
$red: #e66c69;
@@ -25,10 +28,10 @@ $whiteButtonDark: #f5f5f5; // TODO: найти оптимальный цвет
$black: #1c1c1c,
);
@if (map_has_key($elyColorsMap, $color)) {
@return map-get($elyColorsMap, $color);
@if (map.has-key($elyColorsMap, $color)) {
@return map.get($elyColorsMap, $color);
} @else {
@return darken($color, 4%);
@return color.adjust($color, $lightness: -4%, $space: hsl);
}
}
@@ -44,9 +47,9 @@ $whiteButtonDark: #f5f5f5; // TODO: найти оптимальный цвет
$black: #2e2e2e,
);
@if (map_has_key($elyColorsMap, $color)) {
@return map-get($elyColorsMap, $color);
@if (map.has-key($elyColorsMap, $color)) {
@return map.get($elyColorsMap, $color);
} @else {
@return lighten($color, 3%);
@return color.adjust($color, $lightness: 3%, $space: hsl);
}
}
@@ -1,9 +1,10 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import Button from './Button';
storiesOf('UI/Form', module).add('Button', () => (
export default { title: 'UI/Form' };
export const ButtonStory = () => (
<>
<div>
<Button>Green Button</Button> <Button color="blue">Blue Button</Button>{' '}
@@ -79,4 +80,5 @@ storiesOf('UI/Form', module).add('Button', () => (
</Button>
</div>
</>
));
);
ButtonStory.storyName = 'Button';
+1 -1
View File
@@ -131,7 +131,7 @@ export default class Form extends React.Component<Props, State> {
result = (this.props as PropsWithoutForm).onSubmit!(new FormData(form));
}
if (result && result.then) {
if (result) {
this.setState({ isLoading: true });
result
+4 -3
View File
@@ -2,6 +2,7 @@ import React from 'react';
import { MessageDescriptor } from 'react-intl';
import TextareaAutosize, { TextareaAutosizeProps } from 'react-textarea-autosize';
import clsx from 'clsx';
import { uniqueId, omit } from 'app/functions';
import { SKIN_DARK, COLOR_GREEN, Skin, Color } from 'app/components/ui';
@@ -11,8 +12,8 @@ import FormInputComponent from './FormInputComponent';
interface OwnProps {
placeholder?: string | MessageDescriptor;
label?: string | MessageDescriptor;
skin: Skin;
color: Color;
skin?: Skin;
color?: Color;
}
export default class TextArea extends FormInputComponent<OwnProps & Omit<TextareaAutosizeProps, keyof OwnProps>> {
@@ -57,7 +58,7 @@ export default class TextArea extends FormInputComponent<OwnProps & Omit<Textare
{label}
<div className={styles.textAreaContainer}>
<TextareaAutosize
inputRef={this.elRef}
ref={this.elRef}
className={clsx(styles.textArea, styles[`${skin}TextField`], styles[`${color}TextField`])}
placeholder={placeholder}
{...props}
@@ -1,5 +1,5 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
$dropdownPadding: 15px;
+2 -2
View File
@@ -1,5 +1,5 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
@mixin form-transition() {
// Анимация фона должна быть быстрее анимации рамки, т.к. визуально фон заполняется медленнее
@@ -1,4 +1,4 @@
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/colors.scss' as *;
.componentLoader {
text-align: center;
+3 -3
View File
@@ -1,5 +1,5 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
.panel {
background: $black;
@@ -25,7 +25,7 @@ $headerHeight: 60px;
.headerControl {
composes: black from '~app/components/ui/buttons.scss';
float: start;
float: inline-start;
overflow: hidden;
height: $headerHeight - 1px;
width: 49px;
+2 -2
View File
@@ -1,5 +1,5 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
$popupPadding: 20px; // Default content padding
$popupMargin: 20px; // Outer popup margins
+1 -1
View File
@@ -1,4 +1,4 @@
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/colors.scss' as *;
.pseudoAvatarWrapper {
position: relative;
@@ -1,4 +1,4 @@
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/colors.scss' as *;
.steps {
height: 40px;
@@ -1,5 +1,4 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import LoggedInPanel from './LoggedInPanel';
@@ -13,33 +12,39 @@ const activeAccount = {
isDeleted: false,
};
storiesOf('Components/Userbar', module)
.addDecorator((storyFn) => (
<div style={{ background: '#207e5c', paddingRight: '10px', textAlign: 'right' }}>{storyFn()}</div>
))
.add('LoggedInPanel', () => (
<LoggedInPanel
activeAccount={activeAccount}
accounts={[
activeAccount,
{
id: 2,
username: 'AnotherMockUser',
email: 'mock-user2@ely.by',
token: '',
refreshToken: '',
isDeleted: false,
},
{
id: 3,
username: 'DeletedUser',
email: 'i-am-deleted@ely.by',
token: '',
refreshToken: '',
isDeleted: true,
},
]}
onSwitchAccount={async (account) => action('onSwitchAccount')(account)}
onRemoveAccount={async (account) => action('onRemoveAccount')(account)}
/>
));
export default {
title: 'Components/Userbar',
decorators: [
(storyFn: () => React.ReactElement) => (
<div style={{ background: '#207e5c', paddingRight: '10px', textAlign: 'right' }}>{storyFn()}</div>
),
],
};
export const LoggedInPanelStory = () => (
<LoggedInPanel
activeAccount={activeAccount}
accounts={[
activeAccount,
{
id: 2,
username: 'AnotherMockUser',
email: 'mock-user2@ely.by',
token: '',
refreshToken: '',
isDeleted: false,
},
{
id: 3,
username: 'DeletedUser',
email: 'i-am-deleted@ely.by',
token: '',
refreshToken: '',
isDeleted: true,
},
]}
onSwitchAccount={async (account) => action('onSwitchAccount')(account)}
onRemoveAccount={async (account) => action('onRemoveAccount')(account)}
/>
);
LoggedInPanelStory.storyName = 'LoggedInPanel';
@@ -1,8 +1,8 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
// TODO: эту константу можно заимпортить из panel.scss, но это приводит к странным ошибкам
//@import '~app/components/ui/panel.scss';
//@use '~app/components/ui/panel.scss' as *;
$bodyLeftRightPadding: 20px;
$lightBorderColor: #eee;
@@ -1,4 +1,4 @@
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/colors.scss' as *;
.loggedInPanel {
}
+2 -2
View File
@@ -14,7 +14,7 @@ export default `
</script>
<style>
${require('!!css-loader!postcss-loader!sass-loader!app/index.scss')[0][1]}
${require('!!css-loader!postcss-loader!sass-loader!app/components/ui/loader/loader.scss')[0][1]}
${require('!!css-loader?{"esModule":false}!postcss-loader!sass-loader!app/index.scss').toString()}
${require('!!css-loader?{"esModule":false}!postcss-loader!sass-loader!app/components/ui/loader/loader.scss').toString()}
</style>
`;
+2 -6
View File
@@ -1,7 +1,3 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg version="1.2" baseProfile="tiny" id="&#x421;&#x43B;&#x43E;&#x439;_1"
xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="17px" height="19px"
viewBox="0 0 17 19">
<polygon fill="#FFFFFF" points="17,11.93 10.927,11.93 10.927,18 6.073,18 6.073,11.93 0,11.93 0,7.064 6.073,7.064 6.073,1
10.927,1 10.927,7.064 17,7.064 "/>
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" width="17px" height="19px" viewBox="0 0 17 19">
<polygon points="17,11.93 10.927,11.93 10.927,18 6.073,18 6.073,11.93 0,11.93 0,7.064 6.073,7.064 6.073,1 10.927,1 10.927,7.064 17,7.064 "/>
</svg>

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 254 B

+2 -3
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg version="1.2" baseProfile="tiny" id="&#x421;&#x43B;&#x43E;&#x439;_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="17px" height="19px" viewBox="0 0 17 19" xml:space="preserve">
<path fill="#FFFFFF" d="M6.909,3.659c-2.346,0-4.25,1.903-4.25,4.25c0,2.347,1.903,4.248,4.25,4.248c2.347,0,4.249-1.9,4.249-4.248
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" width="17px" height="19px" viewBox="0 0 17 19">
<path d="M6.909,3.659c-2.346,0-4.25,1.903-4.25,4.25c0,2.347,1.903,4.248,4.25,4.248c2.347,0,4.249-1.9,4.249-4.248
C11.157,5.562,9.254,3.659,6.909,3.659z M16.831,16.617l-1.214,1.215c-0.222,0.223-0.586,0.223-0.809,0l-2.429-2.428
c-0.224-0.223-0.224-0.588,0-0.813l0.067-0.064l-1.144-1.141c-0.029-0.029-0.044-0.066-0.066-0.098
c-1.185,0.953-2.689,1.527-4.329,1.527C3.093,14.816,0,11.725,0,7.909S3.093,1,6.909,1s6.909,3.092,6.909,6.909

Before

Width:  |  Height:  |  Size: 875 B

After

Width:  |  Height:  |  Size: 724 B

+2 -2
View File
@@ -1,5 +1,5 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
html,
body,
+3 -9
View File
@@ -11,11 +11,9 @@
"clsx": "^1.1.0",
"copy-to-clipboard": "^3.3.1",
"debounce": "^1.0.0",
"flag-icon-css": "^3.4.6",
"flag-icons": "^7",
"intl": "^1.2.5",
"promise.prototype.finally": "3.1.2",
"prop-types": "^15.6.2",
"raf": "^3.4.1",
"raven-js": "^3.27.0",
"react": "^17",
"react-click-away-listener": "^1.4.3",
@@ -25,23 +23,19 @@
"react-motion": "^0.5.0",
"react-redux": "^7.2.0",
"react-router-dom": "^5.2.0",
"react-textarea-autosize": "^7.1.2",
"react-textarea-autosize": "^8",
"react-transition-group": "^4.4.1",
"redux": "^4.0.5",
"redux-localstorage": "^0.4.1",
"redux-thunk": "^2.0.0",
"rtl-detect": "^1.0.2",
"url-search-params-polyfill": "^8.1.0",
"webfontloader": "^1.6.26",
"whatwg-fetch": "^3.0.0"
"webfontloader": "^1.6.26"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.8.0",
"@testing-library/react": "^12",
"@types/debounce": "^1.2.0",
"@types/intl": "^1.2.0",
"@types/promise.prototype.finally": "^2.0.4",
"@types/raf": "^3.4.0",
"@types/react-dom": "^17",
"@types/react-helmet": "^6.0.0",
"@types/react-motion": "^0.0.29",
+1 -1
View File
@@ -1,4 +1,4 @@
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/fonts.scss' as *;
// Original: http://codepen.io/vanderlanth/pen/rxpNMY
+1 -1
View File
@@ -1,4 +1,4 @@
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/colors.scss' as *;
$sidebar-width: 320px;
+2 -2
View File
@@ -1,5 +1,5 @@
@import '~app/components/ui/fonts.scss';
@import '~app/components/ui/colors.scss';
@use '~app/components/ui/fonts.scss' as *;
@use '~app/components/ui/colors.scss' as *;
.page {
border-top: 50px solid #ddd8ce;
+2 -2
View File
@@ -1,5 +1,5 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
$toolbarHeight: 50px;
+2 -2
View File
@@ -1,5 +1,5 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@use '~app/components/ui/colors.scss' as *;
@use '~app/components/ui/fonts.scss' as *;
.rules {
max-width: 500px;
-10
View File
@@ -1,13 +1,3 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import 'url-search-params-polyfill';
import 'whatwg-fetch';
import { shim as shimPromiseFinally } from 'promise.prototype.finally';
import { polyfill as rafPolyfill } from 'raf';
rafPolyfill();
shimPromiseFinally();
// allow :active styles in mobile Safary
document.addEventListener('touchstart', () => {}, true);
@@ -25,7 +25,7 @@ export default class RegisterState extends AbstractState {
return context
.run('register', payload)
.then(() => context.setState(new CompleteState()))
.catch((err) => err.errors || logger.warn('Error registering', err));
.catch((err) => err?.errors || logger.warn('Error registering', err));
}
reject(context: AuthContext, payload: Record<string, any>): void {
@@ -14,7 +14,7 @@ export default class ResendActivationState extends AbstractState {
return context
.run('resendActivation', payload)
.then(() => context.setState(new ActivationState()))
.catch((err) => err.errors || logger.warn('Error resending activation', err));
.catch((err) => err?.errors || logger.warn('Error resending activation', err));
}
reject(context: AuthContext): void {
+47 -32
View File
@@ -1,35 +1,26 @@
import logger from 'app/services/logger';
const needs = {
intl: !window.Intl,
plural: !window?.Intl?.PluralRules,
// @ts-ignore ts has a little bit outdated definitions
relative: !window?.Intl?.RelativeTimeFormat,
};
/**
* All modern browsers currently do support all required Intl APIs,
* for the outdated browsers we will polyfill this api on demand
*/
async function polyfill(locale: string): Promise<void> {
const promises: Promise<void>[] = [];
if (!needs.intl && Intl.DateTimeFormat.supportedLocalesOf([locale]).length === 0) {
// fallback to polyfill in case, when browser does not support locale we need
needs.intl = true;
needs.plural = true;
needs.relative = true;
}
let intlSupported = !!window.Intl;
let pluralSupported = !!window.Intl?.PluralRules;
let relativeSupported = !!window.Intl?.RelativeTimeFormat;
let localeSupported = intlSupported && Intl.DateTimeFormat.supportedLocalesOf([locale]).length > 0;
if (needs.intl) {
// If locale isn't supported, then we need to fully replace native intl in order to introduce a new locale
if (!intlSupported || !localeSupported) {
promises.push(polyfillIntl(locale));
} else {
if (needs.plural) {
if (!pluralSupported) {
promises.push(polyfillPlural(locale));
}
if (needs.relative) {
if (!relativeSupported) {
promises.push(polyfillRelative(locale));
}
}
@@ -62,16 +53,26 @@ async function polyfillIntl(locale: string): Promise<void> {
// MUST be loaded in series with the main polyfill
await Promise.all([
import(
// WARNING: .js extension is required for proper function of ContextReplacementPlugin
/* webpackChunkName: "intl-[request]" */
`intl/locale-data/jsonp/${locale}.js`
),
polyfillLocale(locale),
polyfillPlural(locale),
polyfillRelative(locale),
]);
}
async function polyfillLocale(locale: string): Promise<void> {
try {
await import(
/* webpackChunkName: "intl-[request]" */
`./overrides/intl/${locale}.js`
);
} catch {
await import(
/* webpackChunkName: "intl-[request]" */
`intl/locale-data/jsonp/${locale}.js`
);
}
}
async function polyfillPlural(locale: string): Promise<void> {
await import(
/* webpackChunkName: "intl-pluralrules" */
@@ -79,11 +80,19 @@ async function polyfillPlural(locale: string): Promise<void> {
);
// MUST be loaded in series with the main polyfill
await import(
// WARNING: .js extension is required for proper function of ContextReplacementPlugin
/* webpackChunkName: "intl-pluralrules-[request]" */
`@formatjs/intl-pluralrules/dist/locale-data/${locale}.js`
);
try {
await import(
// WARNING: .js extension is required for proper function of ContextReplacementPlugin
/* webpackChunkName: "intl-pluralrules-[request]" */
`./overrides/pluralrules/${locale}.js`
);
} catch {
await import(
// WARNING: .js extension is required for proper function of ContextReplacementPlugin
/* webpackChunkName: "intl-pluralrules-[request]" */
`@formatjs/intl-pluralrules/dist/locale-data/${locale}.js`
);
}
}
async function polyfillRelative(locale: string): Promise<void> {
@@ -93,11 +102,17 @@ async function polyfillRelative(locale: string): Promise<void> {
);
// MUST be loaded in series with the main polyfill
await import(
// WARNING: .js extension is required for proper function of ContextReplacementPlugin
/* webpackChunkName: "intl-relativetimeformat-[request]" */
`@formatjs/intl-relativetimeformat/dist/locale-data/${locale}.js`
);
try {
await import(
/* webpackChunkName: "intl-relativetimeformat-[request]" */
`./overrides/relativetimeformat/${locale}.js`
);
} catch {
await import(
/* webpackChunkName: "intl-relativetimeformat-[request]" */
`@formatjs/intl-relativetimeformat/dist/locale-data/${locale}.js`
);
}
}
export default polyfill;
-68
View File
@@ -1,68 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-env node */
/* eslint-disable no-console */
import type { Stats } from 'fs';
import fs from 'fs';
import webpack, { MultiCompiler } from 'webpack';
import chalk from 'chalk';
import webpackConfig from './../../webpack.dll.config';
// @ts-ignore
const compiler: MultiCompiler = webpack(webpackConfig);
Promise.all([stat(`${__dirname}/../../yarn.lock`), stat(`${__dirname}/../../dll/vendor.json`)])
.then(([lockFileStats, dllFileStats]) => {
const lockFile = new Date(lockFileStats.mtime);
const dll = new Date(dllFileStats.mtime);
if (dll < lockFile) {
return Promise.reject({
code: 'OUTDATED',
});
}
logResult(chalk.green('Current dlls are up to date!'));
})
.catch((err) => {
if (err.code !== 'ENOENT' && err.code !== 'OUTDATED') {
return Promise.reject(err);
}
console.log('Rebuilding dlls...');
return new Promise<void>((resolve, reject) => {
compiler.run((err, stats) => {
if (err) {
return reject(err);
}
logResult(
chalk.green('Dll was successfully build in %s ms'),
// @ts-expect-error - something wrong with webpack types
stats.endTime - stats.startTime,
);
resolve();
});
});
})
.catch((err) => {
logResult(chalk.red('Unexpected error checking dll state'), err);
process.exit(1);
});
function logResult(...args: any[]): void {
console.log('\n');
console.log(...args);
console.log('\n');
}
function stat(path: string): Promise<Stats> {
return new Promise((resolve, reject) => {
fs.stat(path, (err, stats) => {
err ? reject(err) : resolve(stats);
});
});
}
+12 -8
View File
@@ -16,22 +16,25 @@ import { prompt, DistinctQuestion } from 'inquirer';
import git from 'git-rev-sync';
import { crowdin as config } from './../../config';
import 'dotenv/config';
if (!config.apiKey) {
const CROWDIN_API_KEY = process.env.CROWDIN_API_KEY;
if (!CROWDIN_API_KEY) {
console.error(ch.red`crowdinApiKey is required`);
process.exit(126);
}
const PROJECT_ID = config.projectId;
const CROWDIN_FILE_PATH = config.filePath;
const SOURCE_LANG = config.sourceLang;
const LANG_DIR = config.basePath;
// @ts-ignore
const PROJECT_ID = parseInt(process.env.CROWDIN_PROJECT_ID || 350687);
const CROWDIN_FILE_PATH = 'accounts/site.json';
const SOURCE_LANG = 'en';
const LANG_DIR = path.resolve(__dirname, '../app/i18n');
const INDEX_FILE_NAME = 'index.js';
const MIN_RELEASE_PROGRESS = config.minTranslated;
const MIN_RELEASE_PROGRESS = 80; // Minimal ready percent before translation can be published
const crowdin = new Crowdin({
token: config.apiKey,
token: CROWDIN_API_KEY,
});
/**
@@ -62,6 +65,7 @@ const NATIVE_NAMES_MAP: Record<string, string> = {
pt: 'Português do Brasil',
sr: 'Српски',
ro: 'Română',
hu: 'Magyar',
udm: 'Удмурт',
zh: '简体中文',
};
+1 -1
View File
@@ -26,6 +26,6 @@
"@types/inquirer": "^6.5.0",
"@types/json5": "^0.0.30",
"@types/progress": "^2.0.3",
"@types/webpack": "^4.41.13"
"@types/webpack": "^5.28.5"
}
}
+9 -9
View File
@@ -7,7 +7,7 @@
* https://github.com/DragonsInn/fontgen-loader
*/
const loaderUtils = require('loader-utils');
const crypto = require('crypto');
const fontgen = require('webfonts-generator');
const path = require('path');
const glob = require('glob');
@@ -69,12 +69,15 @@ function getFilesAndDeps(patterns, context) {
module.exports = function (content) {
this.cacheable();
const params = loaderUtils.getOptions(this) || {};
const params = this.getOptions({}) || {};
let config;
try {
config = JSON.parse(content);
} catch (ex) {
config = this.exec(content, this.resourcePath);
// eslint-disable-next-line no-new-func
const mod = { exports: {} };
new Function('module', 'exports', content)(mod, mod.exports);
config = mod.exports;
}
config.__dirname = path.dirname(this.resourcePath);
@@ -167,16 +170,13 @@ module.exports = function (content) {
const format = formats[i];
if (embed) {
urls[format] = `data:${mimeTypes[format]};charset=utf-8;base64,${new Buffer(res[format]).toString(
'base64',
)}`;
urls[format] = `data:${mimeTypes[format]};charset=utf-8;base64,${Buffer.from(res[format]).toString('base64')}`;
} else {
let filename = config.fileName || params.fileName || '[hash]-[fontname][ext]';
filename = filename.replace('[fontname]', fontconf.fontName).replace('[ext]', `.${format}`);
const url = loaderUtils.interpolateName(this.context, filename, {
content: res[format],
});
const contentHash = crypto.createHash('sha1').update(res[format]).digest('hex').slice(0, 8);
const url = filename.replace('[hash]', contentHash);
urls[format] = path.join(pub, url).replace(/\\/g, '/');
this.emitFile(url, res[format]);
+48 -63
View File
@@ -1,73 +1,58 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-env node */
const path = require('path');
const loaderUtils = require('loader-utils');
const fileCache = {};
const isProduction = process.argv.some((arg) => arg === '-p');
const rootPath = path.resolve('./src');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = ({ webpackLoaderContext: loader }) => ({
syntax: 'postcss-scss',
plugins: {
'postcss-import': {
addModulesDirectories: ['./src'],
module.exports = (ctx) => {
// postcss-loader v5+ uses ctx.webpack, v4 used ctx.webpackLoaderContext
const loader = ctx.webpack || ctx.webpackLoaderContext;
resolve: (
(defaultResolve) => (url, basedir, importOptions) =>
defaultResolve(
// mainly to remove '~' from request
loaderUtils.urlToRequest(url),
basedir,
importOptions,
)
)(require('postcss-import/lib/resolve-id')),
return {
syntax: 'postcss-scss',
plugins: [
require('postcss-import')({
addModulesDirectories: ['./src'],
load: ((defaultLoad) => (filename, importOptions) => {
if (/\.font.(js|json)$/.test(filename)) {
// separately process calls to font loader
// e.g. `@import '~app/icons.font.json';`
if (!fileCache[filename] || !isProduction) {
// do not execute loader on the same file twice
// this is an overcome for a bug with ExtractTextPlugin, for isProduction === true
// when @imported files may be processed multiple times
fileCache[filename] = new Promise((resolve, reject) =>
loader.loadModule(filename, (err, source) => {
if (err) {
reject(err);
resolve: (
(defaultResolve) => (url, basedir, importOptions) =>
defaultResolve(
// mainly to remove '~' from request
url.replace(/^~/, ''),
basedir,
importOptions,
)
)(require('postcss-import/lib/resolve-id')),
return;
}
load: ((defaultLoad) => (filename, importOptions) => {
if (/\.font.(js|json)$/.test(filename)) {
// separately process calls to font loader
// e.g. `@import '~app/icons.font.json';`
if (!fileCache[filename] || !isProduction) {
fileCache[filename] = new Promise((resolve, reject) =>
loader.loadModule(filename, (err, source) => {
if (err) {
reject(err);
resolve(loader.exec(source, rootPath));
}),
);
return;
}
// loader.exec() was removed in webpack 5; evaluate the CommonJS module manually
const mod = { exports: {} };
// eslint-disable-next-line no-new-func
new Function('module', 'exports', 'require', source)(mod, mod.exports, require);
resolve(mod.exports);
}),
);
}
return fileCache[filename];
}
return fileCache[filename];
}
return defaultLoad(filename, importOptions);
})(require('postcss-import/lib/load-content')),
},
'postcss-logical-properties-polyfill': {},
// TODO: for some reason cssnano strips out @mixin declarations
// cssnano: {
// /**
// * TODO: cssnano options
// */
// // autoprefixer: {
// // add: true,
// // remove: true,
// // browsers: ['last 2 versions']
// // },
// // safe: true,
// // // отключаем минификацию цветов, что бы она не ломала такие выражения:
// // // composes: black from '~./buttons.scss';
// // colormin: false,
// // discardComments: {
// // removeAll: true
// // }
// preset: 'default'
// }
},
});
return defaultLoad(filename, importOptions);
})(require('postcss-import/lib/load-content')),
}),
require('postcss-logical-properties-polyfill').default(),
],
};
};
-23
View File
@@ -1,23 +0,0 @@
#!/usr/bin/env node
/* eslint-disable @typescript-eslint/no-var-requires */
const fs = require('fs');
const path = require('path');
const overrideMap = {
// Override some intl definitions
'packages/app/services/i18n/overrides/intl': 'node_modules/intl/locale-data/jsonp',
'packages/app/services/i18n/overrides/pluralrules': 'node_modules/@formatjs/intl-pluralrules/dist/locale-data',
// eslint-disable-next-line prettier/prettier
'packages/app/services/i18n/overrides/relativetimeformat': 'node_modules/@formatjs/intl-relativetimeformat/dist/locale-data',
// Override country flags
'packages/app/components/i18n/flags': 'node_modules/flag-icon-css/flags/4x3',
};
Object.entries(overrideMap).forEach(([sourceDir, targetDir]) => {
fs.readdirSync(sourceDir).forEach((localeFile) => {
fs.copyFileSync(path.join(sourceDir, localeFile), path.join(targetDir, localeFile));
});
});
+2 -1
View File
@@ -18,7 +18,8 @@
// *.intl.json overrides (@see `webpack-loaders.d.ts`)
"resolveJsonModule": false,
"jsx": "preserve",
"strict": true
"strict": true,
"useUnknownInCatchVariables": false
},
"include": ["@types", "packages", "config", ".storybook"],
"exclude": ["packages/app/services/i18n/overrides"]
+88 -100
View File
@@ -5,17 +5,15 @@ require('@babel/register')({
extensions: ['.es6', '.es', '.jsx', '.js', '.mjs', '.ts', '.tsx'],
});
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const SitemapPlugin = require('sitemap-webpack-plugin').default;
const CSPPlugin = require('csp-webpack-plugin');
const WebpackBar = require('webpackbar');
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const EagerImportsPlugin = require('eager-imports-webpack-plugin').default;
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const config = require('./config');
const SUPPORTED_LANGUAGES = Object.keys(require('app/i18n').default);
const { getCountriesList } = require('app/components/i18n/localeFlags');
const rootPath = path.resolve('./packages');
@@ -28,19 +26,20 @@ const isDockerized = !!process.env.DOCKERIZED;
const isStorybook = process.env.APP_ENV === 'storybook';
const isCI = !!process.env.CI;
const isSilent = isCI || process.argv.some((arg) => /quiet/.test(arg));
const isCspEnabled = false;
const enableDll = !isProduction && !isStorybook;
const webpackEnv = isProduction ? 'production' : 'development';
process.env.NODE_ENV = webpackEnv;
const smp = new SpeedMeasurePlugin();
// Collect our custom language flags in order to exclude them from packaged
const customFlags = fs
.readdirSync(path.resolve(rootPath, 'app/components/i18n/flags'))
.filter((file) => file.endsWith('.svg'))
.map((file) => path.basename(file, '.svg'));
const flagIconCountries = getCountriesList().filter((country) => !customFlags.includes(country));
const webpackConfig = {
mode: webpackEnv,
cache: true,
entry: {
app: path.join(__dirname, 'packages/app'),
},
@@ -48,9 +47,18 @@ const webpackConfig = {
output: {
path: outputPath,
publicPath: '/',
filename: `[name].js?[${isProduction ? 'chunkhash' : 'hash'}]`,
filename: `[name].js?[${isProduction ? 'contenthash' : 'fullhash'}]`,
clean: true,
},
// intl-messageformat@8.4.1 incorrectly declares ^5 parser dependency but uses ^6 exports.
// These functions are never called since the app doesn't use skeleton date/number patterns.
// Will be resolve when we will migrate to the newer intl version
ignoreWarnings: [
/export 'parseDateTimeSkeleton'.*was not found in 'intl-messageformat-parser'/,
/export 'convertNumberSkeletonToNumberFormatOptions'.*was not found in 'intl-messageformat-parser'/,
],
resolve: {
modules: [rootPath, 'node_modules'],
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
@@ -59,7 +67,7 @@ const webpackConfig = {
},
},
devtool: 'cheap-module-source-map',
devtool: isProduction ? 'hidden-source-map' : 'eval-source-map',
stats: {
hash: false,
@@ -78,47 +86,36 @@ const webpackConfig = {
},
plugins: [
new WebpackBar(),
new webpack.DefinePlugin({
'window.SENTRY_DSN': config.sentryDSN ? JSON.stringify(config.sentryDSN) : undefined,
'window.GA_ID': config.ga && config.ga.id ? JSON.stringify(config.ga.id) : undefined,
'window.SENTRY_DSN': process.env.SENTRY_DSN ? JSON.stringify(process.env.SENTRY_DSN) : undefined,
'window.GA_ID': process.env.GA_ID ? JSON.stringify(process.env.GA_ID) : undefined,
}),
new webpack.EnvironmentPlugin({
NODE_ENV: process.env.NODE_ENV,
__VERSION__: config.version || '',
__VERSION__: process.env.VERSION || process.env.NODE_ENV,
}),
new HtmlWebpackPlugin({
template: 'packages/app/index.ejs',
favicon: 'packages/app/favicon.ico',
scripts: enableDll ? ['/dll/vendor.dll.js'] : [],
hash: false, // webpack does this for all our assets automagically
filename: 'index.html',
inject: false,
minify: {
collapseWhitespace: isProduction,
},
isCspEnabled,
}),
new SitemapPlugin(
'https://account.ely.by',
['/', '/register', '/resend-activation', '/activation', '/forgot-password', '/rules'],
{
lastMod: true,
changeFreq: 'weekly',
},
),
// restrict webpack import context, to create chunks only for supported locales
// @see services/i18n/intlPolyfill.js
new webpack.ContextReplacementPlugin(/locale-data/, new RegExp(`/(${SUPPORTED_LANGUAGES.join('|')})\\.js$`)),
// @see components/i18n/localeFlags.js
new webpack.ContextReplacementPlugin(
/flag-icon-css\/flags\/4x3/,
new RegExp(`/(${getCountriesList().join('|')})\\.svg`),
/flag-icons\/flags\/4x3/,
new RegExp(`/(${flagIconCountries.join('|')})\\.svg`),
),
// @see components/i18n/localeFlags.js
new webpack.ContextReplacementPlugin(
/app\/components\/i18n\/flags/,
new RegExp(`/(${getCountriesList().join('|')})\\.svg`),
new RegExp(`/(${SUPPORTED_LANGUAGES.join('|')})\\.svg`),
),
],
@@ -129,28 +126,35 @@ const webpackConfig = {
use: [
{
loader: 'style-loader',
options: {
// style-loader@1.1.2 is still buggy. It breaks our icon styles
// (vertical align is broken and styles applied multiple times)
// so we can not use it and it's new `esModule` options
// esModule: true,
},
},
{
loader: 'css-loader',
options: {
modules: {
localIdentName: isProduction ? '[hash:base64:5]' : '[path][name]-[local]',
// css-loader v7 changed the default to 'camel-case-only', which broke
// composes: some-dashed-class from '...', restore the v3 behaviour
exportLocalsConvention: 'as-is',
},
esModule: true,
esModule: false,
importLoaders: 2,
sourceMap: !isProduction,
// Skip resolving absolute URLs (e.g. /assets/fonts/... from fontgen-loader)
url: {
filter: (url) => !url.startsWith('/'),
},
},
},
{
loader: 'sass-loader',
options: {
api: 'modern',
sourceMap: !isProduction,
sassOptions: {
sourceMapIncludeSources: !isProduction,
// icons.scss uses @import for fontgen-loader integration via postcss-import
silenceDeprecations: ['import'],
},
},
},
{
@@ -176,21 +180,25 @@ const webpackConfig = {
},
{
test: /\.(png|gif|jpg|svg)$/,
loader: 'file-loader',
query: {
name: 'assets/[name].[ext]?[contenthash]',
type: 'asset/resource',
generator: {
filename: 'assets/[name][ext]?[contenthash]',
},
},
{
test: /\.(woff|woff2|ttf)$/,
loader: 'file-loader',
query: {
name: 'assets/fonts/[name].[ext]?[contenthash]',
type: 'asset/resource',
generator: {
filename: 'assets/fonts/[name][ext]?[contenthash]',
},
},
{
test: /\.html$/,
loader: 'html-loader',
options: {
// first-render.js uses require('...html') in a template literal — needs a plain string
esModule: false,
},
},
{
// this is consumed by postcss-import
@@ -234,19 +242,36 @@ if (isProduction) {
filename: '[name].css?[contenthash]',
}),
);
webpackConfig.plugins.push(
new SitemapPlugin({
base: 'https://account.ely.by',
paths: [
'/',
'/register',
'/resend-activation',
'/activation',
'/forgot-password',
'/rules',
'/dev/applications',
],
options: {
lastmod: true,
changefreq: 'weekly',
},
}),
);
}
webpackConfig.devtool = 'hidden-source-map';
webpackConfig.optimization = {
moduleIds: 'hashed',
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
name: 'vendors',
priority: 0,
test: (m) => String(m.context).includes('node_modules'),
test: /node_modules/,
chunks: 'all',
},
polyfills: {
@@ -256,12 +281,6 @@ if (isProduction) {
enforce: true,
},
...SUPPORTED_LANGUAGES.reduce((acc, locale) => {
const localePolyfills = [
`intl-${locale}-js`,
`intl-pluralrules-${locale}-js`,
`intl-relativetimeformat-${locale}-js`,
];
acc[locale] = {
name: `locale-${locale}`,
priority: 1,
@@ -271,7 +290,15 @@ if (isProduction) {
acc[`${locale}Polyfill`] = {
name: `locale-${locale}-polyfill`,
priority: 2,
chunks: ({ name }) => localePolyfills.includes(name),
// Matches the upstream locale-data chunks (intl-<locale>-js,
// intl-pluralrules-<locale>-js, intl-relativetimeformat-<locale>-js)
// as well as our ./overrides chunks. The latter live under a
// resolve.modules root, so their [request] chunk name expands to the
// full module path — but it still starts with `intl` and ends with
// `-<locale>-js`, so a prefix/suffix match folds every variant in here
// (and keeps that long, structure-leaking name out of the emitted files).
chunks: ({ name }) =>
typeof name === 'string' && name.startsWith('intl') && name.endsWith(`-${locale}-js`),
enforce: true,
};
@@ -281,61 +308,22 @@ if (isProduction) {
},
};
} else {
webpackConfig.plugins.push(
// force webpack to use mode: eager chunk imports in dev mode
// this will improve build performance
// this mode will be default for dev builds in webpack 5
new EagerImportsPlugin(),
);
if (enableDll) {
webpackConfig.plugins.push(
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dll/vendor.json'),
}),
);
}
webpackConfig.devServer = {
host: 'localhost',
port: 8080,
proxy: config.apiHost && {
'/api': {
target: config.apiHost,
changeOrigin: true, // add host http-header, based on target
secure: false, // allow self-signed certs
proxy: [
{
context: ['/api'],
target: process.env.API_HOST || 'https://account.dev.ely.by',
changeOrigin: true,
secure: false,
},
},
],
hot: true,
inline: true,
historyApiFallback: true,
};
}
if (isCspEnabled) {
webpackConfig.plugins.push(
new CSPPlugin({
'default-src': "'none'",
'style-src': ["'self'", "'unsafe-inline'"],
'script-src': [
"'self'",
"'nonce-edge-must-die'",
"'unsafe-inline'",
'https://www.google-analytics.com',
'https://recaptcha.net/recaptcha/',
'https://www.gstatic.com/recaptcha/',
'https://www.gstatic.cn/recaptcha/',
],
'img-src': ["'self'", 'data:', 'www.google-analytics.com'],
'font-src': ["'self'", 'data:'],
'connect-src': ["'self'", 'https://sentry.ely.by'].concat(isProduction ? [] : ['ws://localhost:8080']),
'frame-src': ['https://www.google.com/recaptcha/', 'https://recaptcha.net/recaptcha/'],
'report-uri': 'https://sentry.ely.by/api/2/csp-report/?sentry_key=088e7718236a4f91937a81fb319a93f6',
}),
);
}
if (isDockerized) {
webpackConfig.watchOptions = {
poll: 2000,
@@ -361,4 +349,4 @@ if (isSilent) {
};
}
module.exports = smp.wrap(webpackConfig);
module.exports = webpackConfig;
-46
View File
@@ -1,46 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-env node */
const path = require('path');
const webpack = require('webpack');
const vendor = Object.keys(require('./packages/app/package.json').dependencies);
const baseConfig = require('./babel.config');
if (process.env.NODE_ENV === 'production') {
throw new Error('Dll plugin should be used for dev mode only');
}
const outputPath = path.join(__dirname, 'dll');
const webpackConfig = {
mode: 'development',
entry: {
vendor: vendor.filter(
(item) =>
// if we include rhl in dll. It won't work for some reason
!item.includes('react-hot-loader'),
),
},
output: {
filename: '[name].dll.js?[hash]',
path: outputPath,
library: '[name]',
publicPath: '/',
},
// @ts-ignore
resolve: baseConfig.resolve,
plugins: [
new webpack.DllPlugin({
name: '[name]',
path: path.join(outputPath, '[name].json'),
}),
new webpack.EnvironmentPlugin({
NODE_ENV: process.env.NODE_ENV,
}),
],
};
module.exports = webpackConfig;
+6670 -9488
View File
File diff suppressed because it is too large. Load diff