Files
ridebus-status/scripts/install-widget-integrations.mjs
Zakhar Timoshenko cb1977b65f feat: add RideBus status widget integration
- 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.
2026-10-14 00:24:40 +03:00

56 lines
7.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.');