hot reload (#24)

* container with hot reload

* update readme
This commit is contained in:
Evgeny
2025-07-05 08:18:15 +05:00
committed by GitHub
parent b129a74d39
commit 4485a10514
9 changed files with 312 additions and 197 deletions
+11
View File
@@ -0,0 +1,11 @@
APP_PATH=/opt/outline
SRC_PATH=./outline
ADDRESS=localhost
PORT_OUTLINE=10240
PORT_OIDC=10241
PORT_REDIS=10242
PORT_POSTGRES=10243
COMMON=outline
SECRET=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
+6 -2
View File
@@ -9,8 +9,8 @@ on:
- .github/workflows/** - .github/workflows/**
- outline/** - outline/**
- tools/translation.json - tools/translation.json
- tools/language.patch - tools/patches/**
- Dockerfile - Dockerfile.prod
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
@@ -79,6 +79,10 @@ jobs:
cache-from: type=gha,scope=build-${{ matrix.arch }} cache-from: type=gha,scope=build-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=build-${{ matrix.arch }} cache-to: type=gha,mode=max,scope=build-${{ matrix.arch }}
platforms: linux/${{ matrix.arch }} platforms: linux/${{ matrix.arch }}
file: Dockerfile.prod
build-args: |
APP_PATH=/opt/outline
SRC_PATH=./outline
labels: ${{ steps.metadata.outputs.labels }} labels: ${{ steps.metadata.outputs.labels }}
outputs: type=image,"name=${{ github.repository }},ghcr.io/${{ github.repository }}",push-by-digest=true,name-canonical=true,push=true outputs: type=image,"name=${{ github.repository }},ghcr.io/${{ github.repository }}",push-by-digest=true,name-canonical=true,push=true
+16 -37
View File
@@ -1,18 +1,14 @@
ARG APP_PATH=/opt/outline FROM node:20 AS base
ARG SRC_PATH=./outline
FROM node:20 AS deps
ARG APP_PATH ARG APP_PATH
ARG SRC_PATH ARG SRC_PATH
WORKDIR $APP_PATH WORKDIR $APP_PATH
FROM base AS deps
COPY ${SRC_PATH}/package.json ${SRC_PATH}/yarn.lock ./ COPY ${SRC_PATH}/package.json ${SRC_PATH}/yarn.lock ./
RUN yarn install --production=true --frozen-lockfile --network-timeout 1000000 && \ RUN yarn install --production=true --frozen-lockfile --network-timeout 1000000 && \
yarn cache clean yarn cache clean
FROM node:20 AS build FROM base AS build
ARG APP_PATH
ARG SRC_PATH
WORKDIR $APP_PATH
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y patch cmake && \ apt-get install -y patch cmake && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
@@ -22,35 +18,18 @@ ENV NODE_OPTIONS="--max-old-space-size=24000"
RUN yarn install --no-optional --frozen-lockfile --network-timeout 1000000 && \ RUN yarn install --no-optional --frozen-lockfile --network-timeout 1000000 && \
yarn cache clean yarn cache clean
COPY ${SRC_PATH} . COPY ${SRC_PATH} .
COPY ./tools/language.patch . COPY --from=deps $APP_PATH/node_modules ./node_modules
RUN patch -p1 < language.patch COPY ./tools/patches/* .
COPY ./tools/translation.json ./shared/i18n/locales/ru_RU/translation.json RUN for patch in $(ls *.patch); do patch -p1 < $patch; done
RUN cat <<EOF > /entrypoint.sh
npx yarn concurrently -n "dev,i18n" \
"yarn dev:watch" \
"yarn nodemon \
--watch './shared/i18n/locales/ru_RU' \
--exec 'yarn build:i18n'"
EOF
ARG CDN_URL ARG CDN_URL
RUN yarn build && rm -rf node_modules
FROM node:22-slim AS release
ARG APP_PATH
ARG SRC_PATH
WORKDIR $APP_PATH
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
ARG DATA_PATH=/var/lib/outline/data ARG DATA_PATH=/var/lib/outline/data
ARG USER=nodejs
RUN useradd -m -U ${USER} && \
mkdir -p ${DATA_PATH} && \
chown -R ${USER}:${USER} ${APP_PATH} ${DATA_PATH}/.. && \
chmod 1777 ${DATA_PATH}
COPY --chown=${USER} --from=deps $APP_PATH/node_modules ./node_modules
COPY --chown=${USER} --from=build $APP_PATH/build ./build
COPY --chown=${USER} --from=build $APP_PATH/server ./server
COPY --chown=${USER} --from=build $APP_PATH/public ./public
COPY --chown=${USER} --from=build $APP_PATH/.sequelizerc .
COPY --chown=${USER} --from=build $APP_PATH/package.json .
ENV NODE_ENV=production
ENV PORT=3000
USER ${USER}
EXPOSE ${PORT}
VOLUME ${DATA_PATH} VOLUME ${DATA_PATH}
HEALTHCHECK --interval=1m CMD curl -fs localhost:${PORT}/_health | grep -q OK || exit 1 STOPSIGNAL SIGKILL
CMD ["yarn", "start"] ENTRYPOINT ["bash", "/entrypoint.sh"]
+51
View File
@@ -0,0 +1,51 @@
FROM node:20 AS base
ARG APP_PATH
ARG SRC_PATH
WORKDIR $APP_PATH
FROM base AS deps
COPY ${SRC_PATH}/package.json ${SRC_PATH}/yarn.lock ./
RUN yarn install --production=true --frozen-lockfile --network-timeout 1000000 && \
yarn cache clean
FROM base AS build
RUN apt-get update && \
apt-get install -y patch cmake && \
rm -rf /var/lib/apt/lists/*
COPY ${SRC_PATH}/patches ./patches
COPY ${SRC_PATH}/package.json ${SRC_PATH}/yarn.lock ./
ENV NODE_OPTIONS="--max-old-space-size=24000"
RUN yarn install --no-optional --frozen-lockfile --network-timeout 1000000 && \
yarn cache clean
COPY ${SRC_PATH} .
COPY ./tools/patches/lang.patch .
RUN for patch in $(ls *.patch); do patch -p1 < $patch; done
COPY ./tools/translation.json ./shared/i18n/locales/ru_RU/translation.json
ARG CDN_URL
RUN yarn build && rm -rf node_modules
FROM node:22-slim AS release
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
ARG DATA_PATH=/var/lib/outline/data
ARG USER=nodejs
ARG APP_PATH
WORKDIR $APP_PATH
RUN useradd -m -U ${USER} && \
mkdir -p ${DATA_PATH} && \
chown -R ${USER}:${USER} ${APP_PATH} ${DATA_PATH}/.. && \
chmod 1777 ${DATA_PATH}
COPY --chown=${USER} --from=deps $APP_PATH/node_modules ./node_modules
COPY --chown=${USER} --from=build $APP_PATH/build ./build
COPY --chown=${USER} --from=build $APP_PATH/server ./server
COPY --chown=${USER} --from=build $APP_PATH/public ./public
COPY --chown=${USER} --from=build $APP_PATH/.sequelizerc .
COPY --chown=${USER} --from=build $APP_PATH/package.json .
ENV NODE_ENV=production
ENV PORT=3000
USER ${USER}
EXPOSE ${PORT}
VOLUME ${DATA_PATH}
HEALTHCHECK --interval=1m CMD curl -fs localhost:${PORT}/_health | grep -q OK || exit 1
CMD ["yarn", "start"]
+35 -40
View File
@@ -35,23 +35,48 @@ services:
## 🛠️ Разработка ## 🛠️ Разработка
### Ключевые файлы
- русский перевод — [tools/translation.json](./tools/translation.json)
- английский перевод — [outline/shared/i18n/locales/en_US/translation.json](https://github.com/outline/outline/blob/main/shared/i18n/locales/en_US/translation.json)
- временный файл — [tools/translation.tmp.json]() (существует только локально)
### Быстрый старт
0. Клонирование репозитория с подмодулем:
```sh
git clone --recurse-submodules git@github.com:flameshikari/outline-ru.git
```
1. Пулл изменений в подмодуле и переключение на коммит с целевой версией:
```sh
cd outline
git pull --rebase --tags
git checkout v0.85.0
cd -
```
2. Запуск контейнеров:
```sh
docker compose up -d --build
```
Веб-интерфейс Outline будет доступен по [этой ссылке](http://localhost:10240).
3. Формирование временного файла с помощью [tools/diff.py](./tools/diff.py):
```sh
python tools/diff.py
```
После можно приступить к переводу сфомированного временного файла. Любые изменения в русском переводе обновят [открытую веб-страницу](http://localhost:10240) через пару секунд.
### Описание ### Описание
Контейнер Outline описан в одном [Dockerfile](./Dockerfile) на основе двух оригинальных с применением [патча](./tools/language.patch) поверх исходного кода Outline (он подключен к этому репозиторию в качестве подмодуля) и копированием [файла перевода](./tools/translation.json). Патч, помимо добавления отсутствующих строк в код, меняет некоторые upstream-ссылки, чтобы Outline мониторил этот контейнер на наличие новых версий, а не оригинальный. Скрипт [tools/diff.py](./tools/diff.py) используется для объединения английского и русского переводов во временный файл. Скрипт не имеет интерактивного режима и каких-либо аргументов/опций, он просто запускается (с выводом некоторой полезной информации) и делает следующее:
В [docker-compose.yml](./docker-compose.yml) описаны четыре контейнера (кликабельные названия далее — это localhost-ссылки): [Outline](http://localhost:10240), Redis, Postgres и [тестовый OIDC-сервер](http://localhost:10241) (логин/пароль: `outline`). Также там описана вся конфигурация контейнеров; настраивайте под себя по желанию. После первой сборки контейнера большинство слоёв берётся из кэша, сам Outline после изменений пересобирается около полминуты-минуты (тут зависит от железа). Пайплайн примерно такой: перевёл → собрал → проверил.
Скрипт [diff.py](./tools/diff.py) используется для объединения переводов [английского](https://github.com/outline/outline/blob/main/shared/i18n/locales/en_US/translation.json) с [русским](./tools/translation.json) во временный файл `./tools/translation.tmp.json`. Скрипт не имеет интерактивного режима и каких-либо аргументов/опций, он просто запускается (с выводом некоторой полезной информации) и делает следующее:
- сохраняет актуальные переведённые строки - сохраняет актуальные переведённые строки
- удаляет неактуальные переведённые строки - удаляет неактуальные переведённые строки
- если в файле перевода есть одинаковые key/value пары, то они считаются исключениями (например, `HTML` или `API`) и переносятся как есть - если в русском переводе есть одинаковые key/value пары, то они считаются исключениями (например, `HTML` или `API`) и переносятся как есть
- новые непереведённые строки добавляются в конец - новые непереведённые строки добавляются в конец
> Возможно, для коллективного перевода стоило использовать [Crowdin](https://crowdin.com), но что-то руки не дошли ¯\\_(ツ)_/¯
Во временном файле вручную делается перевод новых строк, а затем [файл перевода](./tools/translation.json) вручную заменяется временным файлом.
> Если во временном файле присутствуют две одинаковые непереведённые строки, но одна из них с суффиксом `_plural` (множественное число), например: > Если во временном файле присутствуют две одинаковые непереведённые строки, но одна из них с суффиксом `_plural` (множественное число), например:
> >
> ```json > ```json
@@ -69,33 +94,3 @@ services:
> "{{ count }} comment_2": "{{ count }} комментариев" > "{{ count }} comment_2": "{{ count }} комментариев"
> } > }
> ``` > ```
### Команды
0. Клонирование репозитория с подмодулем
```sh
git clone --recurse-submodules git@github.com:flameshikari/outline-ru.git
```
1. Пулл изменений в подмодуле и переключение на коммит с целевой версией
```sh
cd outline
git pull --rebase --tags
git checkout v0.85.0
cd -
```
2. Формирование временного файла с помощью [diff.py](./tools/diff.py)
```sh
python ./tools/diff.py
```
3. Замена [файла перевода](./tools/translation.json) после перевода строк во временном файле
```
cp ./tools/translation.tmp.json ./tools/translation.json
```
4. Сборка контейнера
```sh
docker compose up -d --build
```
+140
View File
@@ -0,0 +1,140 @@
volumes:
outline:
name: outline
outline-postgres:
name: outline-postgres
networks:
default:
name: outline
services:
outline:
container_name: outline
image: flameshikari/outline-ru:nightly
build:
context: .
dockerfile: Dockerfile.prod
args:
- APP_PATH=${APP_PATH}
- SRC_PATH=${SRC_PATH}
network_mode: host
pull_policy: always
volumes:
- outline:/var/lib/outline/data
depends_on:
- outline-postgres
- outline-redis
- outline-oidc
environment:
FILE_STORAGE: local
FORCE_HTTPS: false
PORT: ${PORT_OUTLINE}
URL: http://${ADDRESS}:${PORT_OUTLINE}
SECRET_KEY: ${SECRET}
UTILS_SECRET: ${SECRET}
REDIS_URL: redis://${ADDRESS}:${PORT_REDIS}
DATABASE_URL: postgres://${COMMON}:${COMMON}@${ADDRESS}:${PORT_POSTGRES}/${COMMON}
PGSSLMODE: disable
OIDC_ISSUER_URL: http://${ADDRESS}:${PORT_OIDC}
OIDC_CLIENT_ID: ${COMMON}
OIDC_CLIENT_SECRET: ${COMMON}
outline-oidc:
container_name: outline-oidc
image: ghcr.io/soluto/oidc-server-mock:0.11.0
ports:
- ${PORT_OIDC}:80
healthcheck:
test: curl -fs ${ADDRESS}/health || exit 1
start_period: 2s
interval: 1s
timeout: 100ms
retries: 10
environment:
ASPNETCORE_URLS: http://+:80
ASPNETCORE_ENVIRONMENT: Development
CLIENTS_CONFIGURATION_INLINE: |
[
{
"ClientId": "${COMMON}",
"ClientSecrets": ["${COMMON}"],
"RedirectUris": ["http://${ADDRESS}:${PORT_OUTLINE}/auth/oidc.callback"],
"AllowedGrantTypes": ["authorization_code"],
"AllowedScopes": ["openid", "profile", "email"],
"RequirePkce": false
}
]
USERS_CONFIGURATION_INLINE: |
[
{
"SubjectId": "1",
"Username": "${COMMON}",
"Password": "${COMMON}",
"Claims": [
{
"Type": "email",
"Value": "mail@example.com",
"ValueType": "string"
},
{
"Type": "name",
"Value": "Outline",
"ValueType": "string"
}
]
}
]
SERVER_OPTIONS_INLINE: |
{
"AccessTokenJwtType": "JWT",
"Discovery": {
"ShowKeySet": true
},
"Authentication": {
"CookieSameSiteMode": "Lax",
"CheckSessionCookieSameSiteMode": "Lax"
}
}
LOGIN_OPTIONS_INLINE: |
{
"AllowRememberLogin": false
}
LOGOUT_OPTIONS_INLINE: |
{
"AutomaticRedirectAfterSignOut": true
}
ASPNET_SERVICES_OPTIONS_INLINE: |
{
"ForwardedHeadersOptions": {
"ForwardedHeaders" : "All"
}
}
outline-redis:
container_name: outline-redis
image: redis:7
ports:
- ${PORT_REDIS}:6379
healthcheck:
test: redis-cli ping
interval: 10s
timeout: 30s
retries: 3
outline-postgres:
container_name: outline-postgres
image: postgres:17
ports:
- ${PORT_POSTGRES}:5432
volumes:
- outline-postgres:/var/lib/postgresql/data
healthcheck:
test: pg_isready
interval: 30s
timeout: 20s
retries: 3
environment:
POSTGRES_USER: ${COMMON}
POSTGRES_PASSWORD: ${COMMON}
POSTGRES_DB: ${COMMON}
+18 -118
View File
@@ -10,131 +10,31 @@ networks:
services: services:
outline: outline:
container_name: outline extends:
image: flameshikari/outline-ru:nightly file: docker-compose.prod.yml
build: . service: outline
network_mode: host image: !reset
pull_policy: always pull_policy: !reset
volumes: build:
- outline:/var/lib/outline/data dockerfile: !reset
depends_on: depends_on:
- outline-postgres - outline-postgres
- outline-redis - outline-redis
- outline-oidc - outline-oidc
environment: volumes:
FILE_STORAGE: local - ./tools/translation.json:/opt/outline/shared/i18n/locales/ru_RU/translation.json
FORCE_HTTPS: false
PORT: 10240
URL: http://localhost:10240
SECRET_KEY: deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
UTILS_SECRET: deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
REDIS_URL: redis://localhost:10242
DATABASE_URL: postgres://outline:outline@localhost:10243/outline
PGSSLMODE: disable
OIDC_ISSUER_URL: http://localhost:10241
OIDC_CLIENT_ID: outline
OIDC_CLIENT_SECRET: outline
# OIDC_AUTH_URI: http://localhost:10241/connect/authorize
# OIDC_TOKEN_URI: http://localhost:10241/connect/token
# OIDC_USERINFO_URI: http://localhost:10241/connect/userinfo
# OIDC_SCOPES: openid profile email
# OIDC_USERNAME_CLAIM: username
outline-oidc: outline-oidc:
container_name: outline-oidc extends:
image: ghcr.io/soluto/oidc-server-mock:0.11.0 file: docker-compose.prod.yml
ports: service: outline-oidc
- 10241:80
healthcheck:
test: curl -fs localhost/health || exit 1
start_period: 2s
interval: 1s
timeout: 100ms
retries: 10
environment:
ASPNETCORE_URLS: http://+:80
ASPNETCORE_ENVIRONMENT: Development
CLIENTS_CONFIGURATION_INLINE: |
[
{
"ClientId": "outline",
"ClientSecrets": ["outline"],
"RedirectUris": ["http://localhost:10240/auth/oidc.callback"],
"AllowedGrantTypes": ["authorization_code"],
"AllowedScopes": ["openid", "profile", "email"],
"RequirePkce": false
}
]
USERS_CONFIGURATION_INLINE: |
[
{
"SubjectId": "1",
"Username": "outline",
"Password": "outline",
"Claims": [
{
"Type": "email",
"Value": "mail@example.com",
"ValueType": "string"
},
{
"Type": "name",
"Value": "Outline",
"ValueType": "string"
}
]
}
]
SERVER_OPTIONS_INLINE: |
{
"AccessTokenJwtType": "JWT",
"Discovery": {
"ShowKeySet": true
},
"Authentication": {
"CookieSameSiteMode": "Lax",
"CheckSessionCookieSameSiteMode": "Lax"
}
}
LOGIN_OPTIONS_INLINE: |
{
"AllowRememberLogin": false
}
LOGOUT_OPTIONS_INLINE: |
{
"AutomaticRedirectAfterSignOut": true
}
ASPNET_SERVICES_OPTIONS_INLINE: |
{
"ForwardedHeadersOptions": {
"ForwardedHeaders" : "All"
}
}
outline-redis: outline-redis:
container_name: outline-redis extends:
image: redis:7 file: docker-compose.prod.yml
ports: service: outline-redis
- 10242:6379
healthcheck:
test: redis-cli ping
interval: 10s
timeout: 30s
retries: 3
outline-postgres: outline-postgres:
container_name: outline-postgres extends:
image: postgres:17 file: docker-compose.prod.yml
ports: service: outline-postgres
- 10243:5432
volumes:
- outline-postgres:/var/lib/postgresql/data
healthcheck:
test: pg_isready
interval: 30s
timeout: 20s
retries: 3
environment:
POSTGRES_USER: outline
POSTGRES_PASSWORD: outline
POSTGRES_DB: outline
+35
View File
@@ -0,0 +1,35 @@
diff --git a/server/routes/index.ts b/server/routes/index.ts
index 26fbded27..478de1922 100644
--- a/server/routes/index.ts
+++ b/server/routes/index.ts
@@ -103,7 +103,7 @@ router.get("/locales/:lng.json", async (ctx) => {
await send(ctx, path.join(lng, "translation.json"), {
setHeaders: (res, _, stats) => {
res.setHeader("Last-Modified", formatRFC7231(stats.mtime));
- res.setHeader("Cache-Control", `public, max-age=${7 * Day.seconds}`);
+ res.setHeader("Cache-Control", "no-store");
res.setHeader(
"ETag",
crypto.createHash("md5").update(stats.mtime.toISOString()).digest("hex")
diff --git a/vite.config.ts b/vite.config.ts
index 32b52d44e..26d611fee 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -45,6 +45,17 @@ export default () =>
: { strict: true },
},
plugins: [
+ {
+ name: 'reload',
+ configureServer(server) {
+ const { ws, watcher } = server;
+ watcher.on('change', file => {
+ if (file.endsWith('build/shared/i18n/locales/ru_RU/translation.json')) {
+ ws.send({ type: 'full-reload' });
+ }
+ });
+ },
+ },
// https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#readme
react({
babel: {