Skip to content

Commit

Permalink
feat(client-only): add configuration mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
Heymdall committed Sep 25, 2024
1 parent 19a39e7 commit cf14904
Show file tree
Hide file tree
Showing 12 changed files with 198 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/strong-ghosts-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'arui-scripts': minor
---

Добавлен механизм работы с конфигурацией для client-only режима
70 changes: 68 additions & 2 deletions packages/arui-scripts/docs/client-only.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,74 @@
Он полностью отключает сборку сервера, добавляет [HtmlWebpackPlugin](https://webpack.js.org/plugins/html-webpack-plugin/) для генерации
простой html, а так же значительно упрощает докер-контейнер, собираемый в качестве артефакта.

По-умолчанию html, создаваемый скриптами будет иметь единственный div с id `react-app`. Вы модифицировать этот html
используя [механизм оверрайдов](./overrides.md).
По-умолчанию html, создаваемый скриптами будет иметь div с id `react-app`, подключенные скрипты, а так же специальный тег `<script>` для работы с конфигурацией.
Вы модифицировать этот html используя [механизм оверрайдов](./overrides.md).

docker-контейнер, формируемый командой [arui-scripts docker-build](./commands.md#docker-build) будет содержать
только собранные файлы. node_modules, все что лежит в вашем `src` и других папках проекта будет игнорироваться.

### Работа с конфигурацией
Поскольку в client-only режиме нет никакого сервера - возникает вопрос как получать хоть какую то конфигурацию, которая
будет различаться для разных сред.

Для этого для client-only режима arui-scripts предоставляет механизм env-config.

**Помните - такая конфигурация будет доступна любому пользователю - она не должна содержать приватных данных и секретных ключей.**

Он позволяет вам создать json-шаблон, который будет наполняться данными из env-переменных.

В качестве шаблона используется файл `env-config.json` из root папки вашего проекта. Все вхождения `${NAME}` в нем
будут заменены на значения из env-переменных, а сам файл станет доступен по адресу `/env-config.json`.

Этот же шаблон будет встроен в `index.html` проекта на место, помеченное как `<%= envConfig %>`. В шаблоне для html
по-умолчанию он добавляется в тег `<script id="env-settings" type="application/json">`.
Таким образом вы можете получить те же настройки без дополнительных запросов: `JSON.parse(document.getElementById('env-settings').innerText)`.

В дев-режиме env переменные берутся из того окружения, в котором запущен dev-server.
Собранное приложение будет получать настройки только при запуске докер-контейнера - скрипт `start.sh` обработает все шаблоны таким же образом, как и dev-сервер.

**Пример:**

Содержимое `/env-config.json` вашего проекта:
```json
{
"backendUrl": "${BACKEND_URL}",
"staticData": "something very important",
"unknownEnv": "${UNKNOWN_ENV}"
}
```

Пример запуска в dev-режиме:
```shell
BACKEND_URL=http://example.com yarn arui-scripts start
```

Пример запуска собранного docker-контейнера:
```shell
docker run --env BACKEND_URL=http://example.com -p 8080:8080 example:1.1.42 /src/start.sh
```

В обоих случаях по адресу http://localhost:8080/env-config.json будет такое содержимое:
```json
{
"backendUrl": "http://example.com",
"staticData": "something very important",
"unknownEnv": ""
}
```

С дефолтным html-шаблоном index.html будет выглядеть так:
```html
<!doctype html>
<html lang="ru">
<div id="react-app"></div>
<script id="env-settings" type="application/json">
{
"backendUrl": "http://example.com",
"staticData": "something very important",
"unknownEnv": ""
}
</script>
</body>
</html>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { getEnvConfigContent } from './get-env-config';

export function addEnvToHtmlTemplate(html: string) {
return html.replace('<%= envConfig %>', getEnvConfigContent());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Compiler } from 'webpack';

import { ENV_CONFIG_FILENAME } from './constants';
import { getEnvConfigContent } from './get-env-config';

export class ClientConfigPlugin {
protected cachedContent: string | null = null;

// eslint-disable-next-line class-methods-use-this
apply(compiler: Compiler) {
const pluginName = ClientConfigPlugin.name;

const { webpack } = compiler;

compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
// Tapping to the assets processing pipeline on a specific stage.
compilation.hooks.processAssets.tap(
{
name: pluginName,
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
() => {
const content = getEnvConfigContent();

compilation.emitAsset(
`../${ENV_CONFIG_FILENAME}`,
new webpack.sources.RawSource(content)
);
}
);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ENV_CONFIG_FILENAME = 'env-config.json';
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import fs from 'fs';
import path from 'path';

import { configs } from '../app-configs';

import { ENV_CONFIG_FILENAME } from './constants';

export function replaceTemplateVariables(template: string, variables: Record<string, string | undefined>) {
return template.replace(/\$\{(\w+)}/g, (match, varName) => variables[varName] || '');
}

let cachedEnvConfig: string | null = null;

export function getEnvConfigContent() {
if (cachedEnvConfig) {
return cachedEnvConfig;
}

const configTemplate = path.join(configs.cwd, ENV_CONFIG_FILENAME);

if (!fs.existsSync(configTemplate)) {
cachedEnvConfig = '{}';

return cachedEnvConfig
}

const templateContent = fs.readFileSync(configTemplate, 'utf8');

cachedEnvConfig = replaceTemplateVariables(templateContent, process.env);

return cachedEnvConfig
}
3 changes: 3 additions & 0 deletions packages/arui-scripts/src/configs/client-env-config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { ENV_CONFIG_FILENAME } from './constants'
export { ClientConfigPlugin } from './client-config-plugin';
export { addEnvToHtmlTemplate } from './add-env-to-html-template';
6 changes: 4 additions & 2 deletions packages/arui-scripts/src/configs/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from 'path';

import applyOverrides from './util/apply-overrides';
import { configs } from './app-configs';
import { ENV_CONFIG_FILENAME } from './client-env-config';

const serverProxyConfig = {
'/**': {
Expand Down Expand Up @@ -58,8 +59,9 @@ const devServerConfig = applyOverrides('devServer', {
},
devMiddleware: {
publicPath: `/${configs.publicPath}`,
// dev-сервер не может корректно обработать файлы, которые лежат на уровень выше чем publicPath, а html, создаваемый для client-only режима как раз такой
writeToDisk: (filename) => filename.endsWith('.html'),
// dev-сервер не может корректно обработать файлы, которые лежат на уровень выше чем publicPath, а html,
// создаваемый для client-only режима как раз такой. Так же работает и env-config.json
writeToDisk: (filename) => filename.endsWith('.html') || filename.endsWith(ENV_CONFIG_FILENAME),
},
static: [configs.serverOutputPath],
proxy: Object.assign(configs.proxy || {}, configs.clientOnly ? {} : serverProxyConfig),
Expand Down
6 changes: 5 additions & 1 deletion packages/arui-scripts/src/configs/webpack.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { getWebpackCacheDependencies } from './util/get-webpack-cache-dependenci
import { configs } from './app-configs';
import babelConf from './babel-client';
import { babelDependencies } from './babel-dependencies';
import { addEnvToHtmlTemplate, ClientConfigPlugin } from './client-env-config';
import { patchMainWebpackConfigForModules, patchWebpackConfigForCompat } from './modules';
import postcssConf from './postcss';
import { processAssetsPluginOutput } from './process-assets-plugin-output';
Expand Down Expand Up @@ -480,9 +481,12 @@ export const createSingleClientWebpackConfig = (
new AruiRuntimePlugin(),
configs.clientOnly &&
new HtmlWebpackPlugin({
templateContent: htmlTemplate,
templateContent: mode === 'dev'
? addEnvToHtmlTemplate(htmlTemplate)
: htmlTemplate, // оставляем env шаблоны как есть для прод сборки - их будет менять start.sh
filename: '../index.html',
}),
mode === 'dev' && configs.clientOnly && new ClientConfigPlugin(),

// production plugins:
mode === 'prod' && new WebpackManifestPlugin(),
Expand Down
2 changes: 1 addition & 1 deletion packages/arui-scripts/src/templates/dockerfile.template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ ADD $NGINX_CONF_LOCATION ${nginxConfTargetLocation}
${nginxNonRootPart}
${configs.runFromNonRootUser ? `ADD --chown=nginx:nginx ${appPathToAdd} ${appTargetPath}` : `ADD ${appPathToAdd} ${appTargetPath}`}
${configs.clientOnly ? 'COPY env-config.jso[n] /src/ # конструкция с [n] нужная чтобы копирование было опциональным. Если файла не будет - сборка не упадёт' : ''}
${configs.clientOnly ? 'CMD ["nginx"]' : ''}
`;

Expand Down
3 changes: 3 additions & 0 deletions packages/arui-scripts/src/templates/html.template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const template = `<!doctype html>
</head>
<body>
<div id="react-app"></div>
<script id="env-settings" type="application/json">
<%= envConfig %>
</script>
</body>
</html>`;

Expand Down
40 changes: 38 additions & 2 deletions packages/arui-scripts/src/templates/start.template.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import configs from '../configs/app-configs';
import { configs } from '../configs/app-configs';
import { ENV_CONFIG_FILENAME } from '../configs/client-env-config';
import applyOverrides from '../configs/util/apply-overrides';

const startTemplate = `#!/bin/sh
Expand Down Expand Up @@ -31,4 +32,39 @@ nginx &
node --max-old-space-size="$node_memory_limit" ./${configs.buildPath}/${configs.serverOutput}
`;

export default applyOverrides('start.sh', startTemplate);
const envConfigTargetPath = `/src/${configs.buildPath}/${ENV_CONFIG_FILENAME}`
const envConfigPath = `/src/${ENV_CONFIG_FILENAME}`;
const htmlPath = `/src/${configs.buildPath}/index.html`;

const clientOnlyStartTemplate = `#!/bin/sh
# Мы подставляем значения из env в env-config.json если он есть, и кладем его в публичную папку.
# Дополнительно подставляем контент полученного файла в index.html
# Так как контент env-config может быть многострочным - дополнительно обрабатываем его через awk.
if [ -f ${envConfigPath} ]; then
cat ${envConfigPath} \\
| envsubst \\
> ${envConfigTargetPath}
# Define the placeholder and the file paths
PLACEHOLDER='<%= envConfig %>'
SETTINGS_FILE='${envConfigTargetPath}'
TARGET_FILE='${htmlPath}'
# Escape the placeholder for sed usage
ESCAPED_PLACEHOLDER=$(echo "$PLACEHOLDER" | sed 's/[\\/&]/\\\\&/g')
# Read the content of the settings and prepare it for substitution
SETTINGS_CONTENT=$(awk '{printf "%s\\\\n", $0}' "$SETTINGS_FILE")
# Replace the placeholder in the target file with the content of settings
cat ${htmlPath} \\
| sed "s/$ESCAPED_PLACEHOLDER/$SETTINGS_CONTENT/" \\
> /tmp/index.html
mv /tmp/index.html ${htmlPath}
fi
nginx`;

export default applyOverrides('start.sh', configs.clientOnly ? clientOnlyStartTemplate : startTemplate);

0 comments on commit cf14904

Please sign in to comment.