Random changes for many files.

Signed-off-by: Pavel Kirilin <win10@list.ru>
This commit is contained in:
2021-12-06 02:16:06 +04:00
commit 791f73f2c6
36 changed files with 14336 additions and 0 deletions

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

16
.eslintrc.js Normal file
View File

@ -0,0 +1,16 @@
module.exports = {
root: true,
env: {
browser: true,
node: true
},
extends: [
'@nuxtjs/eslint-config-typescript',
'plugin:nuxt/recommended',
'prettier'
],
plugins: [
],
// add your custom rules here
rules: {}
}

90
.gitignore vendored Normal file
View File

@ -0,0 +1,90 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

3
Caddyfile Normal file
View File

@ -0,0 +1,3 @@
blog.local {
reverse_proxy /* http://localhost:3000
}

69
README.md Normal file
View File

@ -0,0 +1,69 @@
# blog
## Build Setup
```bash
# install dependencies
$ yarn install
# serve with hot reload at localhost:3000
$ yarn dev
# build for production and launch server
$ yarn build
$ yarn start
# generate static project
$ yarn generate
```
For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org).
## Special Directories
You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality.
### `assets`
The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets).
### `components`
The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components).
### `layouts`
Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts).
### `pages`
This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing).
### `plugins`
The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins).
### `static`
This directory contains your static files. Each file inside this directory is mapped to `/`.
Example: `/static/robots.txt` is mapped as `/robots.txt`.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static).
### `store`
This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store).

47
components/BottomNav.vue Normal file
View File

@ -0,0 +1,47 @@
<template>
<div class="flex justify-between">
<NuxtLink
v-if="prev"
:to="{ name: 'blog-slug', params: { slug: prev.slug } }"
class="font-bold text-primary hover:underline"
>
{{ prev.title }}
</NuxtLink>
<span v-else>&nbsp;</span>
<NuxtLink
v-if="next"
:to="{ name: 'blog-slug', params: { slug: next.slug } }"
class="font-bold hover:underline"
>
{{ next.title }}
</NuxtLink>
<span v-else>&nbsp;</span>
</div>
</template>
<script>
import { defineComponent } from '@nuxtjs/composition-api'
export default defineComponent({
setup({ prev = null, next = null }) {
console.log(prev)
console.log(next)
return {
prev,
next,
}
},
})
// export default {
// props: {
// prev: {
// type: Object,
// default: () => null,
// },
// next: {
// type: Object,
// default: () => null,
// },
// },
// }
</script>

13
components/Footer.vue Normal file
View File

@ -0,0 +1,13 @@
<template>
<footer class="p-5">
<div class="content has-text-centered">
<p>
<strong>Dev blog</strong> by
<a href="https://github.com/s3rius/">@s3rius</a>.
Built with and <a href="https://nuxtjs.org/">nuxt</a>.
</p>
</div>
</footer>
</template>
<script></script>

162
content/ru/docker-envs.md Normal file
View File

@ -0,0 +1,162 @@
---
title: Разделение докера на среды.
description: Как работать с несколькими docker-compose.
position: 3
category: DevOps
---
# Разделение докера на среды
Должен ли ты разделять среды докера в несколько `docker-compose` файлов?
Определенно! В некоторых случаях невозможно разобраться что разработчики хотели сделать или почему ничего не работает. Настройка раздельных сред может стать настоязей мешанино. В этой статье я покажу как настроить `docker-compose` и избежать миллиона проблем.
Как мы вообще можем разделить среды для локальной разработки и продовые?
Отет прост: Требуется декомпозировать проект и создать отдельные файлы под каждую из сред или даже сервисов.
Это нормально, если у тебя будет больше 2-ч `docker-compose` файлов.
Например:
```
deploy
├── docker-compose.autotests.yml
├── docker-compose.db.yml
├── docker-compose.dev.yml
├── docker-compose.yml
├── dockerfiles
│   ├── api.Dockerfile
│   └── front.Dockerfile
└── scripts
   ├── start-autotests.sh
   ├── start-backend.sh
   └── start-migrations.sh
```
### Как это работает?
Докер умеет работать с множеством `docker-compose` файлов одновременно.И мы можем использовать это для разделения сред.
Выглядит это следующим образом.
```bash
docker-compose \
-f "deploy/docker-compose.yml" \
-f "deploy/docker-compose.db.yml" \
-f "deploy/docker-compose.dev.yml" \
up
```
В каждом из этих файлов определен какой-то кусок конфигурации, который не пересекается. Например в `docker-compose.yml` определено приложение и некоторые необходимые сервисы, а в остальных файлах добавляются сервисы или меняются значения предыдущих.
Наверное, тут проще на примере пояснить.
Допустим у нас есть проект, у которого поднимается бекенд с параметрами, которые отличаются на проде и локально.
Для простоты создадим простецикий проект со следующей структурой.
```
proj
├── deploy
│   ├── docker-compose.yml
│   └── dockerfiles
│       └── script.Dockerfile
└── script.py
```
Для начала напишем скрипт, который будет в центре всего.
```python{}[script.pt]
from sys import argv # это аргуметы переданные в скрипт
def main():
print("; ".join(argv[1:])) # выводитна экран все аргументы программы
if __name__ == "__main__":
main()
```
После того, как скрипт готов и отлажен давайте завернем его в докер,
создав `Dockerfile`.
```dockerfile{}[deploy/dockerfiles/script.Dockerfile]
from python:3.8 # asd
WORKDIR /app
COPY script.py /app/script.py
CMD python /app/script.py
```
Как вы видите, Dockerfile очень прост. Теперь добавим главный `docker-compose.yml`.
```yaml{}[deploy/docker-compose.yml]
---
version: '3.7'
services:
script:
build: # Собираем приложение используя наш dockerfile.
dockerfile: ./deploy/dockerfiles/script.Dockerfile
context: .
# Запускаем его с командой для продового запуска.
command: python script.py this is prod
```
Теперь запустим всё это чудо.
```bash
d-test docker-compose \
-f "./deploy/docker-compose.yml" \
--project-directory "." \
run --rm script
```
Вот что будет выведено на экран.
```log
Creating d-test_script_run ... done
this; is; prod
```
Как мы видим, на экран вывелось сообщение, которое мы указали в нашем `docker-compose.yml`
А теперь для локальной разработки мы не будем ничего менять в нашем файле композиции, а создадим новый рядом.
```yaml{}[deploy/docker-compose.dev.yml]
---
version: '3.7'
services:
script:
command: python script.py this is dev
```
Теперь добавим ещё один файл в нашу команду запуска.
```bash
d-test docker-compose \
-f "deploy/docker-compose.yml" \
-f "deploy/docker-compose.dev.yml" \
--project-directory "." \
run --rm script
```
Вот что будет выведено на экран:
```log
Creating d-test\_script\_run ... done
this; is; dev
```
Как можно заметить, конфигурация запуска перезаписалась в порядке вызова.
То есть, каждый последующий файл композиции может добавлять сервисы и **частично** изменять конфигурацию предыдущих.
Итоговая структура проекта:
```
proj
├── deploy
│   ├── docker-compose.dev.yml
│   ├── docker-compose.yml
│   └── dockerfiles
│       └── script.Dockerfile
└── script.py
```
### Где это применимо?
Ну, в любом проекте, сложнее того, который мы рассмотрели. Потому что в реальной жизни не всё так радужно и локальная версия приложения может отличаться не только параметрами запуска, но и целыми сервисами, которые требуются для локальной копии приложения.

132
content/ru/makefiles.md Normal file
View File

@ -0,0 +1,132 @@
---
title: Makefiles для чайников.
description: Автоматизируем по старинке.
position: 1
category: 'DevOps'
---
# Makefiles для чайников
Если описывать в двух словах, то это просто описание команд для упрощения работы с проектом. Изначально делались для того, чтобы удобно компилировать всякие проекты на любых языках.
## Как они работают
Достаточно просто.
Вот пример `Makefile`:
```makefile{}[Makefile]
# Сгенерируем простой файл
test.gen:
@echo 'echo "Hi!"' > "test.gen"
# Команда для запуска файла test.gen
.PHONY: run
run: test.gen
sh "test.gen"
```
До двоеточий обазначены команды (tagets). Например тут это: `test.gen` и `run`.
Данные таргеты можно запускать через `make ${target}`.
Например, если ввести `make run` в папке с `Makefile`, то мы получим следующее:
```console
$ sh "test.gen"
Hi!
```
Как видно из выхлопа данной команды, у нас успешно запустился файл `test.gen`, хотя мы не запускали команду `make test.gen`. Что произошло? Давайте разбираться.
## Зависимости таргетов
На строчке объявления таргета `run` видно, что объявлен `test.gen`. Это зависимость данного таргета и она будет вызвана до того, как выполнится скрипт описываемого таргета. Таких зависимостей может быть много, перечисляются они чере пробел.
Например:
```makefile{}[Makefile]
.PHONY: target1
target1:
echo "1"
.PHONY: target2
target2: target1
echo "2"
.PHONY: target3
target3: target1 target2
echo "memes"
```
При вызове `make target3` будет выведено:
```console
$ make target3
echo "1"
1
echo "2"
2
echo "memes"
memes
```
Как можно видеть, он построил граф зависимостей и не выполнил `target1` дважды.
## Сокрытие вывода команд
В предыдущем примере можно заметить, что он написал все команды в терминал. Для того, чтобы этого избежать следует добавить "@" в начало команды и она не будет напечатана.
```makefile{}[Makefile]
.PHONY: target1
target1:
@echo "1"
.PHONY: target2
target2: target1
@echo "2"
.PHONY: target3
target3: target1 target2
@echo "memes"
```
Теперь при вызое `make target3` будет показано следующее:
```cosole
$ make target3
1
2
memes
```
## Валидация сгенерированных файлов
Зачастую `Makefile` используют для компиляции С и зачастую требуется
собрать какую-либо часть проект и пропустить сборку этой части, если эта часть уже собрана.
Раскрою секрет, в Makefile это базовый функционал.
Давайте немного поменяем первый Makefile и запустим дважды.
```makefile{}[Makefile]
# Сгенерируем простой файл
test.gen:
echo 'echo "Hi!"' > "test.gen"
# Команда для запуска файла test.gen
.PHONY: run
run: test.gen
sh "test.gen"
```
Теперь вызываемая команда таргета `test.gen` выводится на экран.
Позапускаем.
```console
$ make run
echo 'echo "Hi!"' > "test.gen" # Наша командабыла вызвана.
sh "test.gen"
Hi!
$ make run
sh "test.gen" # Наша команда не вызвана.
Hi!
```
Дело в том, что названия таргетов - названия файлов, которые должны сгенерировать эти самые таргеты.
То есть, в данном случае таргет `test.gen` должен сгенерировать файл `test.gen` по окончании выполнения. Если этот файл уже присутствует, то команда выполнена не будет. Именно поэтому у нас она не запустилась второй раз, так как в первый запуск был создан треубемый файл и его никто не удалял между запусками.
А что если я вот не хочу чтобы команда создавала и проверяла файлы?
Для этого пишут `.PHONY: ${target}`. Например у нас так объявлен таргет `run` и, даже если файл с названием `run` будет присутствовать в директории цель не будет выполняться.

584
content/ru/project-start.md Normal file
View File

@ -0,0 +1,584 @@
---
title: Как стартовать проект
description: Лучший вариант начала проекта на питоне.
category: Python
position: 1
---
# Как стартовать проект
В целом, вариантов несколько. Например, можно просто хранить несколько скриптов на github и быть довольным, можно дописать к ним `setup.py` и выложиться на pypi. Но лучший способ - структурированный проект с `poetry`.
Почему?
Особенностей у poetry много. Сейчас расскажу про то, как использовать и что делать.
Для начала создадим проект.
```console
$ poetry new new_proj
Created package new_proj in new_proj
$ cd new_proj
```
Данная команда сгенерирует следующую структуру:
```
new_proj
├── new_proj
│   └── __init__.py
├── pyproject.toml
├── README.rst
└── tests
├── __init__.py
└── test_new_proj.py
```
Это наш новый проект с небольшими заготовками. В целом, тут нет ничего необычного. Весь код библиотеки/проекта будет в папке с названием проекта. `README.rst` - дефолтный README. `pyproject.toml` - мета-данные о проекте, такие как: зависимости, описание, дополнительные установочные опции и многое другое. Ты можешь прочитать побольше о `pyproject.toml` [тут](https://python-poetry.org/docs/pyproject/).
## Как работать с poetry
Вся информация есть в [официальной документации](https://python-poetry.org/docs) poetry. Тут я расскажу про основные моменты.
### Управление зависимостями
Чтобы добавить зависимость проекта достаточно выполнить
```console
$ poetry add ${dependency}
```
Данная команда найдет последнюю нужную версию и запишет её в `pyproject.toml` и в `poetry.lock`.
Для того, чтобы установить зависимость для разработки (линтеры например), достаточно добавить флаг `--dev`.
```console
$ poetry add ${dependency} --dev
```
Чтобы удалить зависимость достаточно просто `add` заменить на `remove`.
Примеры работы:
```console
$ poetry add loguru
Using version ^0.5.3 for loguru
Updating dependencies
Resolving dependencies... (0.1s)
Writing lock file
Package operations: 1 install, 0 updates, 0 removals
 • Installing loguru (0.5.3)
$ poetry remove loguru
Updating dependencies
Resolving dependencies... (0.1s)
Writing lock file
Package operations: 0 installs, 0 updates, 1 removal
 • Removing loguru (0.5.3)
```
### Выполнение команд
При использовании virtualenv для входа в оболочку всегда надо было вводить
```bash
source venv/bin/activate
```
Однако с poetry это излишне. Он сам создает и менеджит виртуальные среды.
Чтобы выполнить одиночную команду можно вызвать `run`.
Например:
```console
$ poetry install black --dev
Using version ^20.8b1 for black
• Installing appdirs (1.4.4)
 • Installing click (7.1.2)
 • Installing mypy-extensions (0.4.3)
 • Installing pathspec (0.8.1)
 • Installing regex (2020.11.13)
 • Installing toml (0.10.2)
 • Installing typed-ast (1.4.2)
 • Installing typing-extensions (3.7.4.3)
 • Installing black (20.8b1)
$ poetry run black .
reformatted new_proj/new_proj/__init__.py
reformatted new_proj/tests/test_new_proj.py
All done! ✨ 🍰 ✨
2 files reformatted, 1 file left unchanged.
```
Для выполнения нескольких комманд подряд, можно войти в shell. Это аналог `source venv/bin/activate`.
```console
$ poetry shell
Spawning shell within /home/s3rius/.cache/pypoetry/virtualenvs/new-proj-eutP4v0O-py3.9
$ black .
reformatted new_proj/new_proj/__init__.py
reformatted new_proj/tests/test_new_proj.py
All done! ✨ 🍰 ✨
2 files reformatted, 1 file left unchanged.
```
### Версионирование
Менять версии пакета вручную больше не нужно.
У poetry есть кое что для тебя.
```console
$ poetry version patch
Bumping version from 0.1.0 to 0.1.1
$ poetry version preminor
Bumping version from 0.1.1 to 0.2.0-alpha.0
$ poetry version minor
Bumping version from 0.2.0-alpha.0 to 0.2.0
$ poetry version premajor
Bumping version from 0.2.0 to 1.0.0-alpha.0
$ poetry version major
Bumping version from 1.0.0-alpha.0 to 1.0.0
```
## pyproject.toml
Как было сказано ранее, данный файл содержит в себе мета-информацию пакета.
Пример `pyproject.toml`
```toml{}[pyproject.toml]
[tool.poetry]
name = "new_proj"
version = "0.1.0"
description = "Test library for example"
readme = "README.rst"
homepage = "https://test_url.com/"
repository = "https://github.meme/"
authors = ["Pavel Kirilin <win10@list.ru>"]
[tool.poetry.dependencies]
python = "^3.8"
[tool.poetry.dev-dependencies]
pytest = "^6.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
```
Для того чтобы устновить все зависимости требуется просто ввести
```console
$ poetry install
```
Данная команда создаст виртуальную среду сама и установит **все** зависимости. Включая dev-зависимости. Чтобы установить зависимости только для приложения можно добавить `--no-dev` ключ.
## Как паковать и публиковать на pypi
Всё очень просто.
Давайте добавим какую-нибудь функцию в проект.
```python{}[new_proj/main.py]
def ab_problem(a: int, b: int) -> int:
return a + b
```
```python{}[new_proj/__init.py]
from new_proj.main import ab_problem
__all__ = [
'ab_problem'
]
```
Теперь соберем проект.
```console
$ poetry build
Building new_proj (0.1.0)
- Building sdist
- Built new_proj-0.1.0.tar.gz
- Building wheel
- Built new_proj-0.1.0-py3-none-any.whl
```
Та-да. Это готовый к публикации на pypi пакет. Лежит он в папке dist.
Давайте проверим, что всё работает корректно.
```console
$ pip install "./dist/new_proj-0.1.0-py3-none-any.whl"
Processing ./dist/new_proj-0.1.0-py3-none-any.whl
Installing collected packages: new-proj
Successfully installed new-proj-0.1.0
$ python
Python 3.9.1 (default, Feb 1 2021, 04:02:33)
[GCC 10.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
$ from new_proj import ab_problem
$ ab_problem(1,33)
34
```
Как можно видеть всё работает корректно и теперь мы можем использовать наш пакет.
Для публикации следует использовать:
```console
$ poetry publish -u "user" -p "password"
```
Подробнее можно почитать [тут](https://python-poetry.org/docs/cli/#publish).
# Конфигурация проекта
Конечно, такого рода конфигурация проекта всё равно никуда не годиться.
Давайте настроим автоматический линтинг.
```console
$ poetry add \
$ flake8 \
$ black \
$ isort \
$ mypy \
$ pre-commit \
$ yesqa \
$ autoflake \
$ wemake-python-styleguide --dev
---> 100%
```
Теперь добавим конфигурационных файлов в корень проекта.
Это мои конфигурации, которые я настроил под себя, можешь менять их как хочешь.
`.mypy.ini` для настройки валидации типов.
```ini{}[.mypy.ini]
[mypy]
strict = True
ignore_missing_imports=True
allow_subclassing_any=True
allow_untyped_calls=True
pretty=True
show_error_codes=True
implicit_reexport=True
allow_untyped_decorators=True
```
`.isort.cfg` для конфигурации сортировки импортов.
```ini{}[.isort.cfg]
[isort]
multi_line_output = 3
include_trailing_comma = true
use_parentheses = true
```
`.flake8` - конфигурация линтинга. Тут довольно много. Это игнорирование ненужных кодов ошибок, которые не особо-то и ошибки.
```ini{}[.flake8]
[flake8]
max-complexity = 6
inline-quotes = double
max-line-length = 88
extend-ignore = E203
docstring_style=sphinx
ignore =
; Found `f` string
WPS305,
; Missing docstring in public module
D100,
; Missing docstring in magic method
D105,
; Missing docstring in __init__
D107,
; Found class without a base class
WPS306,
; Missing docstring in public nested class
D106,
; First line should be in imperative mood
D401,
; Found `__init__.py` module with logic
WPS412,
; Found implicit string concatenation
WPS326,
; Found string constant over-use
WPS226,
; Found upper-case constant in a class
WPS115,
; Found nested function
WPS430,
; Found using `@staticmethod`
WPS602,
; Found method without arguments
WPS605,
; Found overused expression
WPS204,
; Found too many module members
WPS202,
; Found too high module cognitive complexity
WPS232,
; line break before binary operator
W503,
; Found module with too many imports
WPS201,
; Found vague import that may cause confusion: X
WPS347,
; Inline strong start-string without end-string.
RST210,
; subprocess call with shell=True seems safe, but may be changed in the future.
S602,
; Starting a process with a partial executable path.
S607,
; Consider possible security implications associated with subprocess module.
S404,
; Found nested class
WPS431,
; Found wrong module name
WPS100,
; Found too many methods
WPS214,
; Found too long ``try`` body
WPS229,
; Found function with too much cognitive complexity
WPS231,
; all init files
__init__.py:
; ignore not used imports
F401,
; ignore import with wildcard
F403,
; Found wrong metadata variable
WPS410,
per-file-ignores =
; all tests
test_*.py,tests.py,tests_*.py,*/tests/*:
; Use of assert detected
S101,
exclude =
./.git,
./venv,
./cached_venv,
./var,
```
`.pre-commit-config.yaml` - конфигураци хуков для запуска всех линтеров перед коммитом.
```yaml{}[.pre-commit-config.yaml]
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
hooks:
- id: check-ast
- id: trailing-whitespace
- id: check-toml
- id: end-of-file-fixer
- repo: https://github.com/asottile/add-trailing-comma
rev: v2.1.0
hooks:
- id: add-trailing-comma
- repo: local
hooks:
- id: black
name: Format with Black
entry: black
language: system
types: [python]
- id: autoflake
name: autoflake
entry: autoflake
language: system
types: [ python ]
args: [ --in-place, --remove-all-unused-imports, --remove-duplicate-keys ]
- id: isort
name: isort
entry: isort
language: system
types: [ python ]
- id: flake8
name: Check with Flake8
entry: flake8
language: system
pass_filenames: false
types: [ python ]
args: [--count, .]
- id: mypy
name: Validate types with MyPy
entry: mypy
language: system
types: [ python ]
- id: yesqa
name: Remove usless noqa
entry: yesqa
language: system
types: [ python ]
- id: pytest
name: pytest
entry: pytest
language: system
pass_filenames: false
types: [ python ]
```
И не забываем про `.gitignore`. Его можно найти [тут](https://github.com/github/gitignore/blob/master/Python.gitignore).
ОН НЕОБХОДИМ ЧТОБЫ pre-commit РАБОТЛ МАКСИМАЛЬНО КОРРЕКТНО.
Теперь установим хуки в репозиторий.
```console
$ git init
Initialized empty Git repository in .git/
$ poetry shell
$ pre-commit install
pre-commit installed at .git/hooks/pre-commit
$ git commit
... # Упадет с кучей ошибок
```
Теперь поправим все возникшие проблемы.
Во первых исправим тесты.
В `tests/test_new_proj.py` напишем следующее:
```python{}[tests/test_new_proj.py]
from new_proj import ab_problem
def test_ab() -> None:
"""AB problecm success case."""
assert ab_problem(1, 2) == 3
```
Добавим описания в `__init__` файлы.
```python{}[tests/__init__.py]
"""Tests for new_proj."""
```
```python{}[new_proj/__init__.py]
"""Project for solving ab problem."""
from new_proj.main import ab_problem
__all__ = [
"ab_problem",
]
```
Пофиксим основной файл проекта.
```python{}[new_proj/main.py]
def ab_problem(first: int, second: int) -> int:
"""
Solve AB problem.
The function sums two integers.
:param first: a argument.
:param second: b argument.
:returns: sum.
"""
return first + second
```
Теперь вы можете сделать свой первый коммит.
```console
$ git commit
Check python ast................Passed
Trim Trailing Whitespace........Passed
Check Toml......................Passed
Fix End of Files................Passed
Add trailing commas.............Passed
Format with Black...............Passed
autoflake.......................Passed
isort...........................Passed
Check with Flake8...............Passed
Validate types with MyPy........Passed
Remove usless noqa..............Passed
pytest..........................Passed
```
Теперь ты знаешь как создавать шедевры.
# Создание CLI-приложения
А что если я хочу cli-приложение?
Ты не представляешь насколько это просто.
Пойдем модифицируем наш основной файл.
```python{}[new_proj/main.py]
import argparse
def parse_args() -> argparse.Namespace:
"""
Parse CLI arguments.
:returns: parsed namespace.
"""
parser = argparse.ArgumentParser()
parser.add_argument("a", type=int)
parser.add_argument("b", type=int)
return parser.parse_args()
def ab_problem(first: int, second: int) -> int:
"""
Solve AB problem.
The function sums two integers.
:param first: a argument.
:param second: b argument.
:returns: sum.
"""
return first + second
def main() -> None:
"""Main function."""
args = parse_args()
print(ab_problem(args.a, args.b)) # noqa: WPS421
```
Теперь поправим pyproject.toml таким образом чтобы он создал cli для нашей функции.
Добавим следующую секцию куда-нибудь в `pyproject.toml`:
```toml
[tool.poetry.scripts]
ab_solver = "new_proj.main:main"
```
Теперь нам доступна программа `ab_solver` внутри shell.
```console
$ poetry install
$ poetry shell
$ ab_solver 1 2
3
```
Хочешь установить? Пожалуйста.
```console
$ poetry build
Building new_proj (0.1.0)
- Building sdist
- Built new_proj-0.1.0.tar.gz
- Building wheel
- Built new_proj-0.1.0-py3-none-any.whl
$ pip install "./dist/new_proj-0.1.0-py3-none-any.whl"
Processing ./dist/new_proj-0.1.0-py3-none-any.whl
Installing collected packages: new-proj
Successfully installed new-proj-0.1.0
$ ab_solver 1 2
3
```
Если запаблишить проект, то у пользователя тоже установится ваша cli-программа.

461
content/ru/traefik.md Normal file
View File

@ -0,0 +1,461 @@
---
title: Traefik - роутинг просто.
description: Изучите как использовать traefik.
position: 2
category: DevOps
---
# Traefik - роутинг просто
Сегодня я бы хотел рассказать о такой клёвой штуке, как [Traefik](https://traefik.io/traefik/). Не так давно я перевёл все сервисы своего сервера на traefik и это буквально сделало жизнь проще. Сегодня я расскажу вам зачем он нужен, как его настроить, и покажу на примере, как именно он помогает. По факту, это nginx на стероидах, в плане конфигурации, потому что в traefik за тебя сделано гораздо больше.
# Что такое traefik
Traefik - это система, которая позволяет настроить маппинг между доменными именами и конкретными приложениями. Допустим, у вас есть контейнер frontend-приложения, которое вы хотели бы разместить на домене `myapp.com`, и также у вас есть контейнер backend-приложения, которое вы бы хотели разместить на домене `api.mayapp.com`. [Traefik](https://traefik.io/traefik/) поможет вам это сделать без лишних файлов конфигурации.
## Сравнение с другими инструментами
Если бы вы решили сделать это через [Nginx](https://www.nginx.com/), то вы бы создали новые файлы конфигураций под каждый домен, в папочке с конфигурациями для всех доменов, положили куда-нибудь сертификат от домена `*myapp.com` и подключали бы его вручную в каждом файле доменов. А если бы вам надо было увеличить количество контейнеров отдельного приложения, вы бы указывали сервера в директиве `upstream` и перечисляли там адреса инстансов приложения вручную.
Любители [Apache HTTP Server](https://httpd.apache.org/) на этом моменте обычно берут дробовик и разносят себе голову хорошим зарядом дроби.
А господа, которые используют [Traefik](https://traefik.io/traefik/), просто дописывают лейблы контейнеру в `docker-compose.yml` и идут дальше дегустировать вино.
<b-message type="is-info" has-icon>
Если же ваше приложение построено на вебсокетах, то тут уже фанаты Nginx тянутся за дробовиком. Ну а для ценителей traefik ничего не меняется, ведь в нём встроена поддержка HTTP/2.0.
</b-message>
<div align="center">
![image](/images/traefik_imgs/nginx_comparasion.png)
</div>
## Архитектура
В официальной документации зарисована следующая схема работы traefik:
<div align="center">
![image](/images/traefik_imgs/traefik-architecture.png)
</div>
Как можно видеть по данному изображению, есть 5 основных составляющих traefik, а именно:
* entrypoints
* routers
* rules (часть routers)
* middlewares (чать routers)
* services
### Entrypoint
Являются основными слушателями входящих соединений. Через них проходит весь трафик. Если попробовать объяснить в двух словах: "Порт, на который должно прийти входящее соединение", - вполне себе неплохое объяснение. В данном туториале я создам 2 `entrypoint`, которые будут слушать на http и https.
### Routers, Rules и Middlewares
Роутер - связующее звено между `entrypoint` и сервисом, куда нужно направить трафик. Роутер хранит в себе информацию, куда направить входящий трафик, и
правила, по которым можно определить, пускать ли трафик дальше.
Rules - это и есть те самые правила, которые определяют, через какой роутер пустить трафик.
Допустим, у вас в конфиге есть следующие правила:
```
Host(`myapp.com`) && (PathPrefix(`/api`) || PathPrefix(`/memes`))
```
Это можно читать как "Этот роутер пустит в моё приложение, если запрос пришёл на хост `myapp.com` и путь запроса начинается с `/api` или `/memes`"
Довольно просто, не так ли?
Ну а middlewares - это некоторая логика, что нужно выполнить с запросом до того, как он попадёт в ваше приложение. В этой статье я не буду рассказывать про существующие `middlewares`, тут потребуется самостоятельное ознакомление с [документацией](https://doc.traefik.io/traefik/middlewares/overview/).
# Конфигурация
<b-message type="is-info" has-icon>
В данной статье я буду использовать Docker для конфигурации traefik.
Вы, конечно же, можете использовать локально установленную версию, всё будет работать, как часы.
</b-message>
Для нашего основного traefik на сервере мы создадим небольшой docker-compose.yml, файл конфига и папочку с сертификатами.
Структура будет следующая:
```
.
├── certs
│   ├── local.key
│   └── local.pem
├── config.toml
└── docker-compose.yml
```
Теперь разберем каждый файл по порядку.
Вот основной docker-compose нашего traefik.
```yaml{}[docker-compose.yml]
---
version: '3.7'
services:
traefik-proxy:
# The official v2.0 Traefik docker image
image: traefik:v2.4.8
container_name: traefik_main
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=traefik-shared
- --providers.file.directory=/etc/traefik/dynamic
- --providers.file.watch=true
- --entrypoints.http.address=:80
- --entrypoints.https.address=:443
ports:
# The HTTP port
- "80:80"
# The HTTPS port
- "443:443"
restart: unless-stopped
networks:
- traefik-shared
environment:
MAIN_HOST: 192.168.1.89
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock
- ./config.toml:/etc/traefik/dynamic/traefik.toml
- ./certs:/etc/certs/
networks:
traefik-shared:
name: traefik-shared
```
Вот какие параметры я передаю в `traefik-cli`.
| Параметр | Что делает |
|-------------------------------------------------|----------------------------------------------------------------------------------|
| `providers.docker=true` | Включает прослушивание докера на новые события и следит за лейблами контейнеров. |
| `providers.docker.exposedbydefault=false` | Отключает автоматическое создание роутеров ко всем контейнерам на сервере. |
| `providers.docker.network=traefik-shared` | Сеть докера, по которой будет выполнятся подключение к контейнерам. |
| `providers.file.directory=/etc/traefik/dynamic` | Папка с конфигурационным файлом. |
| `providers.file.watch=true` | Включает отслеживание изменений файла конфигурации. |
| `entrypoints.http.address=:80` | Создаёт entrypoint с названием http и слушает 80 порт. |
| `entrypoints.https.address=:443` | Создаёт entrypoint с названием https и слушает 443 порт. |
Конечно, вы всегда можете глянуть `traefik --help` и подобрать себе желаемые параметры.
Также из docker-compose файла видно, что я создал докер сеть `traefik-shared`, которую в дальнейшем буду использовать на всех контейнерах, которым требуется свой домен.
Далее следует папка с сертификатами. Для реальных доменов я использую cloudflare и скачиваю сертификаты и ключи с панели администратора. Также не забываю выставлять мод шифрования на strict.
![SSL/TLS encryption mode](/images/traefik_imgs/couldflare_certs.png)
Для генерации локальных сертификатов я использую тулу [mkcert](https://github.com/FiloSottile/mkcert).
Для любого локального домена я делаю что-то типа:
```bash
mkcert "*.local"
mv _wildcard.local-key.pem local.key
mv _wildcard.local.pem local.pem
```
И помещаю это в папочку `certs` рядом с `docker-compose.yml`.
После того как я создал все нужные сертефикаты для всех доменов, их надо указать в файле `config.toml` в папочке traefik.
Вот пример:
```toml{}[config.toml]
[tls.options]
[tls.options.default]
# Эта опция вообще для strict cloudflare tls encryption,
# Но я включаю её и на локальных доменах.
sniStrict = true
[[tls.certificates]]
# Я тут указываю /etc/certs, потому что в docker-compose
# у нас volume на эту папку.
certFile = "/etc/certs/local.pem"
keyFile = "/etc/certs/local.key"
```
<b-message type="is-info" has-icon>
Для добавления новых сертификатов в данный файл достаточно добавить:
</b-message>
```toml
[[tls.certificates]]
certFile = "/etc/certs/<certFile>"
keyFile = "/etc/certs/<keyFile>"
```
После этого вы можете запускать traefik и наслаждаться доменами для ваших контейнеров.
# Запуск приложений
Теперь сконфигурируем приложение таким образом, чтобы к нему можно было обращаться через доменное имя.
Для примера возьмем мелкое приложение на nodejs со следующей структурой проекта:
```
.
├── docker-compose.yml
├── Dockerfile
├── .dockerignore
├── index.js
├── package.json
└── yarn.lock
```
```js{}[index.js]
const express = require('express')
let req_count = 0;
const hostname = process.env.HOSTNAME;
const PORT = process.env.PORT || 3000;
app = express();
app.get("/", (req, res) => {
console.log(`GET / ${hostname}`)
res.send({request_num: req_count++, host: hostname})
})
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
```
```json{}[package.json]
{
"name": "express-test",
"version": "1.0.0",
"main": "index.js",
"author": "s3rius",
"license": "MIT",
"scripts": {
"runserver": "node index.js"
},
"dependencies": {
"express": "^4.17.1"
}
}
```
```dockerfile{}[Dockerfile]
FROM node:16-alpine3.11
WORKDIR /app
COPY package.json yarn.lock /app/
RUN yarn install
COPY . /app/
ENTRYPOINT ["yarn", "run"]
```
```yaml{}[docker-compose.yml]
---
version: '3.7'
services:
server:
build: .
labels:
- traefik.enable=true
- traefik.http.routers.test_node.rule=Host(`test_node.local`)
- traefik.http.routers.test_node.entrypoints=http
- traefik.http.routers.test_node.service=node_test
- traefik.http.services.node_test.loadbalancer.server.port=3000
command: runserver
networks:
- traefik-shared
networks:
traefik-shared:
name: traefik-shared
external: true
```
<b-message type="is-info" has-icon>
Файл yarn.lock генерируется командой `yarn install`. Если у вас не установлен `yarn`, то ничего страшного. Просто это будет занимать чуть больше времени на сборку образа.
</b-message>
Как вы видите, приложение слушает на порт `3000` и отвечает свим hostname и количеством обработанных запросов. А в docker-compose.yml, в отличие от обычного проекта, появились labels.
| Лейбл | Что делает |
|---------------------------------------------------------------|---------------------------------------------------------------------------------------|
| ``traefik.enable=true`` | Включить поддержку роутинга через traefik |
| ``traefik.http.routers.<router>.rule=Host(`test_node.local`)`` | Поставить правило роутинга, если Host запроса равен test_node.local |
| ``traefik.http.routers.<router>.entrypoints=http`` | Слушать на entrypoint http (80 порт, это было объявлено в параметрах запуска traefik) |
| ``traefik.http.routers.<router>.service=<service>`` | Сервис, связанный с роутером test_node |
| ``traefik.http.services.<service>.loadbalancer.server.port=3000`` | Порт, куда направлять запросы в сервис node_test |
<hr/>
<b-message type="is-danger" has-icon>
Роутеры, сервисы и хосты явно создавать нигде не нужно.
Они сами создаются, когда вы их указываете.
Название сервиса и роутера могут совпадать.
Обратите внимание на косые кавычки при указании хоста! Это обязательно.
В объявлении labels могут быть использованы переменные среды. Например:
``traefik.http.routers.test_node.rule=Host(`${APP_HOST}`)``
</b-message>
Также можно видеть, что я подключил контейнер к сети, которую мы указывали в контейнере traefik. Здесь она помечена как external.
Теперь мы можем спокойно запустить наш сервис. Для этого воспользуемся следующей командой:
```bash
docker-compose up --build --scale server=3
```
В данной команде мы указали, что хотим поднять 3 инстанса нашего сервиса. Остальным пусть занимается traefik.
Теперь попробуем некоторое количество раз выполнить запрос на наш сервис.
```console
$ curl -H "Host: test_node.local" "http://localhost"
{"request_num":0,"host":"7417ac8fda92"}
```
Результат должен быть примерно таким:
[![GIF](/images/traefik_imgs/curls.gif)](/images/traefik_imgs/curls.gif)
Как вы видите, traefik балансирует между контейнерами за нас. И я считаю, что это - победа.
## Подключение TLS и сертификатов
Тут всё не намного сложнее. Давайте немного поменяем лейблы нашего контейнера.
```yaml
services:
server:
labels:
- traefik.enable=true
- traefik.http.routers.test_node.rule=Host(`test_node.local`)
- traefik.http.routers.test_node.entrypoints=https
- traefik.http.routers.test_node.tls=true
- traefik.http.routers.test_node.service=node_test
- traefik.http.services.node_test.loadbalancer.server.port=3000
```
Как вы видите, я поменял entrypoints на `https`. Как вы можете помнить, это entrypoint, который слушает на 443 порт. И также я включил поддержку tls лейблом `traefik.http.routers.<router>.tls=true`
На данном этапе вам потребуется добавить свой хост в `/etc/hosts`, если вы используете нормальную систему. Но если вы всё же на windows, то вам потребуется добавить правило в `C:\Windows\System32\drivers\etc\hosts`.
И добавляем в конец файла запись:
```
127.0.0.1 test_node.local
```
И также вам потребуется cертификат на этот домен.
Для этого:
1. Создадим сертификат через `mkcert`, как упоминалось ранее;
2. Поместим ключ и сертификат в папку certs;
3. Добавим ключ и сертификат для вашего домена в config.toml (Формат указан выше).
Теперь вы можете обращаться к вашему приложению напрямую через локальное доменное имя.
```console
$ curl --insecure https://test_node.local
{"request_num":0,"host":"7417ac8fda92"}
```
## Добавление локальных сервисов не из докера
Все те флаги, которые мы указываем в labels, вы также можете указать в файле конфигурации рядом с docker-compose.yml, указав конкретный ip адрес.
Например:
```toml
[http.routers]
# Define a connection between requests and services
[http.routers.<router_name>]
rule = "Host(`my_app.local`)"
entrypoints = "https"
service = "<service_name>"
[http.routers.<router_name>.tls]
[http.services]
# Define how to reach an existing service on our infrastructure
[http.services.<service_name>.loadBalancer]
[[http.services.<service_name>.loadBalancer.servers]]
url = "http://192.168.1.89:8100"
```
# Создание локального DNS
В данном пункте я бы хотел рассказать, как настроить свой DNS-сервер, чтобы ваши домены были доступны всем устройствам в локальной сети. Для этого я буду использовать dnsmasq. Пользователям винды он недоступен, поэтому советую развернуть маленький домашний сервер на линуксе.
Для этого установите `dnsmasq` и найдите и раскомментируйте, либо добавьте следующие строчки в файл `/etc/dnsmasq.conf`:
```conf{}[/etc/dnsmasq.conf]
# Never forward plain names (without a dot or domain part)
domain-needed
# Never forward addresses in the non-routed address spaces.
bogus-priv
address=/.local/192.168.1.89
address=/.<other_domain>/<your_local_ip>
```
У меня traefik развернут на хосте `192.168.1.89`. У вас ip может отличаться.
Чтобы это узнать, посмотрите свой ip через роутер или выполните `ip addr`.
Вообще, `dnsmasq` парсит файл `/etc/hosts` и вы можете туда добавлять записи, типа:
```
192.168.1.1 mydomain.local
```
Но так как я указал `address`, то это необязательно. `dnsmasq` и без явного указания поддоменов должен будет работать отлично.
Запустите `dnsmasq` в режиме сревиса:
```bash
sudo systemctl enable dnsmasq.service
sudo systemctl start dnsmasq.service
```
Теперь пойдите в настройки вашего роутера и найдите DNS-сервера. Добавьте туда ваш ip. Также не забудьте сделать локальный ip вашего устройства статичным.
Для примера, в роутерах keenetic это можно сделать, зарегистрировав устройство в меню 'Список устройств' и нажав на галочку 'Постоянный IP-адрес'.
И добавьте свой DNS-сервер в список DNS серверов вашего роутера.
![Keenetic interface](/images/traefik_imgs/keenetic_dns.png)
Готово. Вы можете попробовать зайти на свой домен с другого устройства в локальной сети, и это должно работать.
# Мониторинг работы traefik
Вообще, traefik имеет WEB интерфейс для отслеживания его работы.
Для того чтобы подключить его, давайте поменяем `docker-compose.yml`.
```yaml
services:
traefik-proxy:
# The official v2.0 Traefik docker image
image: traefik:v2.4.8
container_name: traefik_main
labels:
- traefik.enable=true
- traefik.http.routers.traefik_router.rule=Host(`traefik.local`)
- traefik.http.routers.traefik_router.service=api@internal
- traefik.http.routers.traefik_router.entrypoints=https
- traefik.http.routers.traefik_router.tls=true
command:
- --api.dashboard=true
...
```
Я добавил ключ `--api.dashboard=true` и лейблы для роута до `traefik`.
Замечу, service определён заранее - это `api@internal`. Укажите его в `traefik.http.routers.<router_name>.service`.
Теперь вы можете зайти на `traefik.local` через свой браузер и увидеть конфигурацию traefik.
[![GIF](/images/traefik_imgs/traefik_web.png)](/images/traefik_imgs/traefik_web.png)
Разве это не круто?

12
jsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["./*"],
"@/*": ["./*"],
"~~/*": ["./*"],
"@@/*": ["./*"]
}
},
"exclude": ["node_modules", ".nuxt", "dist"]
}

117
layouts/default.vue Normal file
View File

@ -0,0 +1,117 @@
<template>
<div class="page dark-mode">
<b-navbar type="is-primary w-100">
<template #brand>
<NuxtLink to="/" class="navbar-item">
<img src="/logo.png" alt="Logo" /> S3rius' dev blog
</NuxtLink>
</template>
<template #end>
<div class="navbar-menu">
<a
role="button"
class="navbar-burger"
aria-label="menu"
aria-expanded="false"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div
v-for="[category, pages] in categories"
:key="category"
class="is-hidden-desktop"
>
<p class="navbar-item is-inactive divider">
<span>{{ category }}</span>
</p>
<NuxtLink
class="navbar-item"
v-for="(item, key) of pages"
:to="item.slug"
:key="key"
exact-active-class="is-active"
>
{{ item.title }}
</NuxtLink>
</div>
</template>
</b-navbar>
<section class="main-content columns">
<aside class="column is-2 section is-hidden-touch">
<b-menu>
<div v-for="[category, pages] in categories" :key="category">
<p class="menu-label">{{ category }}</p>
<ul class="menu-list">
<li v-for="(item, key) of pages" :key="key">
<NuxtLink :to="item.slug" exact-active-class="is-active">
{{ item.title }}
</NuxtLink>
</li>
</ul>
</div>
</b-menu>
</aside>
<div class="container column">
<Nuxt />
</div>
</section>
<Footer></Footer>
</div>
</template>
<script>
import { defineComponent, useContext, ref } from '@nuxtjs/composition-api'
import Footer from '../components/Footer.vue'
export default defineComponent({
setup() {
const { $content } = useContext()
const categories = ref([])
$content({ deep: true })
.only(['category', 'slug', 'title', 'position'])
.sortBy('createdAt', 'asc')
.fetch()
.then((pages) => {
let cats = new Map()
for (const page of pages) {
if (cats.get(page.category) === undefined) {
cats.set(page.category, [])
}
cats.get(page.category).push(page)
}
// cats.forEach((pages) => {
// pages.sort(
// (page1, page2) => (page1.position || 0) > (page2.position || 0)
// )
// })
categories.value = Array.from(cats)
})
return {
categories,
}
},
components: { Footer },
})
</script>
<style scoped lang="scss">
.divider {
width: 100%;
text-align: center;
border-bottom: 1px solid #000;
line-height: 0.1em;
margin: 10px 0 20px;
span {
background: #fff;
padding: 0 15px;
}
}
.content {
overflow-y: scroll;
}
</style>

87
nuxt.config.js Normal file
View File

@ -0,0 +1,87 @@
export default {
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
ssr: false,
// Target: https://go.nuxtjs.dev/config-target
target: 'static',
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: "S3rius' dev blog",
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ name: 'format-detection', content: 'telephone=no' },
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: [
'~/static/css/syntax-highlighter.scss',
'~/static/css/global-styles.scss',
],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [
// https://go.nuxtjs.dev/typescript
'@nuxt/typescript-build',
// https://go.nuxtjs.dev/stylelint
'@nuxtjs/stylelint-module',
'@nuxtjs/composition-api/module',
],
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
// https://go.nuxtjs.dev/buefy
'nuxt-buefy',
// https://go.nuxtjs.dev/axios
'@nuxtjs/axios',
// https://go.nuxtjs.dev/pwa
'@nuxtjs/pwa',
// https://content.nuxtjs.org/
'@nuxt/content',
],
// Axios module configuration: https://go.nuxtjs.dev/config-axios
axios: {},
// PWA module configuration: https://go.nuxtjs.dev/pwa
pwa: {
manifest: {
lang: 'en',
},
icon: {
fileName: 'logo.png',
},
},
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {},
content: {
markdown: {
remarkPlugins: [
'remark-squeeze-paragraphs',
'remark-slug',
'remark-external-links',
'remark-footnotes',
],
rehypePlugins: [
'rehype-sort-attribute-values',
'rehype-sort-attributes',
'rehype-raw',
],
prism: {
theme: false,
},
},
},
}

47
package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "blog",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate",
"lint:js": "eslint --ext \".js,.ts,.vue\" --ignore-path .gitignore .",
"lint:style": "stylelint \"**/*.{vue,css}\" --ignore-path .gitignore",
"lint": "yarn lint:js && yarn lint:style"
},
"dependencies": {
"@mapbox/rehype-prism": "^0.8.0",
"@nuxt/content": "^1.15.1",
"@nuxtjs/axios": "^5.13.6",
"@nuxtjs/composition-api": "^0.31.0",
"@nuxtjs/pwa": "^3.3.5",
"clipboard": "^2.0.8",
"core-js": "^3.15.1",
"nuxt": "^2.15.7",
"nuxt-buefy": "^0.4.8",
"prism-themes": "^1.9.0",
"prismjs": "^1.25.0",
"remark-attr": "^0.11.1"
},
"devDependencies": {
"@babel/eslint-parser": "^7.14.7",
"@nuxt/types": "^2.15.8",
"@nuxt/typescript-build": "^2.1.0",
"@nuxtjs/eslint-config-typescript": "^8.0.0",
"@nuxtjs/eslint-module": "^3.0.2",
"@nuxtjs/stylelint-module": "^4.0.0",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-nuxt": "^2.0.0",
"eslint-plugin-vue": "^7.12.1",
"prettier": "^2.3.2",
"sass": "^1.44.0",
"sass-loader": "10.1.1",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-config-standard-scss": "^3.0.0"
}
}

