├── .npmrc ├── eslint-local-rules.js ├── components ├── ExtCapirs │ ├── ExtCapirs.scss │ ├── ExtCapirs.examples │ │ └── default.xml │ ├── commonInterfaces.ts │ ├── inlineScript.ts │ └── ExtCapirs.tsx ├── ExtSmi2Widget │ ├── ExtSmi2Widget.scss │ ├── ExtSmi2Widget.examples │ │ └── default.xml │ └── ExtSmi2Widget.tsx ├── ExtGnezdoWidget │ ├── ExtGnezdoWidget.scss │ ├── ExtGnezdoWidget.examples │ │ └── default.xml │ └── ExtGnezdoWidget.tsx ├── ExtInfoxSgWidget │ ├── ExtInfoxSgWidget.scss │ ├── ExtInfoxSgWidget.examples │ │ └── default.xml │ └── ExtInfoxSgWidget.tsx ├── ExtRuamoOauth │ ├── ExtRuamoOauth.examples │ │ └── default.xml │ ├── ExtRuamoOauth.scss │ └── ExtRuamoOauth.tsx ├── ExtTgmEmbed │ ├── ExtTgmEmbed.examples │ │ └── default.xml │ ├── inlineScript.ts │ └── ExtTgmEmbed.tsx ├── ExtSocialMartWidget │ ├── ExtSocialMartWidget.examples │ │ └── default.xml │ ├── ExtSocialMartWidget.scss │ └── ExtSocialMartWidget.tsx ├── ExtPulseWidget │ ├── ExtPulseWidget.examples │ │ └── ExtPulseWidget.xml │ └── ExtPulseWidget.tsx ├── ExtFancyButton │ ├── ExtFancyButton.examples │ │ └── default.xml │ ├── ExtFancyButton.scss │ └── ExtFancyButton.tsx ├── ExtSmiFmWidget │ ├── ExtSmiFmWidget.examples │ │ └── default.xml │ └── ExtSmiFmWidget.tsx ├── ExtStrossleWidget │ ├── ExtStrossleWidget.scss │ ├── ExtStrossleWidget.examples │ │ └── default.xml │ └── ExtStrossleWidget.tsx ├── ExtSvkNativeWidget │ ├── ExtSvkNativeWidget.examples │ │ └── default.xml │ └── ExtSvkNativeWidget.tsx ├── ExtSmiCenterWidget │ ├── ExtSmiCenterWidget.examples │ │ └── default.xml │ └── ExtSmiCenterWidget.tsx ├── Ext24smiWidget │ ├── Ext24smiWidget.examples │ │ └── default.xml │ └── Ext24smiWidget.tsx ├── ExtDirectadvertWidget │ ├── ExtDirectadvertWidget.examples │ │ └── default.xml │ └── ExtDirectadvertWidget.tsx ├── ExtGiraffWidget │ ├── ExtGiraffWidget.examples │ │ └── default.xml │ └── ExtGiraffWidget.tsx ├── ExtRetellWidget │ ├── ExtRetellWidget.examples │ │ └── default.xml │ └── ExtRetellWidget.tsx ├── ExtDrive2Posts │ ├── ExtDrive2Posts.examples │ │ └── default.xml │ ├── ExtDrive2Posts.scss │ └── ExtDrive2Posts.tsx ├── ExtGrattisWidget │ ├── ExtGrattisWidget.examples │ │ └── default.xml │ └── ExtGrattisWidget.tsx ├── ExtDrive2Gallery │ ├── ExtDrive2Gallery.examples │ │ └── default.xml │ ├── ExtDrive2Gallery.scss │ └── ExtDrive2Gallery.tsx ├── ExtGrattisTurboWidget │ ├── ExtGrattisTurboWidget.examples │ │ └── default.xml │ └── ExtGrattisTurboWidget.tsx ├── ExtMgidWidget │ ├── ExtMgidWidget.examples │ │ └── default.xml │ └── ExtMgidWidget.tsx ├── ExtDrive2PartsOffers │ ├── ExtDrive2PartsOffers.examples │ │ └── default.xml │ ├── ExtDrive2PartsOffers.scss │ └── ExtDrive2PartsOffers.tsx ├── ExtKommersantWidget │ ├── utils │ │ ├── object.ts │ │ ├── json.ts │ │ └── elements.ts │ ├── ExtKommersantWidget.scss │ ├── ExtKommersantWidget.examples │ │ └── default.xml │ └── ExtKommersantWidget.tsx ├── ExtLentainformWidget │ ├── ExtLentainformWidget.examples │ │ └── default.xml │ └── ExtLentainformWidget.tsx ├── ExtRbcCovid │ ├── commonInterfaces.ts │ ├── utils │ │ ├── number.ts │ │ └── date.ts │ ├── ExtRbcCovid.examples │ │ └── default.xml │ ├── ExtRbcCovid.scss │ └── ExtRbcCovid.tsx ├── ExtExchangeRates │ ├── utils │ │ ├── date.ts │ │ └── rates.ts │ ├── ExtExchangeRates.examples │ │ └── default.xml │ ├── ExtExchangeRates.scss │ └── ExtExchangeRates.tsx ├── ExtFancyParagraph │ ├── ExtFancyParagraph.scss │ ├── ExtFancyParagraph.examples │ │ └── default.xml │ └── ExtFancyParagraph.tsx └── ExtEmbed │ ├── README.md │ ├── ExtEmbed.scss │ └── ExtEmbed.tsx ├── .travis.yml ├── server ├── utils │ ├── removeCSP.ts │ ├── handleError.ts │ └── preparePage.ts ├── index.ts ├── getHtml.ts └── routes │ └── index.ts ├── CHANGELOG.md ├── .gitignore ├── lints ├── test │ ├── utils.js │ ├── no-async.js │ └── correct-file-export.js ├── index.js └── rules │ ├── no-async │ ├── globals.js │ ├── Readme.md │ └── index.js │ ├── no-undefined-window │ ├── globals.js │ ├── Readme.md │ └── index.js │ ├── shared │ ├── getSuperclassName.js │ ├── isReactImportedNames.js │ ├── getFirstParent.js │ ├── isValidBinaryExpression.js │ ├── isGuardedUpper.js │ ├── isSafeReactMethod.js │ ├── isSafeLogicalExpression.js │ ├── utils.js │ ├── isValidIdentifier.js │ └── isValidLogicalInner.js │ └── correct-file-export │ ├── Readme.md │ └── index.js ├── tools ├── watch-registry.ts ├── AfterEmitPlugin.js ├── build-registry.ts └── lint-filesystem.ts ├── AUTHORS ├── LICENSE ├── postcss.config.js ├── tsconfig.json ├── stylelint.config.js ├── package.json ├── webpack.config.js ├── README.md └── .eslintrc.js /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /eslint-local-rules.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lints'); 2 | -------------------------------------------------------------------------------- /components/ExtCapirs/ExtCapirs.scss: -------------------------------------------------------------------------------- 1 | .ext-embed__ext-capirs_fill { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /components/ExtSmi2Widget/ExtSmi2Widget.scss: -------------------------------------------------------------------------------- 1 | .ext-embed__ext-smi2-widget { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /components/ExtGnezdoWidget/ExtGnezdoWidget.scss: -------------------------------------------------------------------------------- 1 | .ext-embed__ext-gnezdo-widget { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /components/ExtInfoxSgWidget/ExtInfoxSgWidget.scss: -------------------------------------------------------------------------------- 1 | .ext-embed__ext-infoxsg-widget { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /components/ExtRuamoOauth/ExtRuamoOauth.examples/default.xml: -------------------------------------------------------------------------------- 1 | Создать анкету 2 | -------------------------------------------------------------------------------- /components/ExtTgmEmbed/ExtTgmEmbed.examples/default.xml: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /components/ExtSocialMartWidget/ExtSocialMartWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8.6" 5 | 6 | install: 7 | - npm install 8 | - npm run lint 9 | -------------------------------------------------------------------------------- /components/ExtPulseWidget/ExtPulseWidget.examples/ExtPulseWidget.xml: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /components/ExtFancyButton/ExtFancyButton.examples/default.xml: -------------------------------------------------------------------------------- 1 | 2 | i am an external component 3 | 4 | -------------------------------------------------------------------------------- /components/ExtSmi2Widget/ExtSmi2Widget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /components/ExtSmiFmWidget/ExtSmiFmWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /components/ExtStrossleWidget/ExtStrossleWidget.scss: -------------------------------------------------------------------------------- 1 | .ext-embed__ext-strossle-widget { 2 | width: 100%; 3 | min-height: 290px; 4 | } 5 | -------------------------------------------------------------------------------- /components/ExtSocialMartWidget/ExtSocialMartWidget.scss: -------------------------------------------------------------------------------- 1 | .ext-embed__ext-socialmart-widget { 2 | width: 100%; 3 | min-height: 275px; 4 | } 5 | -------------------------------------------------------------------------------- /components/ExtSvkNativeWidget/ExtSvkNativeWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /components/ExtSmiCenterWidget/ExtSmiCenterWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /components/Ext24smiWidget/Ext24smiWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /components/ExtDirectadvertWidget/ExtDirectadvertWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /components/ExtGiraffWidget/ExtGiraffWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /components/ExtGnezdoWidget/ExtGnezdoWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /components/ExtRetellWidget/ExtRetellWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/ExtInfoxSgWidget/ExtInfoxSgWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /components/ExtDrive2Posts/ExtDrive2Posts.examples/default.xml: -------------------------------------------------------------------------------- 1 | 2 |

Отзывы

3 |
4 | -------------------------------------------------------------------------------- /components/ExtGrattisWidget/ExtGrattisWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /components/ExtCapirs/ExtCapirs.examples/default.xml: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /components/ExtDrive2Gallery/ExtDrive2Gallery.examples/default.xml: -------------------------------------------------------------------------------- 1 | 2 |

Фотографии

