- Implemented StatusWidget component for embedding RideBus service status. - Created widget-client.js for client-side interaction with the status widget. - Developed install-widget-integrations script to automate integration into PWA, website, and admin projects. - Introduced server-side widget rendering in widget.mjs for generating status widget HTML. - Added comprehensive tests for widget functionality and integration.
56 lines
7.3 KiB
JavaScript
56 lines
7.3 KiB
JavaScript
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
||
import { resolve, dirname } from 'node:path';
|
||
import { createRequire } from 'node:module';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const projects = fileURLToPath(new URL('../../', import.meta.url));
|
||
const roots = { pwa: resolve(projects, 'ridebus-pwa'), website: resolve(projects, 'ridebus-website/ridebus-website'), admin: resolve(projects, 'polotsk-transit-api/frontend') };
|
||
// The landing uses the TypeScript JS compiler API; the status project uses
|
||
// the native TypeScript CLI, which does not expose transpileModule.
|
||
const ts = createRequire(resolve(roots.website, 'package.json'))('typescript');
|
||
const source = readFileSync(new URL('../integrations/StatusWidget.tsx', import.meta.url), 'utf8');
|
||
function save(root, path, value) { const target = resolve(root, path); mkdirSync(dirname(target), { recursive: true }); writeFileSync(target, value); }
|
||
function replace(root, path, before, after) {
|
||
const target = resolve(root, path), content = readFileSync(target, 'utf8');
|
||
if (content.includes(after)) return;
|
||
if (!content.includes(before)) throw new Error(`Integration anchor missing: ${target}`);
|
||
save(root, path, content.replace(before, after));
|
||
}
|
||
function append(root, path, value) {
|
||
const target = resolve(root, path), content = existsSync(target) ? readFileSync(target, 'utf8') : '';
|
||
if (!content.includes(value.trim())) save(root, path, `${content.trimEnd()}\n\n${value.trim()}\n`);
|
||
}
|
||
|
||
save(roots.pwa, 'src/components/StatusWidget.tsx', source);
|
||
replace(roots.pwa, 'src/App.tsx', "import { StopList, StopListItem } from './components/StopList';", "import { StopList, StopListItem } from './components/StopList';\nimport StatusWidget from './components/StatusWidget';");
|
||
replace(roots.pwa, 'src/App.tsx', '<div><NavLink to={`/about${urlSuffix}`}>', '<div><StatusWidget url={import.meta.env.VITE_STATUS_URL || (import.meta.env.DEV ? \'http://127.0.0.1:5188\' : undefined)} services={[\'pwa\', \'api\']} locale="ru" theme={theme.resolved} /><NavLink to={`/about${urlSuffix}`}>');
|
||
append(roots.pwa, 'src/styles.scss', '.footer > div { flex-wrap: wrap; row-gap: 12px; max-width: 100%; }');
|
||
append(roots.pwa, '.env.example', '# Public status hosting URL, embedded in the production bundle.\n# VITE_STATUS_URL=https://your-status-domain.example');
|
||
|
||
save(roots.website, 'components/statusWidget/StatusWidget.tsx', source);
|
||
replace(roots.website, 'components/footer/footer.tsx', "import { CursorEffect } from '@/components/cursor';", "import { CursorEffect } from '@/components/cursor';\nimport StatusWidget from '@/components/statusWidget/StatusWidget';");
|
||
replace(roots.website, 'components/footer/footer.tsx', ' <p className={styles.author}>', ' <StatusWidget url={process.env.NEXT_PUBLIC_STATUS_URL || (process.env.NODE_ENV === \'development\' ? \'http://127.0.0.1:5188\' : undefined)} locale={locale === \'ru\' ? \'ru\' : \'en\'} theme="light" services={[\'website\', \'api\']} />\n <p className={styles.author}>');
|
||
append(roots.website, '.env.example', '# Public status hosting URL, embedded at build time.\n# NEXT_PUBLIC_STATUS_URL=https://your-status-domain.example');
|
||
|
||
const jsx = ts.transpileModule(source, { compilerOptions: { jsx: ts.JsxEmit.Preserve, target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.ESNext } }).outputText;
|
||
save(roots.admin, 'src/components/StatusWidget.jsx', jsx);
|
||
replace(roots.admin, 'src/components/AppLayout.jsx', "import { useCity } from '../contexts/CityContext';", "import { useCity } from '../contexts/CityContext';\nimport StatusWidget from './StatusWidget';");
|
||
replace(roots.admin, 'src/components/AppLayout.jsx', ' <div className="sidebar-footer">', ' <div className="sidebar-footer">\n <StatusWidget url={import.meta.env.VITE_STATUS_URL || (import.meta.env.DEV ? \'http://127.0.0.1:5188\' : undefined)} services={[\'admin\', \'api\']} locale="ru" theme="light" />');
|
||
append(roots.admin, '.env.example', '# Public status hosting URL, embedded in the production bundle.\n# VITE_STATUS_URL=https://your-status-domain.example');
|
||
|
||
const docs = `## Виджет статуса RideBus\n\nВстроенный iframe показывает свежий статус этого сервиса и API. При недоступности мониторинга отображается отсутствие данных. Адрес хостинга задаётся публичной переменной сборки: ${'{ENV}'}. Если адрес не задан, production-виджет скрыт; в development используется http://127.0.0.1:5188. После изменения переменной нужна новая сборка.\n\nВиджет не получает сессию пользователя, API-ключи или данные приложения. Состояния, оформление, русская/английская локализация и обновления обслуживаются проектом ridebus-status.\n`;
|
||
append(roots.pwa, 'README.md', docs.replace('{ENV}', '`VITE_STATUS_URL`'));
|
||
append(roots.website, 'README.md', docs.replace('{ENV}', '`NEXT_PUBLIC_STATUS_URL`'));
|
||
append(roots.admin, 'README.md', docs.replace('{ENV}', '`VITE_STATUS_URL`'));
|
||
|
||
replace(roots.pwa, 'Dockerfile', 'COPY public ./public\nRUN npm run build', 'COPY public ./public\nARG VITE_STATUS_URL=\nENV VITE_STATUS_URL=$VITE_STATUS_URL\nRUN npm run build');
|
||
replace(roots.pwa, 'docker-compose.yml', ' dockerfile: Dockerfile\n restart:', ' dockerfile: Dockerfile\n args:\n VITE_STATUS_URL: ${VITE_STATUS_URL:-}\n restart:');
|
||
replace(roots.website, 'Dockerfile', 'COPY . .\nRUN yarn build', 'COPY . .\nARG NEXT_PUBLIC_STATUS_URL=\nENV NEXT_PUBLIC_STATUS_URL=$NEXT_PUBLIC_STATUS_URL\nRUN yarn build');
|
||
replace(roots.website, 'docker-compose.yml', ' build: .', ' build:\n context: .\n args:\n NEXT_PUBLIC_STATUS_URL: ${NEXT_PUBLIC_STATUS_URL:-}');
|
||
replace(roots.admin, 'Dockerfile', 'ENV VITE_API_URL=$VITE_API_URL', 'ENV VITE_API_URL=$VITE_API_URL\nARG VITE_STATUS_URL=\nENV VITE_STATUS_URL=$VITE_STATUS_URL');
|
||
replace(dirname(roots.admin), 'docker-compose.yml', ' VITE_API_URL: ${API_URL:-/api/v1}', ' VITE_API_URL: ${API_URL:-/api/v1}\n VITE_STATUS_URL: ${VITE_STATUS_URL:-}');
|
||
append(dirname(roots.admin), '.env.example', '# Public status hosting URL passed to the frontend build.\n# VITE_STATUS_URL=https://your-status-domain.example');
|
||
for (const root of Object.values(roots)) append(root, 'README.md', 'При запуске через Docker Compose укажите переменную URL в корневом `.env` проекта и пересоберите образ (`docker compose up -d --build`). URL передаётся как build argument; смена только runtime environment не обновляет уже собранный интерфейс.');
|
||
append(dirname(roots.admin), 'README.md', '## Виджет статуса\n\nАдминка включает виджет сервиса `admin` и API. Для Docker Compose задайте публичный `VITE_STATUS_URL` в корневом `.env` перед сборкой frontend. Для запуска без Docker — в `frontend/.env`. Подробности: [frontend/README.md](frontend/README.md).');
|
||
console.log('Status widget integrated into PWA, website and admin sources.');
|