116
pages/_slug.vue Normal file
View File

@ -0,0 +1,116 @@
<template>
<div>
<nuxt-content :document="page" />
</div>
</template>
<script>
import {
defineComponent,
useContext,
ref,
onMounted,
useStore,
} from '@nuxtjs/composition-api'
import Prism from '~/plugins/prism'
export default defineComponent({
setup() {
const page = ref({})
const { $content, params, error } = useContext()
const store = useStore()
// Finding current path's slug.
const slug = params.value.slug || 'index'
$content(store.state.lang, slug)
.fetch()
.then((doc) => {
page.value = doc
})
.catch((_) => {
error({ statusCode: 404, message: 'Page not found' })
})
onMounted(() => {
console.log(Prism.languages)
Prism.highlightAll()
})
return {
page,
}
},
})
</script>
<style lang="scss">
@import '@/static/css/global-styles.scss';
.nuxt-content {
.icon.icon-link {
display: none;
}
@mixin heading {
line-height: 1.3;
font-weight: 300;
letter-spacing: -0.01em;
color: rgba(0, 0, 0, 0.54);
margin: 1em 0.5em;
}
h1 {
@include heading;
font-size: 2.5em;
}
h2 {
@include heading;
font-size: 2em;
}
h3,
h4 {
@include heading;
font-size: 1.5em;
}
.nuxt-content-highlight {
margin-right: 10px;
margin-left: 10px;
.filename {
margin-top: 0.5em;
border-top-left-radius: 0.3em;
border-top-right-radius: 0.3em;
padding-left: 1em;
color: white;
display: flex;
flex: 1 auto;
left: 0;
width: 100%;
background-color: $primary;
font: lighter;
z-index: 10;
}
.filename + pre {
margin-top: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.table {
background-color: inherit !important;
}
}
ul {
list-style: disc;
margin: 1em;
}
li {
margin-bottom: 0.5em !important;
margin-left: 1.25em !important;
}
}
</style>

7
pages/index.vue Normal file
View File

@ -0,0 +1,7 @@
<template>
<section class="section">
<div>a</div>
</section>
</template>
<script></script>

24
plugins/prism.js Normal file
View File

@ -0,0 +1,24 @@
import Prism from 'prismjs'
import 'prism-themes/themes/prism-dracula.min.css'
// Include the toolbar plugin: (optional)
import 'prismjs/plugins/toolbar/prism-toolbar'
// Include the line numbers plugin: (optional)
// import 'prismjs/plugins/line-numbers/prism-line-numbers'
// import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
// Include the line highlight plugin: (optional)
// import 'prismjs/plugins/line-highlight/prism-line-highlight'
// import 'prismjs/plugins/line-highlight/prism-line-highlight.css'
// Include some other plugins: (optional)
import 'prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard'
// import 'prismjs/plugins/highlight-keywords/prism-highlight-keywords'
// import 'prismjs/plugins/show-language/prism-show-language'
import 'prismjs/components/prism-makefile'
// Set vue SFC to markdown
Prism.languages.vue = Prism.languages.markup
export default Prism

View File

@ -0,0 +1,119 @@
// Import Bulma's core
@import "~bulma/sass/utilities/_all";
// Set your colors
$primary: #687cec;
$primary-light: findLightColor($primary);
$primary-dark: findDarkColor($primary);
$primary-invert: findColorInvert($primary);
$background: #fafdff;
$background-invert: findColorInvert($background);
// Lists and maps
$custom-colors: null !default;
$custom-shades: null !default;
// Setup $colors to use as bulma classes (e.g. 'is-twitter')
$colors: mergeColorMaps(
(
"white": (
$white,
$black,
),
"black": (
$black,
$white,
),
"light": (
$light,
$light-invert,
),
"dark": (
$dark,
$dark-invert,
),
"primary": (
$primary,
$primary-invert,
$primary-light,
$primary-dark,
),
"link": (
$link,
$link-invert,
$link-light,
$link-dark,
),
"info": (
$info,
$info-invert,
$info-light,
$info-dark,
),
"success": (
$success,
$success-invert,
$success-light,
$success-dark,
),
"warning": (
$warning,
$warning-invert,
$warning-light,
$warning-dark,
),
"danger": (
$danger,
$danger-invert,
$danger-light,
$danger-dark,
),
),
$custom-colors
);
// Links
$link: $primary;
$link-invert: $primary-invert;
$link-focus-border: $primary;
// Import Bulma and Buefy styles
@import "~bulma";
@import "~buefy/src/scss/buefy";
.bnumber {
align-items: center;
background-color: whitesmoke;
border-radius: 9999px;
display: inline-flex;
font-size: 1.25rem;
height: 2em;
justify-content: center;
margin-right: 1.5rem;
min-width: 2.5em;
padding: 0.25rem 0.5rem;
text-align: center;
vertical-align: top;
}
.number {
display: inherit;
background-color: none;
border: none;
min-width: none;
padding: 0;
margin: 0;
font-size: inherit;
background: none;
width: min-content;
}
html {
background-color: $background;
min-height: 100vh;
}
body {
max-width: 100vw;
overflow: hidden;
}

View File

@ -0,0 +1,9 @@
.nuxt-content {
@apply break-words;
& .nuxt-content-highlight {
& > .filename {
@apply block bg-gray-700 text-gray-100 font-mono text-sm tracking-tight py-2 px-4 -mb-3 rounded-t;
}
}
}

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

10
store/README.md Normal file
View File

@ -0,0 +1,10 @@
# STORE
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains your Vuex Store files.
Vuex Store option is implemented in the Nuxt.js framework.
Creating a file in this directory automatically activates the option in the framework.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).

15
store/index.ts Normal file
View File

@ -0,0 +1,15 @@
interface State{
lang: String
}
export const state = () => {
return {
lang: "ru"
}
}
export const mutations = {
set_lang (state: State, value: String) {
state.lang = value
},
}

9
stylelint.config.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
// add your custom config here
// https://stylelint.io/user-guide/configuration
rules: {
'at-rule-no-unknown': null,
'declaration-empty-line-before': null,
},
}

39
tsconfig.json Normal file
View File

@ -0,0 +1,39 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "Node",
"lib": [
"ESNext",
"ESNext.AsyncIterable",
"DOM"
],
"esModuleInterop": true,
"allowJs": true,
"sourceMap": true,
"strict": true,
"noEmit": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"~/*": [
"./*"
],
"@/*": [
"./*"
]
},
"types": [
"@nuxt/types",
"@nuxtjs/axios",
"@types/node",
"@nuxt/content",
"@nuxtjs/color-mode",
]
},
"exclude": [
"node_modules",
".nuxt",
"dist"
]
}

4
types/vue-shim.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "*.vue" {
import Vue from 'vue'
export default Vue
}

12127
yarn.lock Normal file

File diff suppressed because it is too large Load Diff