3 |
4 | -------------------------------------------------------------------------------- /components/ExtStrossleWidget/ExtStrossleWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 2 | i am Strossle widget component 3 | 4 | -------------------------------------------------------------------------------- /components/ExtGrattisTurboWidget/ExtGrattisTurboWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /components/ExtMgidWidget/ExtMgidWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 2 | Mgid widget 3 | 4 | -------------------------------------------------------------------------------- /components/ExtDrive2PartsOffers/ExtDrive2PartsOffers.examples/default.xml: -------------------------------------------------------------------------------- 1 | 2 |

Где купить

3 |
4 | -------------------------------------------------------------------------------- /server/utils/removeCSP.ts: -------------------------------------------------------------------------------- 1 | const meta = //; 2 | 3 | export default function removeCSP(rawHtml: string): string { 4 | return rawHtml.replace(meta, ''); 5 | } 6 | -------------------------------------------------------------------------------- /components/ExtKommersantWidget/utils/object.ts: -------------------------------------------------------------------------------- 1 | function hasOwnProperty(target: {}, property: string): boolean { 2 | return Object.prototype.hasOwnProperty.call(target, property); 3 | } 4 | 5 | export { hasOwnProperty }; 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.5 4 | * ExtZenWidget 5 | 6 | ## 1.0.4 7 | * ExtEmbed 8 | * ExtStrossleWidget 9 | 10 | ## 1.0.0 11 | ### Новые компоненты 12 | * ExtExchangeRates 13 | * ExtFancyButton 14 | * ExtParagraph 15 | -------------------------------------------------------------------------------- /components/ExtLentainformWidget/ExtLentainformWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 2 | Lentainform widget 3 | 4 | -------------------------------------------------------------------------------- /components/ExtKommersantWidget/ExtKommersantWidget.scss: -------------------------------------------------------------------------------- 1 | .ext-simple-widget__footer { 2 | text-align: center; 3 | } 4 | 5 | .ext-simple-widget__footer-link { 6 | font-size: 15px; 7 | color: #006697; 8 | font-weight: bold; 9 | text-decoration: none; 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | dist 4 | .tmp 5 | .vscodegi 6 | .history 7 | components/**/*.js 8 | components/**/*.map 9 | components/**/*.d.ts 10 | 11 | tools/**/*.js 12 | tools/**/*.map 13 | tools/**/*.d.ts 14 | 15 | server/**/*.js 16 | server/**/*.map 17 | server/**/*.d.ts 18 | -------------------------------------------------------------------------------- /components/ExtKommersantWidget/utils/json.ts: -------------------------------------------------------------------------------- 1 | function isJSON(str: string): boolean { 2 | try { 3 | const result = JSON.parse(str); 4 | return typeof result === 'object' && result !== null; 5 | } catch (e) { 6 | return false; 7 | } 8 | } 9 | 10 | export { isJSON }; 11 | 12 | -------------------------------------------------------------------------------- /components/ExtRbcCovid/commonInterfaces.ts: -------------------------------------------------------------------------------- 1 | interface ICovidStats { 2 | perDay: number; 3 | total: number; 4 | } 5 | type CovidType = 'recovered' | 'infected' | 'died'; 6 | type CovidContent = Record; 7 | 8 | export interface ICovidInfo { 9 | [key: string]: CovidContent; 10 | } 11 | -------------------------------------------------------------------------------- /lints/test/utils.js: -------------------------------------------------------------------------------- 1 | const getCreateSuite = (createErrorMessage, opts = {}) => (code, ...errVarsNames) => ({ 2 | code, 3 | errors: errVarsNames.map(createErrorMessage), 4 | env: { browser: true }, 5 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 6 | ...opts 7 | }); 8 | 9 | module.exports = { getCreateSuite }; 10 | -------------------------------------------------------------------------------- /lints/index.js: -------------------------------------------------------------------------------- 1 | const noAsync = require('./rules/no-async'); 2 | const noUndefinedWindow = require('./rules/no-undefined-window'); 3 | const correctFileExport = require('./rules/correct-file-export'); 4 | 5 | module.exports = { 6 | 'no-async': noAsync, 7 | 'correct-file-export': correctFileExport, 8 | 'no-undefined-window': noUndefinedWindow 9 | }; 10 | -------------------------------------------------------------------------------- /components/ExtTgmEmbed/inlineScript.ts: -------------------------------------------------------------------------------- 1 | export function inlineScript(): void { 2 | window.addEventListener('message', event => { 3 | try { 4 | const data = JSON.parse(event.data); 5 | 6 | if (data.event === 'resize') { 7 | window.parent.postMessage(data, '*'); 8 | } 9 | } catch (e) {} 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /tools/watch-registry.ts: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar'); 2 | const spawn = require('cross-spawn'); 3 | 4 | const watcher = chokidar.watch('components/*', { ignoreInitial: true }); 5 | const buildRegistry = () => spawn('npm', ['run', 'build:registry'], { stdio: 'inherit' }); 6 | 7 | watcher 8 | .on('addDir', buildRegistry) 9 | .on('unlinkDir', buildRegistry); 10 | -------------------------------------------------------------------------------- /server/utils/handleError.ts: -------------------------------------------------------------------------------- 1 | /** Для самой частой ошибки (ошибка в пути) делаем человеко-понятное сообщение */ 2 | const handleErr = (e: NodeJS.ErrnoException, examplePath: string): Error | NodeJS.ErrnoException => { 3 | if (e.code === 'ENOENT') { 4 | return new Error(`Не нашли стаба по адресу: ${examplePath}`); 5 | } 6 | 7 | return e; 8 | }; 9 | 10 | export default handleErr; 11 | -------------------------------------------------------------------------------- /lints/rules/no-async/globals.js: -------------------------------------------------------------------------------- 1 | // Handling only server-side async functions since browser one won't be called anyway 2 | module.exports = new Map([ 3 | [ 4 | 'setTimeout', 5 | true 6 | ], 7 | [ 8 | 'setInterval', 9 | true 10 | ], 11 | [ 12 | 'Promise', 13 | true 14 | ], 15 | [ 16 | 'import', 17 | true 18 | ] 19 | ]); 20 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | The following authors have created the source code of "Turbo components" 2 | published and distributed by YANDEX LLC as the owner: 3 | Maxim Victorov @notary 4 | Roman @werreour 5 | Olga Likhacheva @olliva 6 | Roman Rozhdestvenskiy @sbmaxx 7 | 8 | The following authors have licensed their contributions to YANDEX LLC 9 | and to everyone who uses "Turbo components" under the licensing terms 10 | detailed in LICENSE available at https://github.com/turboext/components/blob/master/LICENSE: 11 | -------------------------------------------------------------------------------- /lints/rules/no-undefined-window/globals.js: -------------------------------------------------------------------------------- 1 | // Using default eslint globals library 2 | const globals = require('globals'); 3 | 4 | // Any window global variable is disallowed 5 | const forbidden = new Map(Object.entries(globals.browser)); 6 | 7 | // Except those are exists in both nodejs and window environments (like console) 8 | const allowed = Object.keys(globals.node); 9 | allowed.forEach(g => forbidden.delete(g)); 10 | 11 | /** Set of globals to search to search */ 12 | module.exports = forbidden; 13 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer as createHttpServer } from 'http'; 2 | import * as express from 'express'; 3 | 4 | import router from './routes'; 5 | 6 | const app = express(); 7 | 8 | app.use('/', router); 9 | app.use('/dist', express.static('dist')); 10 | 11 | const httpServer = createHttpServer(app); 12 | 13 | const HTTP_PORT = 8081; 14 | 15 | /* eslint-disable no-console */ 16 | httpServer.listen(HTTP_PORT, () => { 17 | console.log('http server is listening on', HTTP_PORT); 18 | }); 19 | -------------------------------------------------------------------------------- /components/ExtFancyButton/ExtFancyButton.scss: -------------------------------------------------------------------------------- 1 | .fancy-button { 2 | padding: 15px; 3 | border: 0; 4 | border-radius: 16px; 5 | color: #fff; 6 | background-color: #007bff; 7 | 8 | text-align: center; 9 | 10 | display: block; 11 | 12 | font-size: 15px; 13 | user-select: none; 14 | 15 | transition: 0.2s transform; 16 | 17 | width: 100%; 18 | 19 | margin-top: 20px; 20 | 21 | &:active, &:focus { 22 | outline: none; 23 | border: 0; 24 | } 25 | 26 | &:active { 27 | transform: scale(0.96); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /components/ExtDrive2Gallery/ExtDrive2Gallery.scss: -------------------------------------------------------------------------------- 1 | .d2-gallery { 2 | display: flex; 3 | margin-top: 10px; 4 | overflow-x: auto; 5 | 6 | &__item { 7 | position: relative; 8 | flex: 0 0 33%; 9 | } 10 | 11 | &__item::before { 12 | content: ''; 13 | display: block; 14 | padding-top: 100%; 15 | } 16 | 17 | &__item + &__item { 18 | margin-left: 2px; 19 | } 20 | 21 | &__pic { 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | width: 100%; 26 | height: 100%; 27 | object-fit: cover; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /components/ExtExchangeRates/utils/date.ts: -------------------------------------------------------------------------------- 1 | export function toDoubleDigits(num: number): string { 2 | return num >= 10 ? num.toString() : `0${num}`; 3 | } 4 | 5 | export function getYesterday(): Date { 6 | const today = new Date(); 7 | today.setDate(today.getDate() - 1); 8 | 9 | return today; 10 | } 11 | 12 | export function formatDate(date: Date): string { 13 | const year = date.getFullYear(); 14 | const month = toDoubleDigits(date.getMonth() + 1); 15 | const day = toDoubleDigits(date.getDate()); 16 | 17 | return `${year}-${month}-${day}`; 18 | } 19 | -------------------------------------------------------------------------------- /tools/AfterEmitPlugin.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | module.exports = () => { 4 | return { 5 | apply: compiler => { 6 | compiler.hooks.afterEmit.tap('AfterEmitPlugin', () => { 7 | setTimeout(() => { 8 | // eslint-disable-next-line no-console 9 | console.log('Dev server запущен:'); 10 | // eslint-disable-next-line no-console 11 | console.log(`${chalk.underline('http://localhost:8081')}`); 12 | }, 0); 13 | }); 14 | } 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 YANDEX LLC 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /components/ExtFancyParagraph/ExtFancyParagraph.scss: -------------------------------------------------------------------------------- 1 | .fancy-paragraph { 2 | position: relative; 3 | display: flex; 4 | align-items: center; 5 | flex-flow: column; 6 | padding: 30px 14px; 7 | width: calc(100% + 30px); 8 | left: -15px; 9 | right: -15px; 10 | 11 | margin-top: 20px; 12 | 13 | color: black; 14 | background: rgb(255, 187, 109); 15 | 16 | box-sizing: border-box; 17 | 18 | &__label { 19 | font-size: 18px; 20 | font-weight: bold; 21 | text-align: center; 22 | margin-bottom: 10px; 23 | } 24 | 25 | &__text { 26 | margin-top: 20px; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /components/ExtRuamoOauth/ExtRuamoOauth.scss: -------------------------------------------------------------------------------- 1 | .ext-ruamo-oauth__button { 2 | padding-top: 10px; 3 | padding-bottom: 10px; 4 | border: 1px solid #ff0067; 5 | border-radius: 4px; 6 | color: #000000; 7 | font-weight: bold; 8 | background-color: white; 9 | text-align: center; 10 | display: block; 11 | font-size: 22px; 12 | text-decoration: none; 13 | user-select: none; 14 | transition: 0.2s transform; 15 | width: 100%; 16 | margin-top: 20px; 17 | 18 | &:active, &:focus { 19 | outline: none; 20 | border: 0; 21 | } 22 | 23 | &:active { 24 | transform: scale(0.96); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /components/ExtFancyButton/ExtFancyButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './ExtFancyButton.scss'; 3 | 4 | interface IFancyButtonProps { 5 | color?: string; 6 | className?: string; 7 | onClick?: () => void; 8 | name: string; 9 | children?: React.ReactNode[]; 10 | } 11 | 12 | export function ExtFancyButton({ color, className, onClick, children }: IFancyButtonProps): React.ReactNode { 13 | return ( 14 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /tools/build-registry.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { readdirSync, outputFileSync } from 'fs-extra'; 3 | 4 | const wrapInRegistry = (file: string): string => `window.Registry({'${file}': ` + 5 | `require('../components/${file}/${file}.tsx').${file}});`; 6 | 7 | const componentsRoot = resolve(__dirname, '..', 'components'); 8 | const registryPath = resolve(__dirname, '..', '.tmp', 'registry.tsx'); 9 | 10 | const registry = readdirSync(componentsRoot) 11 | .map((file: string) => wrapInRegistry(file)) 12 | .join('\n'); 13 | 14 | // Инициализируем React-компоненты после всех require'ов 15 | outputFileSync(registryPath, `${registry}\nwindow.Ya.initReact();`); 16 | -------------------------------------------------------------------------------- /components/ExtExchangeRates/ExtExchangeRates.examples/default.xml: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /components/ExtKommersantWidget/ExtKommersantWidget.examples/default.xml: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /components/ExtSmiFmWidget/ExtSmiFmWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ExtEmbed } from '../ExtEmbed/ExtEmbed'; 3 | 4 | interface IProps { 5 | 'data-bid': number; 6 | 'data-height'?: string; 7 | } 8 | 9 | export function ExtSmiFmWidget(props: IProps): React.ReactNode { 10 | const { 11 | 'data-bid': blockId, 12 | 'data-height': height = '300' 13 | } = props; 14 | 15 | const html = ` 16 |
`; 17 | 18 | return ( 19 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/ExtSmiCenterWidget/ExtSmiCenterWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ExtEmbed } from '../ExtEmbed/ExtEmbed'; 3 | 4 | interface IProps { 5 | 'data-bid': number; 6 | 'data-height'?: string; 7 | } 8 | 9 | export function ExtSmiCenterWidget(props: IProps): React.ReactNode { 10 | const { 11 | 'data-bid': blockId, 12 | 'data-height': height = '300' 13 | } = props; 14 | 15 | const html = `
` + 16 | ``; 17 | 18 | return ( 19 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /lints/rules/shared/getSuperclassName.js: -------------------------------------------------------------------------------- 1 | const { isMemberExpression, isIdentifier } = require('./utils'); 2 | 3 | /** Gets the name of extended class */ 4 | const getSuperclassName = ({ superClass }) => { 5 | // React.Component 6 | if (isMemberExpression(superClass)) { 7 | let item = superClass; 8 | // Searching for React in React.SomeModule.Component 9 | while (item.object) { 10 | item = item.object; 11 | } 12 | 13 | return item.name; 14 | } 15 | 16 | // Component 17 | if (isIdentifier(superClass)) { 18 | return superClass.name; 19 | } 20 | 21 | // Any generated names, like function expressions are not handled 22 | return false; 23 | }; 24 | 25 | module.exports = getSuperclassName; 26 | -------------------------------------------------------------------------------- /components/ExtRbcCovid/utils/number.ts: -------------------------------------------------------------------------------- 1 | function formatNumber(num: number): string { 2 | return num.toString().replace(/\./g, ','); 3 | } 4 | 5 | const BILLION = 10 ** 9; 6 | const MILLION = 10 ** 6; 7 | const TEN_THOUSAND = 10 ** 4; 8 | const THOUSAND = 10 ** 3; 9 | 10 | /** 11 | * Функция для отображения общего числа заболевших как в rbc. 12 | * @param num число для форматирования данных как в rbc 13 | */ 14 | export function formatTotal(num: number): string { 15 | if (num >= BILLION) { 16 | return `${formatNumber(num / BILLION)} млрд`; 17 | } else if (num >= MILLION) { 18 | return `${formatNumber(num / MILLION)} млн`; 19 | } else if (num >= TEN_THOUSAND) { 20 | return `${formatNumber(num / THOUSAND)} тыс.`; 21 | } 22 | 23 | return num.toString(); 24 | } 25 | -------------------------------------------------------------------------------- /lints/rules/shared/isReactImportedNames.js: -------------------------------------------------------------------------------- 1 | const { isImportDeclaration, isLiteral } = require('./utils'); 2 | 3 | /** An import from "react" library is considered to react-import */ 4 | const isReactImport = node => isImportDeclaration(node) && isLiteral(node.source) && node.source.value === 'react'; 5 | 6 | /** Traverses through import declarations and detects if superclass was used is an import of react */ 7 | const isReactModuleImport = (node, names) => isReactImport(node) && 8 | node.specifiers.some(({ local: { name } }) => names.includes(name)); 9 | 10 | /** Traverses through all the tree searching for imports and checking them for react */ 11 | const isReactImportedNames = (node, ...importNames) => node.body.some(n => isReactModuleImport(n, importNames)); 12 | 13 | module.exports = isReactImportedNames; 14 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const precss = require('precss'); 2 | const postcssImport = require('postcss-import'); 3 | const postcssNested = require('postcss-nested'); 4 | const postcssAutomath = require('postcss-automath'); 5 | const postcssUrl = require('postcss-url'); 6 | const autoprefixer = require('autoprefixer'); 7 | const postcssReporter = require('postcss-reporter'); 8 | const csswring = require('csswring'); 9 | const postcssScss = require('postcss-scss'); 10 | 11 | const browsers = [ 12 | 'android 4', 13 | 'ios 9', 14 | 'ie 11' 15 | ]; 16 | 17 | const plugins = [ 18 | precss(), 19 | postcssImport(), 20 | postcssNested, 21 | postcssAutomath(), 22 | postcssUrl({ url: 'inline' }), 23 | autoprefixer({ browsers }), 24 | postcssReporter(), 25 | csswring() 26 | ]; 27 | 28 | module.exports = { 29 | plugins, 30 | parser: postcssScss 31 | }; 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "allowSyntheticDefaultImports": true, 5 | "experimentalDecorators": true, 6 | "module": "commonjs", 7 | "target": "esNext", 8 | "lib": ["es7", "dom"], 9 | "sourceMap": true, 10 | "jsx": "react", 11 | "moduleResolution": "node", 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "noEmitOnError": true, 18 | "strictNullChecks": true, 19 | "suppressImplicitAnyIndexErrors": true, 20 | "noUnusedLocals": true, 21 | "downlevelIteration": true, 22 | "baseUrl": ".", 23 | "outDir": "" 24 | }, 25 | "include": [ 26 | "components/**/**.ts", 27 | "components/**/**.tsx", 28 | "server/**/**.ts", 29 | "tools/**/**.ts", 30 | "types/**/*.d.ts" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /server/getHtml.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto'; 2 | import * as rp from 'request-promise'; 3 | 4 | // Save xml data not to make (new) requests 5 | const savedXmls = {}; 6 | 7 | export const getHtml = (xmlData: string, options: rp.RequestPromiseOptions = {}): Promise => { 8 | const hash = createHash('md5').update(xmlData) 9 | .digest('hex'); 10 | if (savedXmls[hash]) { 11 | return Promise.resolve(savedXmls[hash]); 12 | } 13 | 14 | // @ts-ignore broken typings 15 | return rp({ 16 | uri: 'https://turbo-components.yandex.net', 17 | method: 'POST', 18 | headers: { 19 | 'content-type': 'application/json', 20 | ...options.headers || {} 21 | }, 22 | body: JSON.stringify({ xml: xmlData }) 23 | }) 24 | .then(html => { 25 | savedXmls[hash] = html; 26 | return html; 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'stylelint-config-recommended-scss', 3 | plugins: [ 4 | 'stylelint-high-performance-animation' 5 | ], 6 | rules: { 7 | 'plugin/no-low-performance-animation-properties': true, 8 | 'unit-whitelist': ['px', '%', 'rem', 's', 'ms', 'deg', 'vw', 'vh'], 9 | 'selector-max-specificity': '0,4,0', 10 | 'selector-max-type': 0, 11 | 'selector-max-attribute': 0, 12 | 'at-rule-blacklist': ['font-face', 'import'], 13 | 'property-blacklist': [ 14 | 'perspective', 15 | 'backface-visibility', 16 | 'mask', 17 | 'mask-image', 18 | 'mask-border', 19 | 'clip-path' 20 | ], 21 | 'function-url-scheme-whitelist': ['/^\.\//', '/^http/'], 22 | 'function-url-no-scheme-relative': true, 23 | 'selector-class-pattern': '^((?!page__|typo|grid).)*$' 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /lints/rules/shared/getFirstParent.js: -------------------------------------------------------------------------------- 1 | const { isMemberExpression, isUnaryExpression } = require('./utils'); 2 | 3 | /** Gets the first parent to search for correctness from node item */ 4 | const getFirstParent = node => { 5 | // Always node.type === identifier 6 | 7 | // E.g. window.document 8 | if (isMemberExpression(node.parent)) { 9 | let { parent: item } = node; 10 | 11 | // Fetching the highest memberExpression from window.document.somevalue 12 | while (isMemberExpression(item.parent)) { 13 | item = item.parent; 14 | } 15 | 16 | return item; 17 | } 18 | 19 | // E.g. typeof window 20 | if (isUnaryExpression(node.parent)) { 21 | // E.g. Binary expression: typeof window !== undefined 22 | return node.parent; 23 | } 24 | 25 | // AssignmentExpression, VariableDeclaration, Program; whatever, will search for If pattern higher in the scope 26 | return node; 27 | }; 28 | 29 | module.exports = getFirstParent; 30 | -------------------------------------------------------------------------------- /lints/rules/correct-file-export/Readme.md: -------------------------------------------------------------------------------- 1 | # correct-file-export 2 | 3 | Check for correct named exports usage. 4 | 5 | ## Rule Details 6 | 7 | This rule allows only named exports. Export name should be equal to file name. 8 | 9 | Examples of **incorrect** code for this rule: 10 | 11 | ```js 12 | import * as React from 'react' 13 | 14 | export default class asd extends React.PureComponent {} 15 | ``` 16 | ```js 17 | export default function asd () {} 18 | ``` 19 | 20 | ```js 21 | import * as React from 'react' 22 | 23 | class asdPresenter extends React.PureComponent {} 24 | export const asd = asdPresenter; 25 | ``` 26 | 27 | Examples of **correct** code for this rule: 28 | 29 | ```js 30 | import * as React from 'react' 31 | 32 | export class asd extends React.PureComponent {} 33 | ``` 34 | ```js 35 | import * as React from 'react' 36 | 37 | class asd extends React.PureComponent {} 38 | export { asd }; 39 | ``` 40 | ```js 41 | export function asd () {} 42 | ``` 43 | ```js 44 | function asd () {} 45 | export { asd }; 46 | ``` 47 | -------------------------------------------------------------------------------- /lints/rules/shared/isValidBinaryExpression.js: -------------------------------------------------------------------------------- 1 | const { isBinaryExpression, isUnaryExpression } = require('./utils'); 2 | const isCorrectOperator = (operator, expected) => expected.some(e => e === operator); 3 | 4 | /** Being safe means not to throw */ 5 | const isSafeTypeofExpression = ({ type, operator }) => isUnaryExpression(type) && operator === 'typeof'; 6 | 7 | const validBinaryExpressionFactory = 8 | (...expectedOperators) => ({ type, operator, left, right }) => isBinaryExpression(type) && 9 | isCorrectOperator(operator, expectedOperators) && 10 | left.operator === 'typeof' && 11 | left.argument.name === 'window' && 12 | right.value === 'undefined'; 13 | 14 | /** "typeof window !== 'undefined'" */ 15 | const isValidBinary = validBinaryExpressionFactory('!==', '!='); 16 | 17 | /** "typeof window === 'undefined'" */ 18 | const isValidBinaryReverse = validBinaryExpressionFactory('===', '=='); 19 | 20 | module.exports = { 21 | isValidBinary, 22 | isValidBinaryReverse, 23 | isSafeTypeofExpression 24 | }; 25 | -------------------------------------------------------------------------------- /components/ExtExchangeRates/utils/rates.ts: -------------------------------------------------------------------------------- 1 | interface IRawRates { base: string; rates: Record } 2 | 3 | export interface IRate { value: number; sign: number } 4 | export type TRates = Record; 5 | 6 | export function selectRates(ratesList: string[], { base, rates }: IRawRates): TRates { 7 | const baseValue = rates[base]; 8 | 9 | return ratesList.reduce((acc, currency) => { 10 | acc[currency] = { value: baseValue / rates[currency], sign: 0 }; 11 | return acc; 12 | }, {}); 13 | } 14 | 15 | export function diffRates(rate1: TRates, rate2: TRates): TRates { 16 | const result: TRates = {}; 17 | 18 | Object.keys(rate1).forEach(currency => { 19 | if (!rate2[currency]) { 20 | result[currency] = rate1[currency]; 21 | return; 22 | } 23 | 24 | result[currency] = { 25 | value: rate1[currency].value, 26 | sign: rate1[currency].value - rate2[currency].value 27 | }; 28 | }); 29 | 30 | return result; 31 | } 32 | -------------------------------------------------------------------------------- /components/ExtRbcCovid/ExtRbcCovid.examples/default.xml: -------------------------------------------------------------------------------- 1 | 48 | -------------------------------------------------------------------------------- /lints/rules/no-undefined-window/Readme.md: -------------------------------------------------------------------------------- 1 | # no-async 2 | 3 | Forbids usage of browser-only globals on the server. 4 | 5 | ## Rule Details 6 | 7 | This rule usage of browser-only globals when they can throw an error. 8 | 9 | Examples of **incorrect** code for this rule: 10 | 11 | ```js 12 | alert(123); 13 | ``` 14 | ```js 15 | window.document.createElement('div') 16 | ``` 17 | 18 | Examples of **correct** code for this rule: 19 | 20 | ```js 21 | import * as React from 'react' 22 | 23 | export class asd extends React.PureComponent { 24 | componentDidMount() { 25 | alert(123); 26 | } 27 | } 28 | ``` 29 | ```js 30 | let a = 1; 31 | 32 | if (typeof window !== 'undefined') { 33 | alert(a); 34 | } 35 | 36 | ``` 37 | 38 | ```js 39 | const canUseDom = typeof window == 'undefined' && window.document && window.document.createElement; 40 | let a = 1; 41 | 42 | if (canUseDom) { 43 | alert(a); 44 | } 45 | 46 | ``` 47 | 48 | ```js 49 | let a = 1; 50 | 51 | typeof window !== 'undefined' && alert(a) 52 | 53 | ``` 54 | 55 | ```js 56 | let a = 1; 57 | 58 | typeof window === 'undefined' ? console.log(a) : alert(a) 59 | 60 | ``` 61 | -------------------------------------------------------------------------------- /components/ExtGrattisWidget/ExtGrattisWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ExtEmbed } from '../ExtEmbed/ExtEmbed'; 3 | 4 | interface IProps { 5 | 'data-blockid': number; 6 | 'data-width'?: string; 7 | 'data-height'?: string; 8 | } 9 | 10 | export function ExtGrattisWidget(props: IProps): React.ReactNode { 11 | const { 12 | 'data-blockid': blockId, 13 | 'data-width': width = '100%', 14 | 'data-height': height = '250' 15 | } = props; 16 | 17 | const html = ` 18 |
19 | 31 | `; 32 | 33 | return ( 34 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /components/ExtExchangeRates/ExtExchangeRates.scss: -------------------------------------------------------------------------------- 1 | .ext-exchange-rates { 2 | margin-top: 10px; 3 | display: inline-block; 4 | min-height: 64px; 5 | min-width: 100%; 6 | max-width: 100%; 7 | margin-left: -14px; 8 | margin-right: -14px; 9 | padding: 22px 14px; 10 | background-color: #1e1e1e; 11 | 12 | &__description { 13 | color: #8f8e94; 14 | font-size: 14px; 15 | line-height: 16px; 16 | font-style: normal; 17 | font-weight: normal; 18 | margin-top: 8px; 19 | } 20 | 21 | &__wrapper { 22 | width: 100%; 23 | overflow: auto; 24 | white-space: nowrap; 25 | -webkit-overflow-scrolling: touch; 26 | } 27 | 28 | &__rate { 29 | font-size: 16px; 30 | font-weight: bold; 31 | font-style: normal; 32 | 33 | letter-spacing: 1px; 34 | text-transform: uppercase; 35 | color: #fff; 36 | 37 | margin-right: 20px; 38 | 39 | &:last-of-type { 40 | margin-right: 0; 41 | } 42 | } 43 | 44 | &__value_green { 45 | color: #4cda64; 46 | } 47 | 48 | &__value_red { 49 | color: #ff3b2f; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lints/rules/no-async/Readme.md: -------------------------------------------------------------------------------- 1 | # no-async 2 | 3 | Forbids usage of async javascript on the server. 4 | 5 | ## Rule Details 6 | 7 | This rule disallows server-side async javascript functions. 8 | 9 | Examples of **incorrect** code for this rule: 10 | 11 | ```js 12 | setTimeout(() => console.log(123)); 13 | ``` 14 | ```js 15 | new Promise().then(() => {}) 16 | ``` 17 | 18 | ```js 19 | import('some-module').then(/* ... */) 20 | ``` 21 | 22 | Examples of **correct** code for this rule: 23 | 24 | ```js 25 | import * as React from 'react' 26 | 27 | export class asd extends React.PureComponent { 28 | componentDidMount() { 29 | setTimeout(() => { this.setState({a: 1}) }) 30 | } 31 | } 32 | ``` 33 | ```js 34 | let a = 1; 35 | 36 | if (typeof window !== 'undefined') { 37 | setTimeout(() => a = 3); 38 | } 39 | 40 | ``` 41 | 42 | ```js 43 | const canUseDom = typeof window == 'undefined' && window.document; 44 | let a = 1;` 45 | 46 | if (canUseDome) { 47 | setTimeout(() => a = 3); 48 | } 49 | 50 | ``` 51 | 52 | ```js 53 | let a = 1; 54 | 55 | typeof window !== 'undefined' && setTimeout(() => a = 3) 56 | 57 | ``` 58 | 59 | ```js 60 | const promiseLike = typeof window === 'undefined' ? {then(fn) { fn() }} : new Promise(); 61 | ``` 62 | -------------------------------------------------------------------------------- /components/ExtRbcCovid/utils/date.ts: -------------------------------------------------------------------------------- 1 | import { toDoubleDigits } from '../../ExtExchangeRates/utils/date'; 2 | 3 | const months = { 4 | 0: 'январь', 5 | 1: 'февраль', 6 | 2: 'март', 7 | 3: 'апрель', 8 | 4: 'май', 9 | 5: 'июнь', 10 | 6: 'июль', 11 | 7: 'август', 12 | 8: 'сентябрь', 13 | 9: 'октябрь', 14 | 10: 'ноябрь', 15 | 11: 'декабрь' 16 | }; 17 | 18 | const russianTimezones = { 19 | [60 * 2]: 'МСК-1', 20 | [60 * 3]: 'МСК', 21 | [60 * 4]: 'МСК+1', 22 | [60 * 5]: 'МСК+2', 23 | [60 * 6]: 'МСК+3', 24 | [60 * 7]: 'МСК+4', 25 | [60 * 8]: 'МСК+5', 26 | [60 * 9]: 'МСК+6', 27 | [60 * 10]: 'МСК+7', 28 | [60 * 11]: 'МСК+8', 29 | [60 * 12]: 'МСК+9' 30 | }; 31 | 32 | function getTimezone(date: Date): string { 33 | return russianTimezones[-date.getTimezoneOffset()] || 'МСК'; 34 | } 35 | 36 | export function formatDate(date: Date): string { 37 | const month = months[date.getMonth()]; 38 | const day = toDoubleDigits(date.getDate()); 39 | const hours = toDoubleDigits(date.getHours()); 40 | const minutes = toDoubleDigits(date.getMinutes()); 41 | const timezone = getTimezone(date); 42 | 43 | return `${day} ${month}, ${hours}:${minutes} ${timezone}`; 44 | } 45 | -------------------------------------------------------------------------------- /components/ExtRuamoOauth/ExtRuamoOauth.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './ExtRuamoOauth.scss'; 3 | 4 | 5 | export function ExtRuamoOauth(): React.ReactNode { 6 | function label(): string { 7 | if (typeof window !== 'undefined') { 8 | const userAgent = navigator.userAgent.toLowerCase(); 9 | if (userAgent.includes('huawei') || userAgent.includes('honor')) { 10 | return 'Создать анкету'; 11 | } 12 | } 13 | return 'Войти'; 14 | } 15 | 16 | function href(): string { 17 | if (typeof window !== 'undefined') { 18 | const userAgent = navigator.userAgent.toLowerCase(); 19 | if (userAgent.includes('iphone') && userAgent.includes('safari')) { 20 | return 'https://ruamo.ru/oauth/apple'; 21 | } else if (userAgent.includes('android') && userAgent.includes('chrome') && !userAgent.includes('huawei') && !userAgent.includes('honor')) { 22 | return 'https://ruamo.ru/oauth/google'; 23 | } 24 | } 25 | return 'https://ruamo.ru/signup'; 26 | } 27 | 28 | return ( 29 | <> 30 | {label()} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /components/ExtTgmEmbed/ExtTgmEmbed.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { inlineScript } from './inlineScript'; 4 | import { ExtEmbed } from '../ExtEmbed/ExtEmbed'; 5 | 6 | interface IProps { 7 | 'data-post'?: string; 8 | } 9 | 10 | interface IState { 11 | height?: string; 12 | } 13 | 14 | export class ExtTgmEmbed extends React.PureComponent { 15 | public state: IState = {}; 16 | 17 | public render(): React.ReactNode { 18 | const { 19 | 'data-post': postName 20 | } = this.props; 21 | 22 | const html = ` 25 | 26 | `; 27 | 28 | return ( 29 | 35 | ); 36 | } 37 | 38 | private handleMessage = ({ data }: MessageEvent): void => { 39 | if (data.event === 'resize') { 40 | this.setState({ height: `${data.height}px` }); 41 | } 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /components/ExtCapirs/commonInterfaces.ts: -------------------------------------------------------------------------------- 1 | export enum MessageType { 2 | loadingSucceed = 'loading-succeed', 3 | loadingFailed = 'loading-failed', 4 | getLocation = 'get-location', 5 | saveLocation = 'save-location', 6 | } 7 | 8 | export interface IWidgetParams { 9 | begunAutoPad: number; 10 | begunBlockId: number; 11 | height?: number; 12 | width?: number; 13 | json?: Record; 14 | } 15 | 16 | declare global { 17 | // Расширяем существующий интерфейс Window, поэтому он не может начинаться с I - отключаем eslint 18 | // eslint-disable-next-line @typescript-eslint/interface-name-prefix 19 | interface Window { 20 | // AMP-like объект, содержащий location страницы, в которую встроен iframe 21 | context?: { 22 | referrer?: string; 23 | location?: { 24 | href?: string; 25 | }; 26 | }; 27 | 28 | Adf: { 29 | banner: { 30 | ssp: ( 31 | adWrapperID: HTMLElement | null, 32 | sspOptions: Record | undefined, 33 | commonOptions: { 34 | 'begun-auto-pad': number; 35 | 'begun-block-id': number; 36 | } 37 | ) => Promise; 38 | }; 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lints/rules/shared/isGuardedUpper.js: -------------------------------------------------------------------------------- 1 | const { isValidIdentifier, isValidIdentifierReverse } = require('./isValidIdentifier'); 2 | const { isValidBinary, isValidBinaryReverse } = require('./isValidBinaryExpression'); 3 | const { isValidLogicalInner, isValidLogicalInnerReverse } = require('./isValidLogicalInner'); 4 | const { isCondition, isRoot } = require('./utils'); 5 | 6 | /** Returns true if node condition is guarded with "typeof window !== undefined" and statement of node in the consequent block */ 7 | const isValid = ({ test, consequent }, prevItem) => (isValidBinary(test) || 8 | isValidIdentifier(test) || 9 | isValidLogicalInner(test)) && 10 | prevItem === consequent; 11 | 12 | /** Same as function above but mirrored */ 13 | const isValidReverse = ({ test, alternate }, prevItem) => (isValidBinaryReverse(test) || 14 | isValidIdentifierReverse(test) || 15 | isValidLogicalInnerReverse(test)) && 16 | prevItem === alternate; 17 | 18 | /** Checks for typeof window handled upper in the scope */ 19 | const isGuardedUpper = node => { 20 | let item = node; 21 | let prevItem = node; 22 | 23 | while (!isRoot(item.parent)) { 24 | prevItem = item; 25 | item = item.parent; 26 | 27 | if (isCondition(item) && (isValid(item, prevItem) || isValidReverse(item, prevItem))) { 28 | return true; 29 | } 30 | } 31 | 32 | return false; 33 | }; 34 | 35 | module.exports = isGuardedUpper; 36 | -------------------------------------------------------------------------------- /components/ExtFancyParagraph/ExtFancyParagraph.examples/default.xml: -------------------------------------------------------------------------------- 1 | 2 | 26 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, 27 | sed do eiusmod tempor incididunt ut labore et dolore magna 28 | aliqua. Ut enim ad minim veniam, quis nostrud exercitation 29 | ullamco laboris nisi aliquip ex ea commodo consequat. 30 | Duis aute irure dolor in reprehenderit in voluptate velit 31 | esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 32 | occaecat cupidatat non proident, sunt in culpa qui officia 33 | deserunt mollit anim id est laborum. 34 | 35 | -------------------------------------------------------------------------------- /components/ExtDrive2Posts/ExtDrive2Posts.scss: -------------------------------------------------------------------------------- 1 | .d2-posts { 2 | display: flex; 3 | margin-top: 10px; 4 | flex-wrap: wrap; 5 | 6 | &--closed::before { 7 | content: ''; 8 | position: absolute; 9 | bottom: -1px; 10 | left: 0; 11 | width: 100%; 12 | height: 100px; 13 | background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); 14 | pointer-events: none; 15 | } 16 | 17 | & > * { 18 | flex-basis: calc(50% - 5px); 19 | flex-shrink: 1; 20 | min-width: 0; 21 | margin: 10px 0 0 10px; 22 | } 23 | 24 | & > :nth-child(-n+2) { 25 | margin-top: 0; 26 | } 27 | 28 | & > :nth-child(2n+1) { 29 | margin-left: 0; 30 | } 31 | } 32 | 33 | .d2-post { 34 | color: #333; 35 | text-decoration: none; 36 | 37 | &__photo { 38 | position: relative; 39 | border-radius: 6px; 40 | overflow: hidden; 41 | filter: brightness(95%); 42 | } 43 | 44 | &__photo::before { 45 | content: ''; 46 | display: block; 47 | padding-top: 56.25%; 48 | } 49 | 50 | &__pic { 51 | position: absolute; 52 | top: 0; 53 | left: 0; 54 | height: 100%; 55 | width: 100%; 56 | object-fit: cover; 57 | } 58 | 59 | &__title { 60 | font-size: 13px; 61 | font-weight: bold; 62 | line-height: 16px; 63 | margin: 6px 0 0; 64 | } 65 | 66 | &__generation { 67 | margin-top: 5px; 68 | font-size: 12px; 69 | line-height: 14px; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /components/ExtKommersantWidget/utils/elements.ts: -------------------------------------------------------------------------------- 1 | import { MessageType } from '../ExtKommersantWidget'; 2 | 3 | function makeScriptElement(src: string): string { 4 | return ``; 5 | } 6 | 7 | function makeStyleLink(href: string): string { 8 | return ``; 9 | } 10 | 11 | function getWidgetHandlersScript(Id: string): string { 12 | return ``; 32 | } 33 | 34 | export { makeScriptElement, makeStyleLink, getWidgetHandlersScript }; 35 | 36 | -------------------------------------------------------------------------------- /components/ExtStrossleWidget/ExtStrossleWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { ExtEmbed } from '../ExtEmbed/ExtEmbed'; 4 | 5 | import './ExtStrossleWidget.scss'; 6 | 7 | interface IProps { 8 | ['data-widget']: string; 9 | ['data-height']: string; 10 | } 11 | 12 | interface IState { 13 | htmlString: string | null; 14 | } 15 | 16 | const DEFAULT_HEIGHT = '290'; 17 | 18 | export class ExtStrossleWidget extends React.PureComponent { 19 | public readonly state = { htmlString: null }; 20 | 21 | private html = ( 22 |
25 | 37 |
`; 38 | 39 | return ( 40 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /server/utils/preparePage.ts: -------------------------------------------------------------------------------- 1 | import cheerio = require('cheerio'); 2 | 3 | const customComponentStylesheet = ''; 4 | // Выставлен defer, чтобы бандл с локальными компонентами загрузился после вшитого в html 5 | const customComponentScript = ''; 6 | 7 | const preparePage = (rawHtml: string): string => { 8 | /* 9 | * Так как вся страница подготавливается на сервере, и скрипты уже опубликованных кастомных компонентов туда тоже вшиты, 10 | * Появляется проблема, что компоненты, которые приходят с сервера, регистрируются после тех, которые собираются при локальной разработке 11 | * То есть текущие изменения не получается увидеть. Поэтому здесь убрана инициализация компонентов, и перенесена в /tools/build-registry 12 | * Она произойдет после того, когда загрузится локальный бандл с компонентами 13 | */ 14 | const html = rawHtml.replace('window.Ya.initReact();', ''); 15 | const $ = cheerio.load(html); 16 | 17 | /* Вырезаем всех детей у hydro чтобы спокойно зарендерить компоненты с нуля */ 18 | $('.hydro') 19 | .toArray() 20 | .map(component => component.children = []); 21 | 22 | /* Добавляем стили и скрипты всех кастомных компонетов */ 23 | $('.page').append(`${customComponentStylesheet}\n${customComponentScript}`); 24 | 25 | /* Заменяем минифицированную версию реакта на полную (для красивого логирования ошибок) */ 26 | const reactScript = $('script[src$="react-with-dom.min.js"]'); 27 | const reactUrl = reactScript.attr('src').replace('.min', ''); 28 | reactScript.attr('src', reactUrl); 29 | 30 | return $.html(); 31 | }; 32 | 33 | export default preparePage; 34 | -------------------------------------------------------------------------------- /components/ExtDrive2PartsOffers/ExtDrive2PartsOffers.scss: -------------------------------------------------------------------------------- 1 | .d2-offers { 2 | position: relative; 3 | 4 | &--closed::before { 5 | content: ''; 6 | position: absolute; 7 | bottom: -1px; 8 | left: 0; 9 | width: 100%; 10 | height: 100px; 11 | background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); 12 | pointer-events: none; 13 | } 14 | 15 | & * { 16 | box-sizing: border-box; 17 | vertical-align: top; 18 | } 19 | 20 | &__list { 21 | width: 100%; 22 | font-size: 15px; 23 | line-height: 20px; 24 | border-collapse: collapse; 25 | } 26 | 27 | &__item + &__item { 28 | border-top: 1px solid rgba(0, 0, 0, 0.1); 29 | } 30 | 31 | &__caption-col { 32 | position: relative; 33 | width: 75%; 34 | } 35 | 36 | &__marketplace-col { 37 | width: 15%; 38 | } 39 | 40 | &__price-col { 41 | width: 10%; 42 | } 43 | 44 | &__caption { 45 | position: absolute; 46 | width: 100%; 47 | padding: 14px 10px 15px 0; 48 | white-space: nowrap; 49 | color: #333; 50 | overflow: hidden; 51 | text-overflow: ellipsis; 52 | text-decoration: none; 53 | } 54 | 55 | &__marketplace { 56 | display: block; 57 | padding: 14px 10px 15px 27px; 58 | background-position: 0 50%; 59 | background-repeat: no-repeat; 60 | background-size: 20px 20px; 61 | color: #333; 62 | text-decoration: none; 63 | } 64 | 65 | &__price { 66 | display: block; 67 | padding: 10px 0; 68 | text-decoration: none; 69 | } 70 | 71 | &__badge { 72 | display: inline-block; 73 | padding: 5px 15px 4px; 74 | background-color: #5fa03d; 75 | color: #fff; 76 | border-radius: 99px; 77 | font-weight: bold; 78 | white-space: nowrap; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /components/ExtFancyParagraph/ExtFancyParagraph.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './ExtFancyParagraph.scss'; 3 | 4 | interface IFancyParagraphProps { 5 | children: React.ReactChildren; 6 | 'data-title': string; 7 | } 8 | 9 | interface IFancyParagraphState { 10 | isLeft: boolean; 11 | } 12 | 13 | export class ExtFancyParagraph extends React.Component { 14 | public state = { 15 | isLeft: true 16 | } 17 | 18 | public render(): React.ReactNode { 19 | const { children } = this.props; 20 | 21 | // Первым элементом ожидаем exchangeRates 22 | const marquee = React.cloneElement(children[0], { 23 | direction: this.state.isLeft ? 'left' : 'right' 24 | }); 25 | 26 | // Все что будет дальше считаем просто текстом 27 | const text = React.Children.toArray(children).slice(1); 28 | 29 | return ( 30 |
31 |
{this.props['data-title']}
32 | { 33 | 34 | /* 35 | * Несмотря на то, что мы пробрасываем направление, 36 | * сам элемент изменит его только спустя какое-то время 37 | */ 38 | } 39 |
40 | {marquee} 41 |
42 |
43 | {text} 44 |
45 |
46 | ); 47 | } 48 | 49 | public handleClick = () => { 50 | const { isLeft } = this.state; 51 | this.setState({ 52 | isLeft: !isLeft 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /components/ExtEmbed/README.md: -------------------------------------------------------------------------------- 1 | # ExtEmbed 2 | 3 | Компонент ExtEmbed предназначен для встраивания на Турбо-страницу виджетов, 4 | требующих для своей работы загрузку внешних скриптов. 5 | ExtEmbed оборачивает содержимое виджета в безопасную песочницу c iframe. 6 | 7 | ExtEmbed принимает в качестве пропсов: 8 | * html (обязательный) - строка, содержащая html-разметку, которую необходимо поместить в песочницу; 9 | * iframeClass (опциональный) - строка, содержащая css-класс, 10 | который будет установлен тегу iframe для стилизации виджета (например, указания размеров); 11 | * iframeWidth и iframeHeight (опциональные) - строки для установки тегу iframe ширины и высоты соответственно; 12 | * isLoaded (опциональный) - параметр логического типа, с помощью которого ExtEmbed "поймет", что виджет загрузился, 13 | и скроет выводимую поверх виджета паранджу о загрузке. 14 | При отсутствии параметра ExtEmbed будет опираться на событие iframe.onload для скрытия паранджи. 15 | * onMessage (опциональный) - callback-функция, которая будет вызвана при обработке сообщения postMessage 16 | 17 | В случае необходимости обработки сообщений от виджета 18 | можно передать функцию для обработки сообщений в onMessage компонента ExtEmbed для коммуникации. Компонент ExtEmbed подписывается на сообщения в формате [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). Эти сообщения фильтруются и обрабатываются только сообщения от дочерних iframe. При получении таких сообщений вызваются callback-функции, переданные в пропс `onMessage`. Для обработки на стороне виджета нужно сделать 1 общий обработчик, который внутри будет рабирать сообщени по типам. Этот обработчик следует передавать в `onMessage` компонента ExtEmbed. Пример использования можно найти [тут](https://github.com/turboext/components/blob/master/components/ExtDirectadvertWidget/ExtDirectadvertWidget.tsx) -------------------------------------------------------------------------------- /components/ExtGrattisTurboWidget/ExtGrattisTurboWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ExtEmbed } from '../ExtEmbed/ExtEmbed'; 3 | 4 | interface IProps { 5 | 'data-blockid': number; 6 | 'data-width'?: string; 7 | 'data-height'?: string; 8 | } 9 | 10 | const canUseDom = typeof window !== 'undefined' && Boolean(window.document); 11 | 12 | export function ExtGrattisTurboWidget(props: IProps): React.ReactNode { 13 | const { 14 | 'data-blockid': blockId, 15 | 'data-width': width = '100%', 16 | 'data-height': height = '559' 17 | } = props; 18 | 19 | const KEY = 'grattisWidgets'; 20 | const URL_WIDGET_JS = '//cdn-widget.grattis.ru/widget-turbo.min.js'; 21 | 22 | 23 | let title = ''; 24 | let url = ''; 25 | 26 | if (canUseDom) { 27 | const H1 = document.getElementsByTagName('h1'); 28 | 29 | if (H1 && H1.length > 0) { 30 | title = H1[0].innerText; 31 | } else { 32 | ({ title } = document); 33 | } 34 | 35 | url = location ? `${location.protocol}//${location.host}${location.pathname}` : ''; 36 | } 37 | 38 | const html = canUseDom ? ` 39 |
40 | 52 | ` : ''; 53 | 54 | return ( 55 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /lints/test/no-async.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require('eslint'); 2 | 3 | const { getCreateSuite } = require('./utils'); 4 | const rule = require('../rules/no-async'); 5 | 6 | const ruleTester = new RuleTester(); 7 | 8 | const createErrorMessage = varName => `Variable [${varName}] should be used only in safe react lifecycle methods`; 9 | const createSuite = getCreateSuite(createErrorMessage); 10 | 11 | ruleTester.run('no-undefined-window', rule, { 12 | valid: [ 13 | // React safe lifecycle and namespace import 14 | createSuite('import * as React from \'react\'; class myComponent extends React.PureComponent' + 15 | ' { componentDidMount() { setTimeout(() => console.log(123), 123) } }'), 16 | 17 | // React safe lifecycle and namespace import 18 | createSuite('function asyncFn(setTimeout) {setTimeout(() => alert(2), 100)}') 19 | ], 20 | invalid: [ 21 | // React unsafe lifecycle and namespace import — setTimeout 22 | createSuite('import * as React from \'react\'; class myComponent extends React.PureComponent' + 23 | ' { render() { setTimeout(() => console.log(123), 123) } }', 'setTimeout'), 24 | 25 | // React unsafe lifecycle and namespace import - setInterval 26 | createSuite('import * as React from \'react\'; class myComponent extends React.PureComponent' + 27 | ' { constructor() { setInterval(() => console.log(123), 123) } }', 'setInterval'), 28 | 29 | // React unsafe lifecycle and namespace import - new Promise 30 | createSuite('import * as React from \'react\'; class myComponent extends React.PureComponent' + 31 | ' { getDerivedStateFromProps() { new Promise(\'a\') } }', 'Promise'), 32 | 33 | // React unsafe lifecycle and namespace import - Promise.resolve() 34 | createSuite('import * as React from \'react\'; class myComponent extends React.PureComponent' + 35 | ' { render() { Promise.resolve() } }', 'Promise') 36 | ] 37 | }); 38 | -------------------------------------------------------------------------------- /components/ExtDrive2Gallery/ExtDrive2Gallery.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './ExtDrive2Gallery.scss'; 3 | 4 | interface IProps { 5 | children: React.ReactChildren; 6 | 'data-source': string; 7 | } 8 | 9 | interface IPic { 10 | thumbnail: { 11 | url: string; 12 | width: number; 13 | height: number; 14 | size: [number, number]; 15 | primaryColor?: string; 16 | htmlPrimaryColor?: string; 17 | }; 18 | image: { 19 | url: string; 20 | width: number; 21 | height: number; 22 | size: [number, number]; 23 | primaryColor?: string; 24 | htmlPrimaryColor?: string; 25 | }; 26 | isSuggestion: boolean; 27 | } 28 | 29 | interface IState { 30 | pics: IPic[]; 31 | } 32 | 33 | export class ExtDrive2Gallery extends React.Component { 34 | public state = { 35 | pics: [] 36 | } 37 | 38 | private static renderPic(pic: IPic): React.ReactNode { 39 | return ( 40 | 41 | 45 | 46 | ); 47 | } 48 | 49 | public componentDidMount(): void { 50 | const source = this.props['data-source']; 51 | if (typeof window !== 'undefined') { 52 | fetch(source) 53 | .then(res => res.json()) 54 | .then(pics => this.setState({ pics })) 55 | .catch(() => {}); // eslint-disable-line no-empty-function 56 | } 57 | } 58 | 59 | public render(): React.ReactNode { 60 | if (this.state.pics.length === 0) { 61 | return null; 62 | } 63 | 64 | return ( 65 | <> 66 | {this.props.children} 67 |
68 | {this.state.pics.map(ExtDrive2Gallery.renderPic)} 69 |
70 | 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /components/ExtEmbed/ExtEmbed.scss: -------------------------------------------------------------------------------- 1 | @keyframes turbo-loading-dots { 2 | 0% { 3 | opacity: .4; 4 | } 5 | 6 | 5% { 7 | opacity: 1; 8 | } 9 | 10 | 25% { 11 | opacity: 1; 12 | } 13 | 14 | 33% { 15 | opacity: .4; 16 | } 17 | } 18 | 19 | .ext-embed { 20 | position: relative; 21 | 22 | max-width: 100%; 23 | 24 | overflow-x: hidden; 25 | 26 | &__iframe { 27 | overflow: hidden; 28 | 29 | border: none; 30 | } 31 | 32 | &__loader { 33 | position: absolute; 34 | z-index: 1; 35 | top: 0; 36 | right: 0; 37 | bottom: 0; 38 | left: 0; 39 | 40 | background: #f5f5f5; 41 | } 42 | 43 | &__loader-inner { 44 | position: absolute; 45 | top: 50%; 46 | left: 50%; 47 | 48 | transform: translate(-50%, -50%); 49 | } 50 | 51 | &__loader-text { 52 | font-size: 11px; 53 | font-style: normal; 54 | line-height: 13px; 55 | white-space: nowrap; 56 | letter-spacing: .5px; 57 | text-transform: uppercase; 58 | text-overflow: ellipsis; 59 | 60 | color: #999; 61 | } 62 | 63 | &__loader-dots { 64 | position: relative; 65 | 66 | height: 4px; 67 | margin-top: 5px; 68 | } 69 | 70 | &__loader-dot { 71 | position: absolute; 72 | 73 | left: 50%; 74 | 75 | width: 4px; 76 | height: 4px; 77 | 78 | opacity: .4; 79 | 80 | border-radius: 2px; 81 | background-color: #999; 82 | 83 | transform: translate(-50%, 0); 84 | 85 | animation-name: turbo-loading-dots; 86 | animation-duration: 1s; 87 | animation-timing-function: linear; 88 | animation-iteration-count: infinite; 89 | 90 | &:nth-child(1) { 91 | margin-left: -8px; 92 | 93 | animation-delay: -.5s; 94 | } 95 | 96 | &:nth-child(2) { 97 | animation-delay: -.25s; 98 | } 99 | 100 | &:nth-child(3) { 101 | margin-left: 8px; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lints/rules/shared/isSafeLogicalExpression.js: -------------------------------------------------------------------------------- 1 | 2 | const { isValidBinary, isValidBinaryReverse } = require('./isValidBinaryExpression'); 3 | const { isValidLogicalInner, isValidLogicalInnerReverse } = require('./isValidLogicalInner'); 4 | const { isValidIdentifier, isValidIdentifierReverse } = require('./isValidIdentifier'); 5 | const { isConjunction, isDisjunction, isLogicalExpression, getLogicalExpressionParent } = require('./utils'); 6 | 7 | /** Checks the node is the right part of logical statement */ 8 | const isRight = node => node.parent.right === node; 9 | 10 | /** Considering node valid if test is the left part guards the right part */ 11 | const isValid = (node, parent) => (isValidBinary(parent.left) || 12 | isValidLogicalInner(parent.left) || 13 | isValidIdentifier(parent.left)) && 14 | isConjunction(parent) && isRight(node); 15 | 16 | /** Mirrored version of upper function */ 17 | const isValidReverse = (node, parent) => (isValidBinaryReverse(parent.left) || 18 | isValidLogicalInnerReverse(parent.left) || 19 | isValidIdentifierReverse(parent.left)) && 20 | isDisjunction(parent) && isRight(node); 21 | 22 | /** Safe logical is the first function to call on node */ 23 | const isValidLogical = node => { 24 | const { item, parent } = getLogicalExpressionParent(node); 25 | 26 | if (!isLogicalExpression(parent)) { 27 | return false; 28 | } 29 | 30 | /* 31 | * If is not valid still can be guarded by upper logical expression: 32 | * E.g. window.document.title || window.savedTitle will be correct if an expression is guarded with conjunction, 33 | * e.g. typeof window !== undefined && (window.document.title || window.savedTitle). 34 | * Which can be done by the function itself recursively. 35 | */ 36 | 37 | return isValid(item, parent) || isValidLogical(parent); 38 | }; 39 | 40 | const isValidLogicalReverse = node => { 41 | const { item, parent } = getLogicalExpressionParent(node); 42 | 43 | if (!isLogicalExpression(parent)) { 44 | return false; 45 | } 46 | 47 | return isValidReverse(item, parent) || isValidLogicalReverse(parent); 48 | }; 49 | 50 | const isSafeLogicalExpression = node => isValidLogical(node) || isValidLogicalReverse(node); 51 | 52 | module.exports = { isSafeLogicalExpression }; 53 | -------------------------------------------------------------------------------- /lints/rules/no-undefined-window/index.js: -------------------------------------------------------------------------------- 1 | /** @module Eslint plugin to check if window is checked for existence in scope */ 2 | 3 | const globals = require('./globals'); 4 | 5 | const { isSafeTypeofExpression } = require('../shared/isValidBinaryExpression'); 6 | const { isSafeLogicalExpression } = require('../shared/isSafeLogicalExpression'); 7 | const isGuardedUpper = require('../shared/isGuardedUpper'); 8 | const isSafeReactMethod = require('../shared/isSafeReactMethod'); 9 | 10 | const getFirstParent = require('../shared/getFirstParent'); 11 | 12 | module.exports = { 13 | create(context) { 14 | const checkIsSafe = ({ identifier: node }) => { 15 | const startFrom = getFirstParent(node); 16 | 17 | // From fastest to slowest 18 | 19 | // Typeof window 20 | if (isSafeTypeofExpression(startFrom) || 21 | // Typeof window !== undefined && document 22 | isSafeLogicalExpression(startFrom) || 23 | // ComponentDidMount() { alert('mounted!' } 24 | isSafeReactMethod(startFrom) || 25 | // If (typeof window !== 'undefined') {alert('window is defined!')} 26 | isGuardedUpper(startFrom)) { 27 | return; 28 | } 29 | context.report({ 30 | message: `Variable [${node.name}] should be protected via (typeof window !== 'undefined')`, 31 | node 32 | }); 33 | }; 34 | 35 | return { 36 | Program() { 37 | // Get the context of the program 38 | const scope = context.getScope(); 39 | // Find window variable 40 | scope.variables.forEach(variable => { 41 | if (!variable.defs.length && globals.has(variable.name)) { 42 | variable.references.forEach(checkIsSafe); 43 | } 44 | }); 45 | } 46 | }; 47 | }, 48 | 49 | meta: { 50 | docs: { 51 | category: 'Turbo Custom Components custom lints', 52 | description: 'Disallow usage of window in node.js and browser environments without typeof guard', 53 | url: 'https://github.com/turboext/ugc/tree/master/lints/eslint/no-undefined-window/Readme.md' 54 | }, 55 | schema: [] 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /tools/lint-filesystem.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { readdir, lstatSync } from 'fs-extra'; 3 | import pascalCase = require('pascal-case'); 4 | import chalk from 'chalk'; 5 | 6 | const { yellow, green, cyanBright: cyan, redBright: red } = chalk; 7 | 8 | const componentsRoot = resolve(__dirname, '..', 'components'); 9 | const getErrs = () => readdir(componentsRoot) 10 | .then(dirs => { 11 | const errs: string[] = []; 12 | 13 | const promises = dirs.map((dir: string): Promise => { 14 | const dirName = resolve(componentsRoot, dir); 15 | 16 | if (!lstatSync(dirName).isDirectory()) { 17 | errs.push(`expected ${green(dirName)} to be a ${yellow('directory')}`); 18 | return Promise.resolve(); 19 | } 20 | 21 | const pascal = pascalCase(dir); 22 | if (pascal !== dir) { 23 | errs.push(`expected ${green(dir)} component to be named using pascal-case, suggestion: ${yellow(pascal)}`); 24 | return Promise.resolve(); 25 | } 26 | 27 | if (!dir.startsWith('Ext')) { 28 | errs.push(`expected ${green(dir)} component to start with Ext, suggestion: ${green(`Ext${dir}`)}`); 29 | return Promise.resolve(); 30 | } 31 | 32 | const expected = `${dir}.tsx`; 33 | const checkFileExistance = (file: string) => file === expected; 34 | 35 | return readdir(dirName) 36 | .then(files => files.some(checkFileExistance)) 37 | .then(isExists => { 38 | if (!isExists) { 39 | errs.push(`expected to find ${green(expected)} in ${yellow(dirName)}`) 40 | } 41 | }) 42 | }); 43 | 44 | return Promise.all(promises) 45 | .then(() => errs); 46 | }); 47 | 48 | /** 49 | * Index errors for better reporting 50 | * @param error - string representing an error 51 | * @param index - its index it array 52 | */ 53 | const indexify = (error: string, index: number) => `${cyan(`${index + 1})`)} ${error}`; 54 | 55 | getErrs().then(errs => { 56 | if (!errs.length) { 57 | return; 58 | } 59 | 60 | console.log(red('\nSome directory linting errors were found:')); 61 | const result = errs.map(indexify).join('\n'); 62 | console.log(result); 63 | 64 | process.exit(1); 65 | }); 66 | -------------------------------------------------------------------------------- /lints/rules/no-async/index.js: -------------------------------------------------------------------------------- 1 | const globals = require('./globals'); 2 | 3 | const { isSafeTypeofExpression } = require('../shared/isValidBinaryExpression'); 4 | const { isSafeLogicalExpression } = require('../shared/isSafeLogicalExpression'); 5 | const isGuardedUpper = require('../shared/isGuardedUpper'); 6 | const isSafeReactMethod = require('../shared/isSafeReactMethod'); 7 | 8 | const getFirstParent = require('../shared/getFirstParent'); 9 | 10 | module.exports = { 11 | create(context) { 12 | const checkIsSafe = ({ identifier: node }) => { 13 | const startFrom = getFirstParent(node); 14 | 15 | // From fastest to slowest 16 | 17 | // Typeof window 18 | if (isSafeTypeofExpression(startFrom) || 19 | // Typeof window !== undefined && document 20 | isSafeLogicalExpression(startFrom) || 21 | // ComponentDidMount() { alert('mounted!' } 22 | isSafeReactMethod(startFrom) || 23 | // If (typeof window !== 'undefined') {alert('window is defined!')} 24 | isGuardedUpper(startFrom)) { 25 | return; 26 | } 27 | context.report({ 28 | message: `Variable [${node.name}] should be protected via (typeof window !== 'undefined')`, 29 | node 30 | }); 31 | }; 32 | 33 | return { 34 | Program() { 35 | // Get the context of the program 36 | const scope = context.getScope(); 37 | scope.variables.forEach(variable => { 38 | if (!variable.defs.length && globals.has(variable.name)) { 39 | variable.references.forEach(checkIsSafe); 40 | } 41 | }); 42 | 43 | // Report variables not declared at all 44 | scope.through.forEach(reference => { 45 | if (globals.has(reference.identifier.name)) { 46 | checkIsSafe(reference); 47 | } 48 | }); 49 | } 50 | }; 51 | }, 52 | 53 | meta: { 54 | docs: { 55 | category: 'Turbo Custom Components custom lints', 56 | description: 'Disallow usage of window in node.js and browser environments without typeof guard', 57 | url: 'https://github.com/turboext/ugc/tree/master/lints/eslint/no-undefined-window/Readme.md' 58 | }, 59 | schema: [] 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /components/ExtExchangeRates/ExtExchangeRates.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { formatDate } from './utils/date'; 3 | 4 | import './ExtExchangeRates.scss'; 5 | 6 | interface IProps { 7 | 'data-before': Record; 8 | 'data-today': Record; 9 | 'data-date': number; 10 | 'data-supply': string; 11 | direction?: 'left' | 'right'; 12 | } 13 | 14 | interface IRate { 15 | name: string; 16 | value: number; 17 | sign: number; 18 | } 19 | 20 | interface IState { 21 | rates: IRate[]; 22 | } 23 | 24 | export class ExtExchangeRates extends React.PureComponent { 25 | public state = { 26 | rates: [] 27 | }; 28 | 29 | public static getDerivedStateFromProps(props: IProps): IState { 30 | const keys = Object.keys(props['data-today']); 31 | 32 | const rates: IRate[] = keys.map((key: string) => ({ 33 | name: key, 34 | value: 1 / props['data-today'][key], 35 | sign: Math.sign(props['data-today'][key] - props['data-before'][key]) 36 | })); 37 | 38 | return { rates }; 39 | } 40 | 41 | public render(): JSX.Element { 42 | return ( 43 |
44 | { this.renderRates() } 45 |
46 | Курсы валют на {formatDate(new Date())}
47 | по данным {this.props['data-supply']} 48 |
49 |
50 | ); 51 | } 52 | 53 | private renderRates(): JSX.Element { 54 | const { direction = 'left' } = this.props; 55 | 56 | return ( 57 |
58 | {/* Workaround для marquee, так как он не поддержан в typings для react */} 59 | {React.createElement('marquee', { scrolldelay: 1, direction }, this.state.rates.map(this.renderCurrency))} 60 |
61 | ); 62 | } 63 | 64 | private renderCurrency(rate: IRate): JSX.Element { 65 | const color = rate.sign >= 0 ? 'green' : 'red'; 66 | 67 | return ( 68 | 72 | {rate.name}  73 | 76 | {rate.value.toFixed(2)} 77 | 78 | 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /components/ExtInfoxSgWidget/ExtInfoxSgWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | import './ExtInfoxSgWidget.scss'; 4 | import { ExtEmbed } from '../ExtEmbed/ExtEmbed'; 5 | 6 | interface IState { 7 | htmlString: string; 8 | } 9 | 10 | interface IComponentProps { 11 | 'data-script-id': string; 12 | 'data-height': string; 13 | 'data-width'?: string; 14 | } 15 | 16 | function inlineScript(document: Document, scriptId: string): void { 17 | if (typeof window !== 'undefined') { 18 | const n = `infoxContextAsyncCallbacks${scriptId}`; 19 | window[n] = window[n] || []; 20 | window[n].push(() => { 21 | window[`INFOX${scriptId}`].renderTo(`infox_${scriptId}`); 22 | }); 23 | const script = document.createElement('script'); 24 | script.type = 'text/javascript'; 25 | script.src = `//rb.infox.sg/infox/${scriptId}?from=turbo`; 26 | script.async = true; 27 | document.head.appendChild(script); 28 | } 29 | } 30 | 31 | export class ExtInfoxSgWidget extends React.PureComponent { 32 | public readonly state: IState = { 33 | htmlString: '' 34 | }; 35 | 36 | public componentDidMount(): void { 37 | if (typeof window === 'undefined') { 38 | return; 39 | } 40 | this.composeHtmlString(); 41 | } 42 | 43 | public render(): React.ReactNode { 44 | if (!this.state.htmlString) { 45 | return null; 46 | } 47 | return ( 48 | 54 | ); 55 | } 56 | 57 | private composeHtmlString(): void { 58 | if (typeof window !== 'undefined') { 59 | const { 'data-script-id': scriptId } = this.props; 60 | 61 | const html = ( 62 |
63 |
64 | {/* eslint-disable-next-line */} 65 | ` }); 79 | } 80 | 81 | private initDimensions(): void { 82 | const { 'data-width': width, 'data-height': height } = this.props; 83 | this.setState({ 84 | width: width || DEFAULT_WIDTH, 85 | height: height || DEFAULT_HEIGHT 86 | }); 87 | } 88 | 89 | private handlePostMessage(event: MessageEvent): void { 90 | const message = event.data; 91 | 92 | if (message.type) { 93 | if (message.type === 'svk-resize') { 94 | if (message.data !== this.state.height) { 95 | this.setState({ height: message.data }); 96 | } 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /components/ExtSocialMartWidget/ExtSocialMartWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { ExtEmbed } from '../ExtEmbed/ExtEmbed'; 4 | 5 | interface IProps { 6 | ['data-widget-id']: string; 7 | ['data-widget-src']?: string; 8 | ['data-model-id']?: string; 9 | ['data-render']?: string; 10 | ['data-search']?: string; 11 | ['data-sm-source']?: string; 12 | ['data-sm-source-target']?: string; 13 | ['data-category-ids']?: string; 14 | ['data-vendor-ids']?: string; 15 | ['data-matrix-cols']?: number; 16 | ['data-matrix-rows']?: number; 17 | ['data-matrix-dimension']?: string; 18 | 19 | /** 20 | * Ширина для тега iframe 21 | */ 22 | ['data-iframe-width']?: string; 23 | 24 | /** 25 | * Высота для тега iframe 26 | */ 27 | ['data-iframte-height']?: string; 28 | } 29 | 30 | export class ExtSocialMartWidget extends React.PureComponent { 31 | public static defaultProps = { 32 | 'data-render': 'js', 33 | 'data-widget-src': '//widget.socialmart.ru/init.php' 34 | }; 35 | 36 | public readonly state = { htmlString: null }; 37 | 38 | public componentDidMount(): void { 39 | if (typeof window !== 'undefined') { 40 | const tempDiv = document.createElement('div'); 41 | render(this.html(), tempDiv, () => { 42 | this.setState({ htmlString: tempDiv.innerHTML }); 43 | }); 44 | } 45 | } 46 | 47 | public render(): React.ReactNode { 48 | if (!this.state.htmlString) { 49 | return null; 50 | } 51 | 52 | return ( 53 | 59 | ); 60 | } 61 | 62 | private html(): JSX.Element { 63 | return ( 64 | <> 65 |
78 | 88 |