diff --git a/cli/bin/reactpress.js b/cli/bin/reactpress.js index b18b28f..67829e1 100755 --- a/cli/bin/reactpress.js +++ b/cli/bin/reactpress.js @@ -8,12 +8,14 @@ const { Command } = require('commander'); const path = require('path'); const chalk = require('chalk'); -const { ensureOriginalCwd, getMonorepoRoot } = require('../lib/root'); +const { brand, divider } = require('../ui/theme'); +const { ensureOriginalCwd } = require('../lib/root'); const { ensureProjectEnvironment, initMonorepoProject } = require('../lib/bootstrap'); const { runDev } = require('../lib/dev'); const { runApiDev } = require('../lib/api-dev'); const { runLifecycleCommand } = require('../lib/lifecycle'); const { runDockerCommand } = require('../lib/docker'); +const { runNginxCommand } = require('../lib/nginx'); const { printUnifiedStatus } = require('../lib/status'); const { runDoctor } = require('../lib/doctor'); const { runDbBackup } = require('../lib/db-backup'); @@ -24,7 +26,7 @@ const { getClientBin } = require('../lib/paths'); const { runInteractiveLoop } = require('../ui/interactive'); const { t } = require('../lib/i18n'); -const rootPkg = require(path.join(getMonorepoRoot(), 'package.json')); +const rootPkg = require(path.join(__dirname, '..', 'package.json')); const program = new Command(); @@ -176,6 +178,102 @@ dockerCmd await runDockerCommand('logs', ensureOriginalCwd(), service ? [service] : []); }); +const nginxCmd = program.command('nginx').description(t('cli.nginx.description')); + +function nginxActionOptions(cmd) { + return cmd.option('--prod', t('cli.nginx.prod')).option('-f, --force', t('cli.nginx.force')); +} + +nginxActionOptions(nginxCmd.command('ensure').description(t('cli.nginx.ensure'))).action(async (options) => { + try { + await runNginxCommand('ensure', ensureOriginalCwd(), [], options); + } catch (err) { + console.error(chalk.red('[reactpress]'), err.message || err); + process.exit(1); + } +}); + +nginxActionOptions(nginxCmd.command('up').description(t('cli.nginx.up'))).action(async (options) => { + try { + await runNginxCommand('up', ensureOriginalCwd(), [], options); + } catch (err) { + console.error(chalk.red('[reactpress]'), err.message || err); + process.exit(1); + } +}); + +nginxCmd + .command('down') + .alias('stop') + .description(t('cli.nginx.down')) + .option('--prod', t('cli.nginx.prod')) + .action(async (options) => { + try { + await runNginxCommand('down', ensureOriginalCwd(), [], options); + } catch (err) { + console.error(chalk.red('[reactpress]'), err.message || err); + process.exit(1); + } + }); + +nginxActionOptions(nginxCmd.command('restart').description(t('cli.nginx.restart'))).action(async (options) => { + try { + await runNginxCommand('restart', ensureOriginalCwd(), [], options); + } catch (err) { + console.error(chalk.red('[reactpress]'), err.message || err); + process.exit(1); + } +}); + +nginxCmd + .command('status') + .description(t('cli.nginx.status')) + .option('--prod', t('cli.nginx.prod')) + .action(async (options) => { + try { + await runNginxCommand('status', ensureOriginalCwd(), [], options); + } catch (err) { + console.error(chalk.red('[reactpress]'), err.message || err); + process.exit(1); + } + }); + +nginxCmd.command('logs').description(t('cli.nginx.logs')).action(async () => { + try { + await runNginxCommand('logs', ensureOriginalCwd()); + } catch (err) { + console.error(chalk.red('[reactpress]'), err.message || err); + process.exit(1); + } +}); + +nginxCmd.command('test').description(t('cli.nginx.test')).action(async () => { + try { + await runNginxCommand('test', ensureOriginalCwd()); + } catch (err) { + console.error(chalk.red('[reactpress]'), err.message || err); + process.exit(1); + } +}); + +nginxCmd.command('reload').description(t('cli.nginx.reload')).action(async () => { + try { + await runNginxCommand('reload', ensureOriginalCwd()); + } catch (err) { + console.error(chalk.red('[reactpress]'), err.message || err); + process.exit(1); + } +}); + +nginxCmd.command('open').description(t('cli.nginx.open')).action(async () => { + try { + await runNginxCommand('open', ensureOriginalCwd()); + } catch (err) { + console.error(chalk.red('[reactpress]'), err.message || err); + process.exit(1); + } +}); + program .command('status') .description(t('cli.status.description')) @@ -212,19 +310,16 @@ program .option('--build', t('cli.publish.build')) .option('--publish', t('cli.publish.publish')) .action(async (options) => { - const prev = process.argv.slice(); - const args = [process.argv[0], process.argv[1]]; - if (options.build) args.push('--build'); - else if (options.publish) args.push('--publish'); - else args.push('--publish'); - process.argv = args; try { - await require('../lib/publish').main(); + const publish = require('../lib/publish'); + if (options.build) { + await publish.buildPackages(); + return; + } + await publish.publishPackages(); } catch (err) { console.error(chalk.red('[reactpress]'), err.message || err); process.exit(1); - } finally { - process.argv = prev; } }); @@ -233,9 +328,14 @@ program .description(t('cli.start.description')) .action(async () => { const projectRoot = ensureOriginalCwd(); - const { spawn } = require('child_process'); + const { hasClient } = require('../lib/project-type'); const code = await runLifecycleCommand('start', projectRoot); if (code !== 0) process.exit(code); + if (!hasClient(projectRoot)) { + console.log(t('dev.standaloneHint')); + return; + } + const { spawn } = require('child_process'); const child = spawn('pnpm', ['run', '--dir', './client', 'start'], { stdio: 'inherit', shell: true, @@ -246,16 +346,23 @@ program program.on('--help', () => { console.log(''); - console.log(chalk.gray(t('cli.help.examples'))); - console.log(t('cli.help.interactive')); - console.log(t('cli.help.dev')); - console.log(t('cli.help.init')); - console.log(t('cli.help.server')); - console.log(t('cli.help.status')); - console.log(t('cli.help.doctor')); - console.log(t('cli.help.docker')); - console.log(t('cli.help.build')); - console.log(t('cli.help.publish')); + console.log(brand.bold(t('cli.help.examples'))); + console.log(divider(40)); + const lines = [ + t('cli.help.interactive'), + t('cli.help.dev'), + t('cli.help.init'), + t('cli.help.server'), + t('cli.help.status'), + t('cli.help.doctor'), + t('cli.help.docker'), + t('cli.help.nginx'), + t('cli.help.build'), + t('cli.help.publish'), + ]; + for (const line of lines) { + console.log(brand.dim(line)); + } console.log(''); }); diff --git a/cli/lib/api-dev.js b/cli/lib/api-dev.js index d6434ad..cac3fc4 100644 --- a/cli/lib/api-dev.js +++ b/cli/lib/api-dev.js @@ -1,8 +1,14 @@ -const { spawn, spawnSync } = require('child_process'); +const { spawn } = require('child_process'); const path = require('path'); const { ensureProjectEnvironment } = require('./bootstrap'); -const { isUsingMonorepoServer } = require('./paths'); -const { getMonorepoRoot } = require('./root'); +const { + getServerBin, + getServerDir, + isUsingMonorepoServer, + canStartLocalApi, +} = require('./paths'); +const { stopApi } = require('./lifecycle'); +const { ensureOriginalCwd } = require('./root'); const { t } = require('./i18n'); let apiChild; @@ -11,16 +17,13 @@ function stopApiDev(projectRoot) { if (apiChild && !apiChild.killed) { apiChild.kill('SIGTERM'); } - if (!isUsingMonorepoServer()) { - spawnSync('pnpm', ['exec', 'reactpress-cli', 'stop'], { - cwd: projectRoot, - stdio: 'inherit', - }); + if (!isUsingMonorepoServer(projectRoot)) { + stopApi(projectRoot); } } function startApiDev(projectRoot) { - if (isUsingMonorepoServer()) { + if (isUsingMonorepoServer(projectRoot)) { console.log(t('apiDev.modeServer')); apiChild = spawn('pnpm', ['run', '--dir', './server', 'dev'], { cwd: projectRoot, @@ -31,18 +34,19 @@ function startApiDev(projectRoot) { REACTPRESS_ORIGINAL_CWD: projectRoot, }, }); - } else { - console.log(t('apiDev.modeCli')); - const start = spawnSync('pnpm', ['exec', 'reactpress-cli', 'start'], { - cwd: projectRoot, + } else if (canStartLocalApi(projectRoot)) { + console.log(t('apiDev.modeBundled')); + apiChild = spawn(process.execPath, [getServerBin(projectRoot)], { + cwd: getServerDir(projectRoot), stdio: 'inherit', + env: { + ...process.env, + REACTPRESS_ORIGINAL_CWD: projectRoot, + }, }); - if (start.status !== 0) { - process.exit(start.status ?? 1); - } - console.log(t('apiDev.startedByCli')); - process.stdin.resume(); - return; + } else { + console.error(t('lifecycle.noServerAvailable')); + process.exit(1); } if (apiChild) { @@ -54,7 +58,7 @@ function startApiDev(projectRoot) { } } -async function runApiDev(projectRoot = process.env.REACTPRESS_ORIGINAL_CWD || getMonorepoRoot()) { +async function runApiDev(projectRoot = ensureOriginalCwd()) { try { await ensureProjectEnvironment(projectRoot); } catch (err) { diff --git a/cli/lib/bootstrap.js b/cli/lib/bootstrap.js index 909a2c9..9d5dd7b 100644 --- a/cli/lib/bootstrap.js +++ b/cli/lib/bootstrap.js @@ -1,7 +1,7 @@ const fs = require('fs'); const path = require('path'); const { pathToFileURL } = require('url'); -const { getMonorepoRoot, isMonorepoCheckout } = require('./root'); +const { ensureOriginalCwd, isMonorepoCheckout } = require('./root'); const { getCliPackageRoot } = require('./paths'); const { t } = require('./i18n'); @@ -68,7 +68,7 @@ async function initMonorepoProject(projectRoot, { force = false } = {}) { }; } -async function ensureProjectEnvironment(projectRoot = getMonorepoRoot()) { +async function ensureProjectEnvironment(projectRoot = ensureOriginalCwd()) { const root = path.resolve(projectRoot); const { setProjectCwd } = await importCliModule('utils/cli-context.js'); setProjectCwd(root); diff --git a/cli/lib/build.js b/cli/lib/build.js index 6036c42..ee70bc1 100644 --- a/cli/lib/build.js +++ b/cli/lib/build.js @@ -1,4 +1,7 @@ -const chalk = require('chalk'); +const fs = require('fs'); +const path = require('path'); +const ora = require('ora'); +const { brand, icon, ok, warn, label, chip } = require('../ui/theme'); const { runSync } = require('./spawn'); const { ensureOriginalCwd } = require('./root'); const { t } = require('./i18n'); @@ -22,6 +25,64 @@ const TARGETS = Object.keys(BUILD_STEPS); const buildChildEnv = { REACTPRESS_BUILD_ACTIVE: '1' }; +function readPackageScripts(packageJsonPath) { + try { + const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + return pkg.scripts || {}; + } catch { + return {}; + } +} + +/** Prefer workspace package scripts over root package.json aliases. */ +function resolveBuildInvocation(script, projectRoot) { + const root = path.resolve(projectRoot); + + if (script === 'build:toolkit') { + const toolkitDir = path.join(root, 'toolkit'); + if (fs.existsSync(path.join(toolkitDir, 'package.json'))) { + return { command: 'pnpm', args: ['run', 'build'], cwd: toolkitDir }; + } + const rootScripts = readPackageScripts(path.join(root, 'package.json')); + if (rootScripts['build:toolkit']) { + return { command: 'pnpm', args: ['run', 'build:toolkit'], cwd: root }; + } + return null; + } + + if (script === 'build:server') { + const serverDir = path.join(root, 'server'); + if (fs.existsSync(path.join(serverDir, 'package.json'))) { + return { command: 'pnpm', args: ['run', 'build'], cwd: serverDir }; + } + } + + if (script === 'build:client') { + const clientDir = path.join(root, 'client'); + if (fs.existsSync(path.join(clientDir, 'package.json'))) { + return { command: 'pnpm', args: ['run', 'build'], cwd: clientDir }; + } + } + + if (script === 'build:docs') { + const docsDir = path.join(root, 'docs'); + if (fs.existsSync(path.join(docsDir, 'package.json'))) { + return { command: 'pnpm', args: ['run', 'build'], cwd: docsDir }; + } + } + + const rootScripts = readPackageScripts(path.join(root, 'package.json')); + if (rootScripts[script]) { + return { command: 'pnpm', args: ['run', script], cwd: root }; + } + + return null; +} + +function stepBadge(current, total) { + return chip(`${current}/${total}`, brand.primary); +} + async function runBuild(target = 'all', projectRoot = ensureOriginalCwd()) { if (process.env.REACTPRESS_BUILD_ACTIVE === '1') { throw new Error(t('build.recursive')); @@ -40,8 +101,10 @@ async function runBuild(target = 'all', projectRoot = ensureOriginalCwd()) { const total = steps.length; const buildStarted = Date.now(); + console.log(''); if (total > 1) { - console.log(t('build.plan', { total })); + console.log(label(t('build.plan', { total }))); + console.log(''); } for (let i = 0; i < steps.length; i++) { @@ -51,25 +114,49 @@ async function runBuild(target = 'all', projectRoot = ensureOriginalCwd()) { } const current = i + 1; - const label = t(labelKey); + const stepLabel = t(labelKey); const stepStarted = Date.now(); + const badge = stepBadge(current, total); + + const invocation = resolveBuildInvocation(script, projectRoot); + if (!invocation) { + console.log(` ${badge} ${warn(t('build.stepSkipped', { label: stepLabel }))}`); + continue; + } + + const spinner = ora({ + text: `${badge} ${t('build.step', { current, total, label: stepLabel })}`, + color: 'magenta', + spinner: 'dots', + }).start(); - console.log(t('build.step', { current, total, label })); try { - runSync('pnpm', ['run', script], { cwd: projectRoot, env: buildChildEnv }); + runSync(invocation.command, invocation.args, { + cwd: invocation.cwd, + env: buildChildEnv, + }); } catch (err) { - console.error(chalk.red(t('build.stepFailed', { current, total, label }))); + spinner.fail(`${badge} ${t('build.stepFailed', { current, total, label: stepLabel })}`); throw err; } const seconds = ((Date.now() - stepStarted) / 1000).toFixed(1); - console.log(chalk.green(t('build.stepDone', { current, total, label, seconds }))); + spinner.succeed( + `${badge} ${ok(t('build.stepDone', { current, total, label: stepLabel, seconds }))}` + ); } if (total > 1) { const totalSeconds = ((Date.now() - buildStarted) / 1000).toFixed(1); - console.log(chalk.green(t('build.done', { seconds: totalSeconds }))); + console.log(''); + console.log(` ${icon.spark} ${ok(t('build.done', { seconds: totalSeconds }))}`); } + console.log(''); } -module.exports = { runBuild, TARGETS, BUILD_STEPS }; +module.exports = { + runBuild, + TARGETS, + BUILD_STEPS, + resolveBuildInvocation, +}; diff --git a/cli/lib/db-backup.js b/cli/lib/db-backup.js index 23152f0..729cb5f 100644 --- a/cli/lib/db-backup.js +++ b/cli/lib/db-backup.js @@ -3,6 +3,18 @@ const path = require('path'); const { execSync } = require('child_process'); const chalk = require('chalk'); const { t } = require('./i18n'); +const { mysqldumpFromDbContainer } = require('./docker'); + +function isLocalDbHost(host) { + const h = String(host || '').toLowerCase(); + return h === '127.0.0.1' || h === 'localhost' || h === '::1' || h === ''; +} + +function isMysqldumpNotFoundError(err) { + const msg = `${err && err.message ? err.message : ''}\n${err && err.stderr ? err.stderr : ''}`; + if (err && err.status === 127) return true; + return /command not found|not recognized as an internal or external command/i.test(msg); +} function parseEnv(projectRoot) { const envPath = path.join(projectRoot, '.env'); @@ -38,6 +50,15 @@ async function runDbBackup(projectRoot, outputPath) { console.log(chalk.green('[reactpress]'), t('db.backup.done')); return out; } catch (err) { + if (isMysqldumpNotFoundError(err) && isLocalDbHost(host)) { + const via = mysqldumpFromDbContainer(projectRoot, { user, password, database }); + if (via.ok) { + console.log(chalk.cyan('[reactpress]'), t('db.backup.viaDocker')); + fs.writeFileSync(out, via.stdout, 'utf8'); + console.log(chalk.green('[reactpress]'), t('db.backup.done')); + return out; + } + } console.error(chalk.red('[reactpress]'), t('db.backup.fail')); throw err; } diff --git a/cli/lib/dev-banner.js b/cli/lib/dev-banner.js index a683f02..876f720 100644 --- a/cli/lib/dev-banner.js +++ b/cli/lib/dev-banner.js @@ -1,4 +1,15 @@ -const chalk = require('chalk'); +const { + brand, + icon, + ok, + divider, + padRight, + terminalWidth, + gradientText, + palette, + pulseBar, + statusLights, +} = require('../ui/theme'); const { loadClientSiteUrl, loadServerSiteUrl, @@ -20,20 +31,41 @@ function getDevUrls(projectRoot) { }; } +function urlLine(key, url, { underline = true } = {}) { + const keyCol = brand.muted(padRight(key, 10)); + const value = underline ? brand.accent.underline(url) : brand.dim(url); + return ` ${brand.accent('▸ ')}${keyCol} ${value}`; +} + function printDevReadyBanner(projectRoot, { apiOnly = false } = {}) { const urls = getDevUrls(projectRoot); + const w = Math.min(terminalWidth() - 4, 56); + console.log(''); - console.log(chalk.bold.green(t('devBanner.ready'))); - console.log(chalk.gray(' ─────────────────────────────────────────')); + console.log( + ` ${icon.ok} ${gradientText(t('devBanner.ready'), [palette.green, palette.accent], { bold: true })} ${statusLights('online')}` + ); + console.log(` ${brand.primary('╔' + '═'.repeat(w) + '╗')}`); + if (!apiOnly) { - console.log(` ${chalk.cyan(t('devBanner.site'))} ${chalk.underline(urls.site)}`); - console.log(` ${chalk.cyan(t('devBanner.admin'))} ${chalk.underline(urls.admin)}`); + console.log(urlLine(t('devBanner.site'), urls.site)); + console.log(urlLine(t('devBanner.admin'), urls.admin)); + } + console.log(urlLine(t('devBanner.api'), urls.api)); + console.log(urlLine(t('devBanner.swagger'), urls.swagger)); + console.log(urlLine(t('devBanner.health'), urls.health, { underline: false })); + + const pulseWidth = Math.min(20, w - 4); + if (pulseWidth > 6) { + console.log( + ` ${brand.muted(' ')}${pulseBar(pulseWidth, pulseWidth)} ${brand.success(t('devBanner.allSystemsGo'))}` + ); } - console.log(` ${chalk.cyan('API')} ${chalk.underline(urls.api)}`); - console.log(` ${chalk.cyan('Swagger')} ${chalk.underline(urls.swagger)}`); - console.log(` ${chalk.cyan(t('devBanner.health'))} ${urls.health}`); - console.log(chalk.gray(' ─────────────────────────────────────────')); - console.log(chalk.gray(t('devBanner.hint'))); + + console.log(` ${brand.primary('╚' + '═'.repeat(w) + '╝')}`); + console.log( + ` ${brand.dim(t('devBanner.hint'))} ${brand.muted('·')} ${brand.dim(t('devBanner.shortcuts'))}` + ); console.log(''); } diff --git a/cli/lib/dev.js b/cli/lib/dev.js index c8b701b..838585e 100644 --- a/cli/lib/dev.js +++ b/cli/lib/dev.js @@ -1,18 +1,24 @@ const { spawn } = require('child_process'); const path = require('path'); +const ora = require('ora'); const { runBuild } = require('./build'); const { ensureProjectEnvironment } = require('./bootstrap'); const { loadServerSiteUrl, loadClientSiteUrl, waitForHttp } = require('./http'); const { printDevReadyBanner } = require('./dev-banner'); const { ensureOriginalCwd } = require('./root'); +const { detectProjectType, hasClient, hasToolkit } = require('./project-type'); const { t } = require('./i18n'); const CLIENT_READY_TIMEOUT_MS = 120_000; - const API_READY_TIMEOUT_MS = 180_000; function formatDevFailureHint() { - return [t('dev.nextSteps'), t('dev.nextDoctor'), t('dev.nextDocker'), t('dev.nextEnv')].join('\n'); + return [ + t('dev.nextSteps'), + t('dev.nextDoctor'), + t('dev.nextDocker'), + t('dev.nextEnv'), + ].join('\n'); } let apiChild; @@ -22,22 +28,17 @@ let shuttingDown = false; function shutdown(signal = 'SIGINT') { if (shuttingDown) return; shuttingDown = true; - if (webChild && !webChild.killed) { - webChild.kill(signal); - } - if (apiChild && !apiChild.killed) { - apiChild.kill(signal); - } + if (webChild && !webChild.killed) webChild.kill(signal); + if (apiChild && !apiChild.killed) apiChild.kill(signal); } async function buildToolkit(projectRoot) { + if (!hasToolkit(projectRoot)) return; await runBuild('toolkit', projectRoot); } -async function startDevStack(projectRoot) { - const serverUrl = loadServerSiteUrl(projectRoot); +function spawnApi(projectRoot) { const apiDevRunner = path.join(__dirname, 'api-dev-runner.js'); - console.log(t('dev.startingApi')); apiChild = spawn(process.execPath, [apiDevRunner], { stdio: 'inherit', @@ -53,25 +54,28 @@ async function startDevStack(projectRoot) { process.exit(code ?? 0); return; } - if (webChild && !webChild.killed) { - webChild.kill('SIGINT'); - } + if (webChild && !webChild.killed) webChild.kill('SIGINT'); process.exit(code ?? 1); }); +} - console.log(t('dev.waitingApi', { url: serverUrl })); +async function waitForApiReady(projectRoot) { + const serverUrl = loadServerSiteUrl(projectRoot); + const spinner = ora({ + text: t('dev.waitingApi', { url: serverUrl }), + color: 'magenta', + spinner: 'dots', + }).start(); const ready = await waitForHttp(serverUrl, API_READY_TIMEOUT_MS); if (!ready) { - console.error( - t('dev.apiTimeout', { seconds: API_READY_TIMEOUT_MS / 1000 }), - ); + spinner.fail(t('dev.apiTimeout', { seconds: API_READY_TIMEOUT_MS / 1000 })); shutdown('SIGINT'); process.exit(1); } + spinner.succeed(t('dev.apiReady')); +} - printDevReadyBanner(projectRoot, { apiOnly: true }); - - console.log(t('dev.apiReady')); +function spawnClient(projectRoot) { webChild = spawn('pnpm', ['run', '--dir', './client', 'dev'], { stdio: 'inherit', shell: true, @@ -87,28 +91,38 @@ async function startDevStack(projectRoot) { t('dev.clientSlow', { seconds: CLIENT_READY_TIMEOUT_MS / 1000, url: clientUrl, - }), + }) ); } }); webChild.on('close', (code) => { - if (!shuttingDown) { - shutdown('SIGINT'); - } + if (!shuttingDown) shutdown('SIGINT'); process.exit(code ?? 0); }); } +async function startDevStack(projectRoot) { + const includeClient = hasClient(projectRoot); + + spawnApi(projectRoot); + await waitForApiReady(projectRoot); + printDevReadyBanner(projectRoot, { apiOnly: !includeClient }); + + if (!includeClient) { + console.log(t('dev.standaloneHint')); + return; + } + spawnClient(projectRoot); +} + async function runDev(projectRoot = ensureOriginalCwd()) { process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); try { const result = await ensureProjectEnvironment(projectRoot); - if (result.message) { - console.log(`[reactpress] ${result.message}`); - } + if (result.message) console.log(`[reactpress] ${result.message}`); } catch (err) { console.error(t('dev.envFailed'), err.message || err); console.error(formatDevFailureHint()); @@ -119,4 +133,9 @@ async function runDev(projectRoot = ensureOriginalCwd()) { await startDevStack(projectRoot); } -module.exports = { runDev, buildToolkit, startDevStack }; +module.exports = { + runDev, + buildToolkit, + startDevStack, + detectProjectType, +}; diff --git a/cli/lib/docker.js b/cli/lib/docker.js index ad6ac2d..cf4932b 100644 --- a/cli/lib/docker.js +++ b/cli/lib/docker.js @@ -1,6 +1,9 @@ -const { spawn, execSync } = require('child_process'); +const fs = require('fs'); const path = require('path'); +const { spawn, execSync, spawnSync } = require('child_process'); +const ora = require('ora'); const { ensureOriginalCwd } = require('./root'); +const { detectProjectType, hasClient } = require('./project-type'); const { t } = require('./i18n'); function isDockerRunning() { @@ -12,18 +15,58 @@ function isDockerRunning() { } } +function pickDockerComposeCommand() { + const v2 = spawnSync('docker', ['compose', 'version'], { stdio: 'ignore' }); + if (v2.status === 0) return { command: 'docker', baseArgs: ['compose'] }; + + const v1 = spawnSync('docker-compose', ['version'], { stdio: 'ignore' }); + if (v1.status === 0) return { command: 'docker-compose', baseArgs: [] }; + + return { command: 'docker', baseArgs: ['compose'] }; +} + +/** + * Resolve which docker-compose file to use for the current project. + * + * - Monorepo checkouts use `docker-compose.dev.yml` at the repo root. + * - Standalone projects use `.reactpress/docker-compose.yml` (managed by init). + * + * @returns {{ composeFile: string, cwd: string, type: 'monorepo' | 'standalone' }} + */ +function resolveComposeContext(projectRoot) { + const type = detectProjectType(projectRoot); + if (type === 'monorepo') { + const composeFile = path.join(projectRoot, 'docker-compose.dev.yml'); + if (fs.existsSync(composeFile)) { + return { composeFile, cwd: projectRoot, type }; + } + } + const standaloneCompose = path.join(projectRoot, '.reactpress', 'docker-compose.yml'); + if (fs.existsSync(standaloneCompose)) { + return { composeFile: standaloneCompose, cwd: path.dirname(standaloneCompose), type: 'standalone' }; + } + const fallback = path.join(projectRoot, 'docker-compose.dev.yml'); + return { composeFile: fallback, cwd: projectRoot, type }; +} + +function runCompose(args, ctx, options = {}) { + const { command, baseArgs } = pickDockerComposeCommand(); + return spawnSync( + command, + [...baseArgs, '-f', ctx.composeFile, ...args], + { stdio: options.stdio ?? 'inherit', cwd: ctx.cwd, ...options } + ); +} + function stopDockerServices(projectRoot) { console.log(t('docker.stopping')); - try { - execSync('docker-compose -f docker-compose.dev.yml down', { - stdio: 'inherit', - cwd: projectRoot, - }); - console.log(t('docker.stopped')); - } catch (error) { - console.error(t('docker.stopFailed'), error.message); - throw error; + const ctx = resolveComposeContext(projectRoot); + const result = runCompose(['down'], ctx); + if (result.status !== 0) { + console.error(t('docker.stopFailed')); + throw new Error(t('docker.stopFailed')); } + console.log(t('docker.stopped')); } function startDockerServices(projectRoot) { @@ -31,42 +74,86 @@ function startDockerServices(projectRoot) { if (!isDockerRunning()) { throw new Error(t('docker.notRunning')); } - execSync('docker-compose -f docker-compose.dev.yml up -d', { - stdio: 'inherit', - cwd: projectRoot, - }); + try { + const { ensureNginxConfig } = require('./nginx'); + const { configPath, created } = ensureNginxConfig(projectRoot, { mode: 'dev' }); + if (created) { + console.log(t('nginx.configCreated', { path: configPath })); + } + } catch (err) { + console.warn(t('nginx.ensureWarn', { message: err.message || err })); + } + const ctx = resolveComposeContext(projectRoot); + const result = runCompose(['up', '-d'], ctx); + if (result.status !== 0) { + throw new Error(t('docker.notRunning')); + } console.log(t('docker.started')); } -async function waitForMysql(maxAttempts = 30) { - console.log(t('docker.waitingMysql')); +function resolveDbContainerName(ctx, projectRoot) { + if (ctx.type === 'standalone') return 'reactpress_cli_db'; + return 'reactpress_db'; +} + +function resolveDbCredentialsFromEnv(projectRoot) { + const envPath = path.join(projectRoot, '.env'); + let user = 'reactpress'; + let password = 'reactpress'; + try { + const content = fs.readFileSync(envPath, 'utf8'); + const u = content.match(/^DB_USER=(.+)$/m); + const p = content.match(/^(DB_PASSWD|DB_PASSWORD)=(.+)$/m); + if (u) user = u[1].trim().replace(/^['"]|['"]$/g, ''); + if (p) password = p[2].trim().replace(/^['"]|['"]$/g, ''); + } catch { + // ignore + } + return { user, password }; +} + +async function waitForMysql(projectRoot, maxAttempts = 30) { + const ctx = resolveComposeContext(projectRoot); + const container = resolveDbContainerName(ctx, projectRoot); + const { user, password } = resolveDbCredentialsFromEnv(projectRoot); + + const spinner = ora({ + text: t('docker.waitingMysql'), + color: 'magenta', + spinner: 'dots', + }).start(); + let attempts = 0; while (attempts < maxAttempts) { - try { - execSync('docker exec reactpress_db mysql -u reactpress -preactpress -e "SELECT 1"', { - stdio: 'ignore', - }); - console.log(t('docker.mysqlReady')); + const probe = spawnSync( + 'docker', + ['exec', container, 'mysql', `-u${user}`, `-p${password}`, '-e', 'SELECT 1'], + { stdio: 'ignore' } + ); + if (probe.status === 0) { + spinner.succeed(t('docker.mysqlReady')); return true; - } catch { - attempts += 1; - if (attempts % 5 === 0) { - console.log(t('docker.waitingMysqlProgress', { attempts, max: maxAttempts })); - } - await new Promise((r) => setTimeout(r, 1000)); } + attempts += 1; + spinner.text = t('docker.waitingMysqlProgress', { attempts, max: maxAttempts }); + await new Promise((r) => setTimeout(r, 1000)); } - console.error(t('docker.mysqlTimeout')); + spinner.fail(t('docker.mysqlTimeout')); return false; } async function dockerStartWithDev(projectRoot) { startDockerServices(projectRoot); - const ready = await waitForMysql(); + const ready = await waitForMysql(projectRoot); if (!ready) { throw new Error(t('docker.mysqlNotReady')); } + if (!hasClient(projectRoot)) { + console.log(t('dev.standaloneHint')); + return; + } + const { buildToolkit } = require('./dev'); await buildToolkit(projectRoot); @@ -108,39 +195,67 @@ async function dockerStartWithDev(projectRoot) { }); } +/** + * Run mysqldump inside the compose `db` container (MySQL image ships mysqldump). + * Used when the host has no `mysqldump` binary but Docker DB is running. + * + * @returns {{ ok: true, stdout: string } | { ok: false, stderr: string }} + */ +function mysqldumpFromDbContainer(projectRoot, { user, password, database }) { + const ctx = resolveComposeContext(projectRoot); + if (!fs.existsSync(ctx.composeFile)) { + return { ok: false, stderr: 'compose file missing' }; + } + if (!isDockerRunning()) { + return { ok: false, stderr: 'docker not running' }; + } + const container = resolveDbContainerName(ctx, projectRoot); + const res = spawnSync( + 'docker', + ['exec', container, 'mysqldump', `-u${user}`, `-p${password}`, database], + { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 } + ); + if (res.error) { + return { ok: false, stderr: res.error.message }; + } + if (res.status !== 0) { + return { ok: false, stderr: res.stderr || res.stdout || `exit ${res.status}` }; + } + return { ok: true, stdout: res.stdout }; +} + async function runDockerCommand(command, projectRoot = ensureOriginalCwd(), extraArgs = []) { + const ctx = resolveComposeContext(projectRoot); switch (command) { case 'up': startDockerServices(projectRoot); - await waitForMysql(); + await waitForMysql(projectRoot); return; case 'down': + case 'stop': stopDockerServices(projectRoot); return; case 'start': await dockerStartWithDev(projectRoot); return; - case 'stop': - stopDockerServices(projectRoot); - return; case 'restart': stopDockerServices(projectRoot); await new Promise((r) => setTimeout(r, 2000)); startDockerServices(projectRoot); - await waitForMysql(); + await waitForMysql(projectRoot); return; - case 'status': - execSync('docker-compose -f docker-compose.dev.yml ps', { - stdio: 'inherit', - cwd: projectRoot, - }); + case 'status': { + const res = runCompose(['ps'], ctx); + if (res.status !== 0) { + throw new Error(t('docker.unknownCommand', { command: 'ps' })); + } return; + } case 'logs': { - const service = extraArgs[0] || ''; - execSync(`docker-compose -f docker-compose.dev.yml logs -f ${service}`.trim(), { - stdio: 'inherit', - cwd: projectRoot, - }); + const service = extraArgs[0]; + const args = ['logs', '-f']; + if (service) args.push(service); + runCompose(args, ctx); return; } default: @@ -154,4 +269,7 @@ module.exports = { stopDockerServices, waitForMysql, isDockerRunning, + resolveComposeContext, + pickDockerComposeCommand, + mysqldumpFromDbContainer, }; diff --git a/cli/lib/doctor.js b/cli/lib/doctor.js index 8798fc6..13d0e8e 100644 --- a/cli/lib/doctor.js +++ b/cli/lib/doctor.js @@ -2,9 +2,21 @@ const fs = require('fs'); const net = require('net'); const path = require('path'); const { execSync } = require('child_process'); -const chalk = require('chalk'); +const ora = require('ora'); +const { + brand, + icon, + ok, + warn, + divider, + sectionHeader, + terminalWidth, + gradientText, + palette, +} = require('../ui/theme'); const { getHealthUrl, checkHealth } = require('./http'); const { isDockerRunning } = require('./docker'); +const { checkNginx } = require('./nginx'); const { envFileStatus } = require('./status'); const { t } = require('./i18n'); @@ -64,6 +76,16 @@ async function checkPorts(projectRoot) { const env = parseEnv(projectRoot); const apiPort = parseInt(env.SERVER_PORT || '3002', 10); const clientPort = parseInt(env.CLIENT_PORT || '3001', 10); + + const healthUrl = getHealthUrl(projectRoot); + const apiHealth = await checkHealth(healthUrl); + if (apiHealth.ok) { + return { + ok: true, + message: t('doctor.portOk', { apiPort, clientPort }), + }; + } + const [apiBusy, clientBusy] = await Promise.all([checkPort(apiPort), checkPort(clientPort)]); const issues = []; if (apiBusy) issues.push(t('doctor.portApiBusy', { port: apiPort })); @@ -152,6 +174,21 @@ function checkPnpm() { } } +async function runCheckWithSpinner(name, run) { + const spinner = ora({ + text: t('doctor.checking', { name }), + color: 'magenta', + spinner: 'dots', + }).start(); + const result = await run(); + if (result.ok) { + spinner.stop(); + } else { + spinner.stop(); + } + return result; +} + async function runDoctor(projectRoot) { const env = envFileStatus(projectRoot); const checks = [ @@ -174,34 +211,53 @@ async function runDoctor(projectRoot) { }), }, { name: 'Docker', run: () => checkDocker() }, + { name: t('doctor.check.nginx'), run: () => checkNginx(projectRoot) }, { name: t('doctor.check.ports'), run: () => checkPorts(projectRoot) }, { name: t('doctor.check.database'), run: () => checkDatabase(projectRoot) }, { name: t('doctor.check.api'), run: () => checkApiHealth(projectRoot) }, ]; + const w = Math.min(terminalWidth() - 4, 52); + const results = []; + const fixes = []; + console.log(''); - console.log(chalk.bold.cyan(' ReactPress Doctor')); - console.log(chalk.gray(t('doctor.project', { path: projectRoot }))); - console.log(chalk.gray(' ─────────────────────────────────────')); + console.log( + ` ${gradientText(t('doctor.title'), [palette.primary, palette.accent], { bold: true })} ${brand.dim(t('doctor.subtitle'))}` + ); + console.log(` ${brand.dim(t('doctor.project', { path: projectRoot }))}`); + console.log(` ${divider(w)}`); - let failed = 0; for (const { name, run } of checks) { - const result = await run(); - const icon = result.ok ? chalk.green('✓') : chalk.red('✗'); - console.log(` ${icon} ${chalk.bold(name)} ${result.message}`); + const result = await runCheckWithSpinner(name, run); + results.push({ name, ...result }); + const mark = result.ok ? icon.ok : icon.fail; + const msgColor = result.ok ? brand.dim : brand.warn; + console.log(` ${mark} ${brand.bold(name)} ${msgColor(result.message)}`); if (!result.ok && result.fix) { - console.log(chalk.yellow(` → ${result.fix}`)); - failed += 1; - } else if (!result.ok) { - failed += 1; + fixes.push({ name, fix: result.fix }); } } - console.log(chalk.gray(' ─────────────────────────────────────')); + const passed = results.filter((r) => r.ok).length; + const failed = results.length - passed; + + console.log(` ${divider(w)}`); + console.log( + ` ${brand.dim(t('doctor.summary', { passed, failed, total: results.length }))}` + ); + if (failed === 0) { - console.log(chalk.green(t('doctor.allPass'))); + console.log(` ${ok(t('doctor.allPass'))}`); } else { - console.log(chalk.yellow(t('doctor.failed', { count: failed }))); + console.log(` ${warn(t('doctor.failed', { count: failed }))}`); + if (fixes.length) { + console.log(''); + console.log(sectionHeader(t('doctor.fixesHeader'))); + for (const { name, fix } of fixes) { + console.log(` ${brand.primary('→')} ${brand.dim(name)} ${brand.warn(fix)}`); + } + } } console.log(''); return failed === 0 ? 0 : 1; diff --git a/cli/lib/http.js b/cli/lib/http.js index 23b23e1..403c486 100644 --- a/cli/lib/http.js +++ b/cli/lib/http.js @@ -50,13 +50,13 @@ function getHealthUrl(projectRoot) { return `${base}${prefix}/health`; } -function checkHealth(url, timeoutMs = 3000) { +function probeHttp(url, timeoutMs = 3000) { return new Promise((resolve) => { let parsed; try { parsed = new URL(url); } catch { - resolve({ ok: false }); + resolve({ ok: false, statusCode: 0, data: null }); return; } const port = parsed.port || (parsed.protocol === 'https:' ? 443 : 80); @@ -87,13 +87,48 @@ function checkHealth(url, timeoutMs = 3000) { ); req.on('timeout', () => { req.destroy(); - resolve({ ok: false }); + resolve({ ok: false, statusCode: 0, data: null }); }); - req.on('error', () => resolve({ ok: false })); + req.on('error', () => resolve({ ok: false, statusCode: 0, data: null })); req.end(); }); } +/** + * Health probe: prefers `/api/health` JSON; falls back to API prefix (e.g. Swagger) + * for older bundled servers that omit the health route. + */ +async function checkHealth(url, timeoutMs = 3000) { + const primary = await probeHttp(url, timeoutMs); + if (primary.ok) return primary; + + if (primary.statusCode === 404 || primary.statusCode === 0) { + try { + const parsed = new URL(url); + const prefix = parsed.pathname.replace(/\/health\/?$/, '') || '/api'; + const candidates = [ + `${parsed.origin}${prefix}/`, + `${parsed.origin}${prefix}`, + parsed.origin, + ]; + for (const fallback of candidates) { + const alt = await probeHttp(fallback, timeoutMs); + if (alt.statusCode === 200) { + return { + ok: true, + statusCode: 200, + data: { status: 'ok', database: 'unknown' }, + }; + } + } + } catch { + // ignore + } + } + + return primary; +} + function isHttpResponding(url, timeoutMs = 2000) { return new Promise((resolve) => { let parsed; diff --git a/cli/lib/i18n/strings.js b/cli/lib/i18n/strings.js index 1001fff..4469cc2 100644 --- a/cli/lib/i18n/strings.js +++ b/cli/lib/i18n/strings.js @@ -26,6 +26,19 @@ const STRINGS = { 'cli.docker.restart': 'Restart Docker services', 'cli.docker.status': 'Docker container status', 'cli.docker.logs': 'Docker logs (db | nginx)', + 'cli.nginx.description': 'Nginx reverse proxy (unified entry on :80)', + 'cli.nginx.ensure': 'Write default nginx config if missing', + 'cli.nginx.up': 'Start nginx container', + 'cli.nginx.down': 'Stop nginx container', + 'cli.nginx.restart': 'Restart nginx container', + 'cli.nginx.status': 'Show nginx container and config status', + 'cli.nginx.logs': 'Follow nginx container logs', + 'cli.nginx.test': 'Validate nginx config inside container (nginx -t)', + 'cli.nginx.reload': 'Reload nginx after config change', + 'cli.nginx.open': 'Open nginx entry URL in browser', + 'cli.nginx.prod': 'Use production compose + nginx.conf (monorepo only)', + 'cli.nginx.force': 'Overwrite existing nginx config from template', + 'cli.help.nginx': ' reactpress nginx up Start reverse proxy (:80)', 'cli.status.description': 'Project, API, frontend, and Docker status', 'cli.doctor.description': 'Diagnose Node, Docker, ports, database, and API health', 'cli.db.description': 'Database operations', @@ -47,6 +60,19 @@ const STRINGS = { 'cli.help.publish': ' reactpress publish Publish npm packages', 'cli.build.target': 'Build target: toolkit | server | client | docs | all', 'banner.subtitle': ' · Full-stack publishing CLI ', + /** Left label for the decorative pulse bar (not a URL — the repo link + * lives directly under the title bar at the top of the card). */ + 'banner.pulseLabel': 'Setup', + 'banner.pulseReady': 'READY', + 'banner.pulsePending': 'INIT', + 'banner.label.mode': 'MODE', + 'banner.label.path': 'PATH', + 'banner.mode.standalone': 'STANDALONE', + 'banner.mode.monorepo': 'MONOREPO', + 'banner.mode.uninitialized': 'UNINITIALIZED', + 'banner.systemLabel': 'SYSTEM', + 'banner.systemOnline': 'ONLINE', + 'banner.systemPending': 'PENDING', 'menu.dev': 'Zero-config dev (env + DB + API + frontend)', 'menu.init': 'Initialize project (.reactpress + .env + database)', 'menu.status': 'View project status', @@ -73,6 +99,55 @@ const STRINGS = { 'menu.done': 'Done', 'menu.opening': 'Opening {url}', 'menu.goodbye': ' Goodbye.', + 'menu.section.run': 'Run', + 'menu.section.lifecycle': 'Lifecycle', + 'menu.section.build': 'Build & Deploy', + 'menu.section.tools': 'Tools', + 'menu.tip': 'Tip: arrow keys to navigate, Enter to select, Ctrl+C to quit.', + 'menu.shortcuts': '↑/↓ navigate · enter select · esc back · ctrl+c quit', + 'menu.statusHeader': 'Status', + 'menu.contextStandalone': 'Project mode · standalone (using bundled API)', + 'menu.contextMonorepo': 'Project mode · monorepo (server/src + client/)', + 'menu.contextUnknown': 'Project mode · not initialized (run `init`)', + 'menu.statusApi': 'API {status}', + 'menu.statusDb': 'DB {status}', + 'menu.statusDocker': 'Docker {status}', + 'menu.statusLabelApi': 'API', + 'menu.statusLabelDb': 'DB', + 'menu.statusLabelDocker': 'Docker', + 'menu.statusChecking': 'checking…', + 'menu.startingApi': 'Starting API…', + 'menu.stoppingApi': 'Stopping API…', + 'menu.restartingApi': 'Restarting API…', + 'menu.statusOn': 'online', + 'menu.statusOff': 'offline', + 'menu.statusReady': 'ready', + 'menu.statusNotReady': 'not ready', + 'menu.statusYes': 'available', + 'menu.statusNo': 'unavailable', + 'menu.hint.dev': 'API + DB + frontend', + 'menu.hint.init': '.reactpress + .env', + 'menu.hint.status': 'all services overview', + 'menu.hint.doctor': 'environment diagnostics', + 'menu.hint.devApi': 'watch mode', + 'menu.hint.devClient': 'Next.js dev', + 'menu.hint.serverStart': 'production background', + 'menu.hint.serverStop': '', + 'menu.hint.serverRestart': '', + 'menu.hint.build': 'production output', + 'menu.hint.dockerStart': 'DB + nginx + dev stack', + 'menu.hint.dockerUp': 'database only', + 'menu.hint.dockerStop': '', + 'menu.nginxUp': 'Start nginx reverse proxy (:80)', + 'menu.nginxOpen': 'Open nginx entry in browser', + 'menu.nginxReload': 'Reload nginx after editing config', + 'menu.hint.nginxUp': 'unified entry :80', + 'menu.hint.nginxOpen': 'http://localhost', + 'menu.hint.nginxReload': 'nginx -t && reload', + 'menu.hint.openAdmin': 'opens in browser', + 'menu.hint.publish': 'maintainers only', + 'menu.hint.exit': '', + 'menu.actionPrefix': 'action', 'dev.startingApi': '[reactpress] Starting API (first run may install deps)…', 'dev.waitingApi': '[reactpress] Waiting for API: {url}', 'dev.apiTimeout': '[reactpress] API not ready within {seconds}s.\n → Run reactpress doctor\n → Embedded MySQL: reactpress docker up\n → Check DB_* and SERVER_SITE_URL in .env', @@ -84,11 +159,16 @@ const STRINGS = { 'dev.nextDoctor': ' → reactpress doctor Diagnostics', 'dev.nextDocker': ' → reactpress docker up Start embedded MySQL', 'dev.nextEnv': ' → Check DB_* and SERVER_SITE_URL in .env', - 'devBanner.ready': ' ✓ ReactPress dev environment is ready', + 'dev.standaloneHint': '[reactpress] Standalone project: only API is started here; build your own frontend separately.', + 'devBanner.ready': 'ReactPress dev environment is ready', 'devBanner.site': 'Site', 'devBanner.admin': 'Admin', + 'devBanner.api': 'API', + 'devBanner.swagger': 'Swagger', 'devBanner.health': 'Health', - 'devBanner.hint': ' Diagnostics: reactpress doctor · Status: reactpress status', + 'devBanner.hint': 'Diagnostics: reactpress doctor · Status: reactpress status', + 'devBanner.shortcuts': 'Ctrl+C stop', + 'devBanner.allSystemsGo': 'ALL SYSTEMS GO', 'doctor.nodeBad': 'Node.js {version} (requires ≥ 18)', 'doctor.nodeFix': 'Install Node.js 18+: https://nodejs.org/', 'doctor.dockerOk': 'Docker engine is available', @@ -119,17 +199,22 @@ const STRINGS = { 'doctor.envOk': '.env exists', 'doctor.envBad': 'Missing .env', 'doctor.envFix': 'Run reactpress init or reactpress config --apply', - 'doctor.project': ' Project: {path}', - 'doctor.allPass': ' All checks passed. You can start developing.', - 'doctor.failed': ' {count} item(s) need attention.', - 'status.title': ' ReactPress project status', - 'status.dir': ' Project {path}', - 'status.apiSource': ' API source {source}', + 'doctor.project': 'Project {path}', + 'doctor.allPass': 'All checks passed. You can start developing.', + 'doctor.failed': '{count} item(s) need attention.', + 'doctor.title': 'ReactPress Doctor', + 'doctor.subtitle': 'environment diagnostics', + 'doctor.checking': 'Checking {name}…', + 'doctor.summary': '{passed} passed · {failed} failed · {total} total', + 'doctor.fixesHeader': 'Suggested fixes', + 'status.title': 'ReactPress project status', + 'status.dir': 'Project {path}', + 'status.apiSource': 'API source {source}', 'status.apiSource.monorepo': 'monorepo server/', 'status.apiSource.bundle': 'reactpress-cli', - 'status.configOk': '.reactpress/config.json ✓', + 'status.configOk': '.reactpress/config.json', 'status.configBad': 'not initialized', - 'status.envOk': '.env ✓', + 'status.envOk': '.env', 'status.envBad': 'missing .env', 'status.apiOnline': 'online', 'status.apiOffline': 'offline', @@ -137,10 +222,24 @@ const STRINGS = { 'status.dbUp': 'connected', 'status.dbDown': 'unavailable', 'status.pidRunning': '(running)', - 'status.frontend': ' Frontend', - 'status.docker': ' Docker', + 'status.frontend': 'Frontend', + 'status.docker': 'Docker', 'status.dockerUp': 'available', 'status.dockerDown': 'not running', + 'status.section.project': 'Project', + 'status.section.api': 'API service', + 'status.section.frontend': 'Frontend', + 'status.section.docker': 'Docker', + 'status.field.url': 'URL', + 'status.field.http': 'HTTP', + 'status.field.health': 'Health', + 'status.field.database': 'Database', + 'status.field.pid': 'PID', + 'status.field.engine': 'Engine', + 'status.field.config': 'Config', + 'status.field.env': '.env', + 'status.field.source': 'Source', + 'status.field.dir': 'Directory', 'bootstrap.configReady': 'Config exists and database is ready.', 'bootstrap.projectDbPending': 'Project created, but database is not ready: {message}. Start Docker and run reactpress dev again.', 'bootstrap.ready': 'ReactPress dev environment is ready (config + database).', @@ -151,7 +250,9 @@ const STRINGS = { 'bootstrap.dbReady': 'Database is ready', 'db.backup.to': 'Backing up database to {path}', 'db.backup.done': 'Backup complete', - 'db.backup.fail': 'mysqldump failed; ensure MySQL client is installed and .env is correct', + 'db.backup.viaDocker': 'mysqldump not on PATH; using mysqldump inside the db container…', + 'db.backup.fail': + 'mysqldump failed; install a MySQL client (e.g. brew install mysql-client), or ensure Docker db is running for automatic container backup', 'common.done': 'Done', 'common.yes': 'yes', 'common.no': 'no', @@ -160,15 +261,16 @@ const STRINGS = { 'lifecycle.apiStopped': '[reactpress] API process stopped (pid {pid})', 'lifecycle.stopPidFailed': '[reactpress] Failed to stop pid {pid}:', 'lifecycle.apiAlreadyRunning': '[reactpress] API already running (pid {pid})', - 'lifecycle.noServerSrc': '[reactpress] server/src not found, falling back to reactpress-cli start', + 'lifecycle.noServerAvailable': '[reactpress] No API runtime found. Reinstall @fecommunity/reactpress or run from a project with server/src.', 'lifecycle.startingLocalApi': '[reactpress] Starting local API (server/)…', + 'lifecycle.startingBundledApi': '[reactpress] Starting bundled API…', 'lifecycle.apiStartedBg': '[reactpress] API started in background (pid {pid})', 'lifecycle.apiTimeout120': '[reactpress] API not ready within 120s: {url}', 'lifecycle.apiReady': '[reactpress] API ready: {url}', 'lifecycle.apiStatusTitle': '[reactpress] API status', 'lifecycle.source': ' Source: {source}', 'lifecycle.source.monorepo': 'monorepo server/', - 'lifecycle.source.bundle': 'reactpress-cli bundle', + 'lifecycle.source.bundle': 'bundled API (@fecommunity/reactpress)', 'lifecycle.pidFile': ' PID file: {path}', 'lifecycle.recordedPid': ' Recorded PID: {pid}', 'lifecycle.processAlive': ' Process alive: {alive}', @@ -188,22 +290,50 @@ const STRINGS = { 'docker.mysqlTimeout': '[reactpress] MySQL not ready within timeout.', 'docker.mysqlNotReady': 'MySQL is not ready', 'docker.startDevStack': '[reactpress] Starting API + frontend (Docker MySQL)…', - 'docker.visitUrls': '[reactpress] Visit: http://localhost:8080 (nginx) / http://localhost:3001 (client)', + 'docker.visitUrls': '[reactpress] Visit: http://localhost (nginx) / http://localhost:3001 (client)', 'docker.devProcessExit': 'Dev process exited with code {code}', 'docker.unknownCommand': 'Unknown docker command: {command}', + 'nginx.configCreated': '[reactpress] Created nginx config: {path}', + 'nginx.configExists': '[reactpress] Nginx config already exists: {path}', + 'nginx.ensureWarn': '[reactpress] Could not ensure nginx config: {message}', + 'nginx.started': '[reactpress] Nginx started — {url}', + 'nginx.configPath': '[reactpress] Config: {path}', + 'nginx.stopped': '[reactpress] Nginx stopped.', + 'nginx.startFailed': 'Failed to start nginx container', + 'nginx.prodMonorepoOnly': 'Production nginx (--prod) requires monorepo with docker-compose.prod.yml', + 'nginx.statusTitle': '[reactpress] Nginx status', + 'nginx.statusContainer': ' Container {name}: {running}', + 'nginx.statusConfig': ' Config {path}: {exists}', + 'nginx.statusUrl': ' Entry {url} (port {port})', + 'nginx.statusMode': ' Mode: {mode}', + 'nginx.notRunning': 'Nginx container is not running. Run: reactpress nginx up', + 'nginx.testOk': '[reactpress] Nginx config test passed.', + 'nginx.testFailed': 'Nginx config test failed', + 'nginx.reloadOk': '[reactpress] Nginx reloaded.', + 'nginx.reloadFailed': 'Nginx reload failed', + 'nginx.opening': '[reactpress] Opening {url}', + 'nginx.unknownCommand': 'Unknown nginx command: {command}', + 'nginx.templateMissing': 'Bundled nginx template missing: {path}', + 'nginx.doctorSkippedDocker': 'Skipped (Docker not running)', + 'nginx.doctorSkippedNotRunning': 'Not started (optional: reactpress nginx up)', + 'nginx.doctorNotRunningFix': 'reactpress nginx up (or reactpress docker up)', + 'nginx.doctorOk': 'Nginx healthy ({url}/health)', + 'nginx.doctorUnhealthy': 'Nginx running but /health failed ({url})', + 'nginx.doctorUnhealthyFix': 'Ensure client (:3001) and API (:3002) are running; reactpress nginx reload', + 'doctor.check.nginx': 'Nginx proxy', 'apiDev.modeServer': '[reactpress] Dev mode: server/ (nest start --watch)', - 'apiDev.modeCli': '[reactpress] Dev mode: reactpress-cli (server/src not found)', - 'apiDev.startedByCli': '[reactpress] API started via reactpress-cli.', + 'apiDev.modeBundled': '[reactpress] Dev mode: bundled API (built-in server)', 'apiDev.ctrlCHint': '[reactpress] Press Ctrl+C to stop API.', 'apiDev.stopHint': '[reactpress] Stop separately: reactpress server stop', 'build.unknownTarget': 'Unknown build target: {target}. Available: {available}', 'build.recursive': 'Build recursion detected (pnpm run build must not call itself). Use build:toolkit, build:server, or build:client.', 'build.forbiddenScript': 'Invalid build script "{script}"; use granular scripts like build:toolkit.', - 'build.stepFailed': '[reactpress] [{current}/{total}] {label} failed', - 'build.plan': '[reactpress] Production build — {total} step(s): toolkit → server → client', - 'build.step': '[reactpress] [{current}/{total}] {label}…', - 'build.stepDone': '[reactpress] [{current}/{total}] {label} ✓ ({seconds}s)', - 'build.done': '[reactpress] Build finished in {seconds}s', + 'build.stepFailed': '[{current}/{total}] {label} failed', + 'build.plan': 'Production build — {total} step(s): toolkit → server → client', + 'build.step': '[{current}/{total}] {label}', + 'build.stepDone': '[{current}/{total}] {label} ({seconds}s)', + 'build.stepSkipped': 'skipped {label} (not part of this project layout)', + 'build.done': 'Build finished in {seconds}s', 'build.label.toolkit': 'Toolkit', 'build.label.server': 'API (server)', 'build.label.client': 'Frontend (client)', @@ -317,6 +447,19 @@ const STRINGS = { 'cli.docker.restart': '重启 Docker 服务', 'cli.docker.status': '查看 Docker 容器状态', 'cli.docker.logs': '查看 Docker 日志 (db | nginx)', + 'cli.nginx.description': 'Nginx 反向代理(统一入口 :80)', + 'cli.nginx.ensure': '若缺失则生成默认 nginx 配置', + 'cli.nginx.up': '启动 nginx 容器', + 'cli.nginx.down': '停止 nginx 容器', + 'cli.nginx.restart': '重启 nginx 容器', + 'cli.nginx.status': '查看 nginx 容器与配置状态', + 'cli.nginx.logs': '跟踪 nginx 容器日志', + 'cli.nginx.test': '在容器内校验配置 (nginx -t)', + 'cli.nginx.reload': '修改配置后热加载 nginx', + 'cli.nginx.open': '在浏览器打开 nginx 入口', + 'cli.nginx.prod': '使用生产 compose + nginx.conf(仅 monorepo)', + 'cli.nginx.force': '用模板覆盖已有 nginx 配置', + 'cli.help.nginx': ' reactpress nginx up 启动反向代理 (:80)', 'cli.status.description': '查看项目、API、前端、Docker 综合状态', 'cli.doctor.description': '诊断环境:Node、Docker、端口、数据库、API 健康', 'cli.db.description': '数据库运维', @@ -338,6 +481,18 @@ const STRINGS = { 'cli.help.publish': ' reactpress publish 发布 npm 包', 'cli.build.target': '构建目标: toolkit | server | client | docs | all', 'banner.subtitle': ' · 全栈发布平台 CLI ', + /** 左侧装饰性进度条标签(不是网址;仓库地址放在卡片顶部 Title 正下方)。 */ + 'banner.pulseLabel': '准备', + 'banner.pulseReady': '就绪', + 'banner.pulsePending': '初始化', + 'banner.label.mode': '模式', + 'banner.label.path': '路径', + 'banner.mode.standalone': '独立项目', + 'banner.mode.monorepo': 'MONOREPO', + 'banner.mode.uninitialized': '未初始化', + 'banner.systemLabel': '系统', + 'banner.systemOnline': '在线', + 'banner.systemPending': '准备中', 'menu.dev': '零配置开发 (env + DB + API + 前端)', 'menu.init': '初始化项目 (.reactpress + .env + 数据库)', 'menu.status': '查看项目状态', @@ -364,6 +519,55 @@ const STRINGS = { 'menu.done': '完成', 'menu.opening': '打开 {url}', 'menu.goodbye': ' 再见。', + 'menu.section.run': '运行', + 'menu.section.lifecycle': '生命周期', + 'menu.section.build': '构建与部署', + 'menu.section.tools': '工具', + 'menu.tip': '提示:上下方向键选择,回车确认,Ctrl+C 退出。', + 'menu.shortcuts': '↑/↓ 选择 · 回车 确认 · esc 返回 · Ctrl+C 退出', + 'menu.statusHeader': '当前状态', + 'menu.contextStandalone': '项目类型 · 独立项目(使用内置 API)', + 'menu.contextMonorepo': '项目类型 · monorepo(server/src + client/)', + 'menu.contextUnknown': '项目类型 · 未初始化(请先运行 init)', + 'menu.statusApi': 'API {status}', + 'menu.statusDb': '数据库 {status}', + 'menu.statusDocker': 'Docker {status}', + 'menu.statusLabelApi': 'API', + 'menu.statusLabelDb': '数据库', + 'menu.statusLabelDocker': 'Docker', + 'menu.statusChecking': '检测中…', + 'menu.startingApi': '正在启动 API…', + 'menu.stoppingApi': '正在停止 API…', + 'menu.restartingApi': '正在重启 API…', + 'menu.statusOn': '在线', + 'menu.statusOff': '离线', + 'menu.statusReady': '就绪', + 'menu.statusNotReady': '未就绪', + 'menu.statusYes': '可用', + 'menu.statusNo': '不可用', + 'menu.hint.dev': 'API + 数据库 + 前端', + 'menu.hint.init': '生成 .reactpress + .env', + 'menu.hint.status': '所有服务概览', + 'menu.hint.doctor': '环境健康检查', + 'menu.hint.devApi': 'watch 模式', + 'menu.hint.devClient': 'Next.js 开发', + 'menu.hint.serverStart': '后台生产模式', + 'menu.hint.serverStop': '', + 'menu.hint.serverRestart': '', + 'menu.hint.build': '生产构建产物', + 'menu.hint.dockerStart': '数据库 + nginx + 全栈', + 'menu.hint.dockerUp': '仅数据库', + 'menu.hint.dockerStop': '', + 'menu.nginxUp': '启动 nginx 反向代理 (:80)', + 'menu.nginxOpen': '在浏览器打开 nginx 入口', + 'menu.nginxReload': '修改配置后重载 nginx', + 'menu.hint.nginxUp': '统一入口 :80', + 'menu.hint.nginxOpen': 'http://localhost', + 'menu.hint.nginxReload': 'nginx -t 后 reload', + 'menu.hint.openAdmin': '在浏览器打开', + 'menu.hint.publish': '仅维护者使用', + 'menu.hint.exit': '', + 'menu.actionPrefix': '操作', 'dev.startingApi': '[reactpress] 正在启动 API(首次可能需安装依赖,请稍候)…', 'dev.waitingApi': '[reactpress] 等待 API 就绪: {url}', 'dev.apiTimeout': '[reactpress] API 在 {seconds}s 内未就绪。\n → 运行 reactpress doctor 查看详情\n → 嵌入式 MySQL:reactpress docker up\n → 检查 .env 中 DB_* 与 SERVER_SITE_URL', @@ -375,11 +579,16 @@ const STRINGS = { 'dev.nextDoctor': ' → reactpress doctor 环境诊断', 'dev.nextDocker': ' → reactpress docker up 启动嵌入式 MySQL', 'dev.nextEnv': ' → 检查 .env 中 DB_* 与 SERVER_SITE_URL', - 'devBanner.ready': ' ✓ ReactPress 开发环境已就绪', + 'dev.standaloneHint': '[reactpress] 独立项目:当前目录仅启动 API,前端请单独构建。', + 'devBanner.ready': 'ReactPress 开发环境已就绪', 'devBanner.site': '前台', 'devBanner.admin': '管理端', + 'devBanner.api': 'API', + 'devBanner.swagger': 'Swagger', 'devBanner.health': '健康检查', - 'devBanner.hint': ' 诊断: reactpress doctor · 状态: reactpress status', + 'devBanner.hint': '诊断: reactpress doctor · 状态: reactpress status', + 'devBanner.shortcuts': 'Ctrl+C 停止', + 'devBanner.allSystemsGo': '一切就绪', 'doctor.nodeBad': 'Node.js {version}(需要 ≥ 18)', 'doctor.nodeFix': '请安装 Node.js 18+:https://nodejs.org/', 'doctor.dockerOk': 'Docker 引擎可用', @@ -410,17 +619,22 @@ const STRINGS = { 'doctor.envOk': '.env 存在', 'doctor.envBad': '缺少 .env', 'doctor.envFix': '运行 reactpress init 或 reactpress config --apply', - 'doctor.project': ' 项目: {path}', - 'doctor.allPass': ' 全部检查通过,可以开始开发。', - 'doctor.failed': ' {count} 项需要处理。', - 'status.title': ' ReactPress 项目状态', - 'status.dir': ' 项目目录 {path}', - 'status.apiSource': ' API 来源 {source}', + 'doctor.project': '项目目录 {path}', + 'doctor.allPass': '全部检查通过,可以开始开发。', + 'doctor.failed': '{count} 项需要处理。', + 'doctor.title': 'ReactPress Doctor', + 'doctor.subtitle': '环境健康检查', + 'doctor.checking': '正在检查 {name}…', + 'doctor.summary': '通过 {passed} · 失败 {failed} · 共 {total} 项', + 'doctor.fixesHeader': '修复建议', + 'status.title': 'ReactPress 项目状态', + 'status.dir': '项目目录 {path}', + 'status.apiSource': 'API 来源 {source}', 'status.apiSource.monorepo': 'monorepo server/', 'status.apiSource.bundle': 'reactpress-cli', - 'status.configOk': '.reactpress/config.json ✓', + 'status.configOk': '.reactpress/config.json', 'status.configBad': '未初始化', - 'status.envOk': '.env ✓', + 'status.envOk': '.env', 'status.envBad': '缺少 .env', 'status.apiOnline': '在线', 'status.apiOffline': '离线', @@ -428,10 +642,24 @@ const STRINGS = { 'status.dbUp': '连通', 'status.dbDown': '不可用', 'status.pidRunning': '(运行中)', - 'status.frontend': ' 前端', - 'status.docker': ' Docker', + 'status.frontend': '前端', + 'status.docker': 'Docker', 'status.dockerUp': '可用', 'status.dockerDown': '未运行', + 'status.section.project': '项目信息', + 'status.section.api': 'API 服务', + 'status.section.frontend': '前端', + 'status.section.docker': 'Docker', + 'status.field.url': 'URL', + 'status.field.http': 'HTTP', + 'status.field.health': '健康', + 'status.field.database': '数据库', + 'status.field.pid': 'PID', + 'status.field.engine': '引擎', + 'status.field.config': '配置', + 'status.field.env': '环境', + 'status.field.source': '来源', + 'status.field.dir': '目录', 'bootstrap.configReady': '配置已存在,数据库已就绪。', 'bootstrap.projectDbPending': '项目已创建,但数据库未就绪: {message}。请确认 Docker 已启动后重试 reactpress dev。', 'bootstrap.ready': 'ReactPress 开发环境已就绪(配置 + 数据库)。', @@ -442,7 +670,9 @@ const STRINGS = { 'bootstrap.dbReady': '数据库已就绪', 'db.backup.to': '备份数据库到 {path}', 'db.backup.done': '备份完成', - 'db.backup.fail': 'mysqldump 失败,请确认已安装 MySQL 客户端且 .env 正确', + 'db.backup.viaDocker': '本机未找到 mysqldump,改用 Docker 内 db 容器的 mysqldump…', + 'db.backup.fail': + 'mysqldump 失败:请安装 MySQL 客户端(如 brew install mysql-client),或确保 Docker 数据库已运行以便自动在容器内备份', 'common.done': '完成', 'common.yes': '是', 'common.no': '否', @@ -451,15 +681,16 @@ const STRINGS = { 'lifecycle.apiStopped': '[reactpress] 已停止 API 进程 (pid {pid})', 'lifecycle.stopPidFailed': '[reactpress] 停止 pid {pid} 失败:', 'lifecycle.apiAlreadyRunning': '[reactpress] API 已在运行 (pid {pid})', - 'lifecycle.noServerSrc': '[reactpress] 未检测到 server/src,回退到 reactpress-cli start', + 'lifecycle.noServerAvailable': '[reactpress] 未找到可用的 API 运行时。请重新安装 @fecommunity/reactpress,或在含 server/src 的项目目录中运行。', 'lifecycle.startingLocalApi': '[reactpress] 正在启动本地 API (server/)…', + 'lifecycle.startingBundledApi': '[reactpress] 正在启动内置 API…', 'lifecycle.apiStartedBg': '[reactpress] API 已后台启动 (pid {pid})', 'lifecycle.apiTimeout120': '[reactpress] API 在 120s 内未就绪: {url}', 'lifecycle.apiReady': '[reactpress] API 已就绪: {url}', 'lifecycle.apiStatusTitle': '[reactpress] API 状态', 'lifecycle.source': ' 来源: {source}', 'lifecycle.source.monorepo': 'monorepo server/', - 'lifecycle.source.bundle': 'reactpress-cli bundle', + 'lifecycle.source.bundle': '内置 API (@fecommunity/reactpress)', 'lifecycle.pidFile': ' PID 文件: {path}', 'lifecycle.recordedPid': ' 记录 PID: {pid}', 'lifecycle.processAlive': ' 进程存活: {alive}', @@ -479,22 +710,50 @@ const STRINGS = { 'docker.mysqlTimeout': '[reactpress] MySQL 在超时时间内未就绪。', 'docker.mysqlNotReady': 'MySQL 未就绪', 'docker.startDevStack': '[reactpress] 启动 API + 前端 (Docker MySQL)…', - 'docker.visitUrls': '[reactpress] 访问: http://localhost:8080 (nginx) / http://localhost:3001 (client)', + 'docker.visitUrls': '[reactpress] 访问: http://localhost (nginx) / http://localhost:3001 (client)', 'docker.devProcessExit': '开发进程退出: {code}', 'docker.unknownCommand': '未知 docker 命令: {command}', + 'nginx.configCreated': '[reactpress] 已生成 nginx 配置: {path}', + 'nginx.configExists': '[reactpress] nginx 配置已存在: {path}', + 'nginx.ensureWarn': '[reactpress] 无法确保 nginx 配置: {message}', + 'nginx.started': '[reactpress] Nginx 已启动 — {url}', + 'nginx.configPath': '[reactpress] 配置: {path}', + 'nginx.stopped': '[reactpress] Nginx 已停止。', + 'nginx.startFailed': '启动 nginx 容器失败', + 'nginx.prodMonorepoOnly': '生产 nginx(--prod)需要 monorepo 且存在 docker-compose.prod.yml', + 'nginx.statusTitle': '[reactpress] Nginx 状态', + 'nginx.statusContainer': ' 容器 {name}: {running}', + 'nginx.statusConfig': ' 配置 {path}: {exists}', + 'nginx.statusUrl': ' 入口 {url} (端口 {port})', + 'nginx.statusMode': ' 模式: {mode}', + 'nginx.notRunning': 'Nginx 容器未运行。请执行: reactpress nginx up', + 'nginx.testOk': '[reactpress] Nginx 配置校验通过。', + 'nginx.testFailed': 'Nginx 配置校验失败', + 'nginx.reloadOk': '[reactpress] Nginx 已重载。', + 'nginx.reloadFailed': 'Nginx 重载失败', + 'nginx.opening': '[reactpress] 正在打开 {url}', + 'nginx.unknownCommand': '未知 nginx 命令: {command}', + 'nginx.templateMissing': '内置 nginx 模板缺失: {path}', + 'nginx.doctorSkippedDocker': '已跳过(Docker 未运行)', + 'nginx.doctorSkippedNotRunning': '未启动(可选: reactpress nginx up)', + 'nginx.doctorNotRunningFix': 'reactpress nginx up(或 reactpress docker up)', + 'nginx.doctorOk': 'Nginx 健康 ({url}/health)', + 'nginx.doctorUnhealthy': 'Nginx 在运行但 /health 失败 ({url})', + 'nginx.doctorUnhealthyFix': '确认前端 (:3001) 与 API (:3002) 已启动;可执行 reactpress nginx reload', + 'doctor.check.nginx': 'Nginx 代理', 'apiDev.modeServer': '[reactpress] 开发模式: server/ (nest start --watch)', - 'apiDev.modeCli': '[reactpress] 开发模式: reactpress-cli(未找到 server/src)', - 'apiDev.startedByCli': '[reactpress] API 已由 reactpress-cli 启动。', + 'apiDev.modeBundled': '[reactpress] 开发模式: 内置 API(随包附带)', 'apiDev.ctrlCHint': '[reactpress] 按 Ctrl+C 停止 API。', 'apiDev.stopHint': '[reactpress] 单独停止: reactpress server stop', 'build.unknownTarget': '未知构建目标: {target},可选: {available}', 'build.recursive': '检测到构建递归(pnpm run build 不能再次调用自身)。请使用 build:toolkit、build:server 或 build:client。', 'build.forbiddenScript': '无效的构建脚本 "{script}",请使用 build:toolkit 等细分脚本。', - 'build.stepFailed': '[reactpress] [{current}/{total}] {label} 失败', - 'build.plan': '[reactpress] 生产构建 — 共 {total} 步:toolkit → server → client', - 'build.step': '[reactpress] [{current}/{total}] {label}…', - 'build.stepDone': '[reactpress] [{current}/{total}] {label} ✓ ({seconds}s)', - 'build.done': '[reactpress] 构建完成,耗时 {seconds}s', + 'build.stepFailed': '[{current}/{total}] {label} 失败', + 'build.plan': '生产构建 — 共 {total} 步:toolkit → server → client', + 'build.step': '[{current}/{total}] {label}', + 'build.stepDone': '[{current}/{total}] {label} ({seconds}s)', + 'build.stepSkipped': '已跳过 {label}(当前项目无对应源码包)', + 'build.done': '构建完成,耗时 {seconds}s', 'build.label.toolkit': 'Toolkit', 'build.label.server': 'API (server)', 'build.label.client': '前端 (client)', diff --git a/cli/lib/lifecycle.js b/cli/lib/lifecycle.js index f1de03c..e1c2ce7 100644 --- a/cli/lib/lifecycle.js +++ b/cli/lib/lifecycle.js @@ -1,16 +1,51 @@ -const { spawn, spawnSync } = require('child_process'); +const { spawn } = require('child_process'); +const ora = require('ora'); const { ensureProjectEnvironment } = require('./bootstrap'); const { loadServerSiteUrl, waitForHttp } = require('./http'); const { getServerBin, getServerDir, isUsingMonorepoServer, + canStartLocalApi, getPidFile, } = require('./paths'); +const net = require('net'); const { readPid, isProcessRunning, clearPidFile, writePid } = require('./process'); -const { getMonorepoRoot } = require('./root'); +const { ensureOriginalCwd } = require('./root'); const { t } = require('./i18n'); +function parseServerPort(projectRoot) { + try { + const url = new URL(loadServerSiteUrl(projectRoot)); + return Number(url.port) || 3002; + } catch { + return 3002; + } +} + +function isPortBusy(port, host = '127.0.0.1') { + return new Promise((resolve) => { + const socket = net.createConnection({ port, host }, () => { + socket.destroy(); + resolve(true); + }); + socket.on('error', () => resolve(false)); + socket.setTimeout(800, () => { + socket.destroy(); + resolve(false); + }); + }); +} + +async function waitForPortFree(port, timeoutMs = 8000) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (!(await isPortBusy(port))) return true; + await new Promise((r) => setTimeout(r, 200)); + } + return false; +} + async function ensureConfig(projectRoot) { try { await ensureProjectEnvironment(projectRoot); @@ -46,18 +81,19 @@ async function startApi(projectRoot, { wait = true } = {}) { } clearPidFile(projectRoot); - if (!isUsingMonorepoServer()) { - console.warn(t('lifecycle.noServerSrc')); - const start = spawnSync('pnpm', ['exec', 'reactpress-cli', 'start'], { - cwd: projectRoot, - stdio: 'inherit', - }); - return start.status ?? 1; + if (!canStartLocalApi(projectRoot)) { + console.error(t('lifecycle.noServerAvailable')); + return 1; + } + + if (isUsingMonorepoServer(projectRoot)) { + console.log(t('lifecycle.startingLocalApi')); + } else { + console.log(t('lifecycle.startingBundledApi')); } - console.log(t('lifecycle.startingLocalApi')); - const child = spawn(process.execPath, [getServerBin()], { - cwd: getServerDir(), + const child = spawn(process.execPath, [getServerBin(projectRoot)], { + cwd: getServerDir(projectRoot), detached: true, stdio: 'ignore', env: { @@ -75,12 +111,17 @@ async function startApi(projectRoot, { wait = true } = {}) { } const serverUrl = loadServerSiteUrl(projectRoot); + const spinner = ora({ + text: t('dev.waitingApi', { url: serverUrl }), + color: 'magenta', + spinner: 'dots', + }).start(); const ready = await waitForHttp(serverUrl); if (!ready) { - console.error(t('lifecycle.apiTimeout120', { url: serverUrl })); + spinner.fail(t('lifecycle.apiTimeout120', { url: serverUrl })); return 1; } - console.log(t('lifecycle.apiReady', { url: serverUrl })); + spinner.succeed(t('lifecycle.apiReady', { url: serverUrl })); return 0; } @@ -90,7 +131,7 @@ async function statusApi(projectRoot) { const { isHttpResponding } = require('./http'); const httpOk = await isHttpResponding(serverUrl); - const source = isUsingMonorepoServer() + const source = isUsingMonorepoServer(projectRoot) ? t('lifecycle.source.monorepo') : t('lifecycle.source.bundle'); @@ -119,7 +160,7 @@ async function statusApi(projectRoot) { ); } -async function runLifecycleCommand(command, projectRoot = process.env.REACTPRESS_ORIGINAL_CWD || getMonorepoRoot()) { +async function runLifecycleCommand(command, projectRoot = ensureOriginalCwd()) { switch (command) { case 'start': return startApi(projectRoot, { wait: true }); @@ -127,22 +168,11 @@ async function runLifecycleCommand(command, projectRoot = process.env.REACTPRESS return startApi(projectRoot, { wait: false }); case 'stop': stopApi(projectRoot); - if (!isUsingMonorepoServer()) { - spawnSync('pnpm', ['exec', 'reactpress-cli', 'stop'], { - cwd: projectRoot, - stdio: 'inherit', - }); - } return 0; case 'restart': stopApi(projectRoot); - if (!isUsingMonorepoServer()) { - spawnSync('pnpm', ['exec', 'reactpress-cli', 'restart'], { - cwd: projectRoot, - stdio: 'inherit', - }); - return 0; - } + await waitForPortFree(parseServerPort(projectRoot)); + await new Promise((r) => setTimeout(r, 400)); return startApi(projectRoot, { wait: true }); case 'status': await statusApi(projectRoot); diff --git a/cli/lib/nginx.js b/cli/lib/nginx.js new file mode 100644 index 0000000..e056cab --- /dev/null +++ b/cli/lib/nginx.js @@ -0,0 +1,342 @@ +const fs = require('fs'); +const path = require('path'); +const http = require('http'); +const { spawnSync } = require('child_process'); +const open = require('open'); +const { detectProjectType } = require('./project-type'); +const { isDockerRunning, pickDockerComposeCommand } = require('./docker'); +const { t } = require('./i18n'); + +const NGINX_CONTAINER = 'reactpress_nginx'; +const DEFAULT_NGINX_PORT = 80; + +function resolveNginxMode(options = {}) { + return options.prod ? 'prod' : 'dev'; +} + +function resolveNginxConfigBasename(mode) { + return mode === 'prod' ? 'nginx.conf' : 'nginx.dev.conf'; +} + +function resolveNginxConfigPath(projectRoot, mode = 'dev') { + const basename = resolveNginxConfigBasename(mode); + const type = detectProjectType(projectRoot); + if (type === 'monorepo') { + return path.join(projectRoot, basename); + } + return path.join(projectRoot, '.reactpress', basename); +} + +function bundledTemplatePath(mode) { + const file = mode === 'prod' ? 'nginx.prod.conf' : 'nginx.dev.conf'; + return path.join(__dirname, '..', 'templates', file); +} + +function resolveNginxPort(projectRoot) { + const envPath = path.join(projectRoot, '.env'); + try { + const content = fs.readFileSync(envPath, 'utf8'); + const m = content.match(/^NGINX_PORT=(.+)$/m); + if (m) { + const port = parseInt(m[1].trim().replace(/^['"]|['"]$/g, ''), 10); + if (port > 0) return port; + } + } catch { + // ignore + } + return DEFAULT_NGINX_PORT; +} + +function nginxEntryUrl(projectRoot) { + const port = resolveNginxPort(projectRoot); + return port === 80 ? 'http://localhost' : `http://localhost:${port}`; +} + +/** + * Write default nginx config from CLI templates when missing (or when force). + * + * @returns {{ configPath: string, created: boolean, mode: 'dev' | 'prod' }} + */ +function ensureNginxConfig(projectRoot, options = {}) { + const mode = resolveNginxMode(options); + const configPath = resolveNginxConfigPath(projectRoot, mode); + const templatePath = bundledTemplatePath(mode); + if (!fs.existsSync(templatePath)) { + throw new Error(t('nginx.templateMissing', { path: templatePath })); + } + + const exists = fs.existsSync(configPath); + if (exists && !options.force) { + return { configPath, created: false, mode }; + } + + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.copyFileSync(templatePath, configPath); + return { configPath, created: !exists || !!options.force, mode }; +} + +function resolveNginxComposeContext(projectRoot, mode = 'dev') { + const type = detectProjectType(projectRoot); + if (mode === 'prod' && type === 'monorepo') { + return { + composeFile: path.join(projectRoot, 'docker-compose.prod.yml'), + cwd: projectRoot, + service: 'nginx', + }; + } + if (type === 'monorepo') { + return { + composeFile: path.join(projectRoot, 'docker-compose.dev.yml'), + cwd: projectRoot, + service: 'nginx', + }; + } + return { + composeFile: path.join(projectRoot, '.reactpress', 'docker-compose.yml'), + cwd: path.join(projectRoot, '.reactpress'), + service: 'nginx', + }; +} + +function composeDefinesNginxService(composeFile) { + try { + const content = fs.readFileSync(composeFile, 'utf8'); + return /^\s*nginx:\s*$/m.test(content); + } catch { + return false; + } +} + +function runComposeOnContext(ctx, args, options = {}) { + const { command, baseArgs } = pickDockerComposeCommand(); + return spawnSync(command, [...baseArgs, '-f', ctx.composeFile, ...args], { + stdio: options.stdio ?? 'inherit', + cwd: ctx.cwd, + ...options, + }); +} + +function isNginxContainerRunning() { + const res = spawnSync( + 'docker', + ['inspect', '-f', '{{.State.Running}}', NGINX_CONTAINER], + { encoding: 'utf8' } + ); + return res.status === 0 && res.stdout.trim() === 'true'; +} + +function startNginxContainer(configPath, port) { + spawnSync('docker', ['rm', '-f', NGINX_CONTAINER], { stdio: 'ignore' }); + const absConfig = path.resolve(configPath); + const res = spawnSync( + 'docker', + [ + 'run', + '-d', + '--name', + NGINX_CONTAINER, + '-p', + `${port}:80`, + '-v', + `${absConfig}:/etc/nginx/conf.d/default.conf:ro`, + '--add-host', + 'host.docker.internal:host-gateway', + 'nginx:alpine', + ], + { encoding: 'utf8' } + ); + if (res.status !== 0) { + throw new Error(res.stderr?.trim() || t('nginx.startFailed')); + } +} + +function stopNginxContainer() { + spawnSync('docker', ['rm', '-sf', NGINX_CONTAINER], { stdio: 'ignore' }); +} + +function nginxUp(projectRoot, options = {}) { + if (!isDockerRunning()) { + throw new Error(t('docker.notRunning')); + } + + const mode = resolveNginxMode(options); + const type = detectProjectType(projectRoot); + + if (mode === 'prod' && type !== 'monorepo') { + throw new Error(t('nginx.prodMonorepoOnly')); + } + + const { configPath } = ensureNginxConfig(projectRoot, { mode, force: options.force }); + const port = resolveNginxPort(projectRoot); + const ctx = resolveNginxComposeContext(projectRoot, mode); + + if (fs.existsSync(ctx.composeFile) && composeDefinesNginxService(ctx.composeFile)) { + const result = runComposeOnContext(ctx, ['up', '-d', ctx.service]); + if (result.status !== 0) { + throw new Error(t('nginx.startFailed')); + } + } else { + startNginxContainer(configPath, port); + } + + console.log(t('nginx.started', { url: nginxEntryUrl(projectRoot) })); + console.log(t('nginx.configPath', { path: configPath })); +} + +function nginxDown(projectRoot, options = {}) { + const mode = resolveNginxMode(options); + const ctx = resolveNginxComposeContext(projectRoot, mode); + if (fs.existsSync(ctx.composeFile) && composeDefinesNginxService(ctx.composeFile)) { + runComposeOnContext(ctx, ['stop', ctx.service], { stdio: 'ignore' }); + } + stopNginxContainer(); + console.log(t('nginx.stopped')); +} + +function nginxRestart(projectRoot, options = {}) { + nginxDown(projectRoot, options); + nginxUp(projectRoot, options); +} + +function nginxStatus(projectRoot, options = {}) { + const mode = resolveNginxMode(options); + const configPath = resolveNginxConfigPath(projectRoot, mode); + const port = resolveNginxPort(projectRoot); + const running = isNginxContainerRunning(); + const configExists = fs.existsSync(configPath); + + console.log(t('nginx.statusTitle')); + console.log(t('nginx.statusContainer', { name: NGINX_CONTAINER, running: running ? t('common.yes') : t('common.no') })); + console.log(t('nginx.statusConfig', { path: configPath, exists: configExists ? t('common.yes') : t('common.no') })); + console.log(t('nginx.statusUrl', { url: nginxEntryUrl(projectRoot), port })); + console.log(t('nginx.statusMode', { mode })); +} + +function nginxLogs(extraArgs = []) { + const args = ['logs', '-f', NGINX_CONTAINER, ...extraArgs]; + spawnSync('docker', args, { stdio: 'inherit' }); +} + +function dockerExecNginx(args) { + return spawnSync('docker', ['exec', NGINX_CONTAINER, 'nginx', ...args], { + encoding: 'utf8', + }); +} + +function nginxTest() { + if (!isNginxContainerRunning()) { + throw new Error(t('nginx.notRunning')); + } + const res = dockerExecNginx(['-t']); + process.stdout.write(res.stdout || ''); + process.stderr.write(res.stderr || ''); + if (res.status !== 0) { + throw new Error(t('nginx.testFailed')); + } + console.log(t('nginx.testOk')); +} + +function nginxReload() { + nginxTest(); + const res = dockerExecNginx(['-s', 'reload']); + if (res.status !== 0) { + throw new Error(res.stderr?.trim() || t('nginx.reloadFailed')); + } + console.log(t('nginx.reloadOk')); +} + +async function nginxOpen(projectRoot) { + const url = nginxEntryUrl(projectRoot); + console.log(t('nginx.opening', { url })); + await open(url); +} + +function probeNginxHealth(projectRoot, timeoutMs = 2000) { + const url = new URL('/health', nginxEntryUrl(projectRoot)); + return new Promise((resolve) => { + const req = http.get(url, { timeout: timeoutMs }, (res) => { + res.resume(); + resolve(res.statusCode === 200); + }); + req.on('error', () => resolve(false)); + req.on('timeout', () => { + req.destroy(); + resolve(false); + }); + }); +} + +async function checkNginx(projectRoot) { + if (!isDockerRunning()) { + return { ok: true, message: t('nginx.doctorSkippedDocker') }; + } + if (!isNginxContainerRunning()) { + return { ok: true, message: t('nginx.doctorSkippedNotRunning') }; + } + const healthy = await probeNginxHealth(projectRoot); + if (healthy) { + return { + ok: true, + message: t('nginx.doctorOk', { url: nginxEntryUrl(projectRoot) }), + }; + } + return { + ok: false, + message: t('nginx.doctorUnhealthy', { url: nginxEntryUrl(projectRoot) }), + fix: t('nginx.doctorUnhealthyFix'), + }; +} + +async function runNginxCommand(command, projectRoot, extraArgs = [], options = {}) { + switch (command) { + case 'ensure': { + const { configPath, created } = ensureNginxConfig(projectRoot, options); + console.log( + created ? t('nginx.configCreated', { path: configPath }) : t('nginx.configExists', { path: configPath }) + ); + return; + } + case 'up': + nginxUp(projectRoot, options); + return; + case 'down': + case 'stop': + nginxDown(projectRoot, options); + return; + case 'restart': + nginxRestart(projectRoot, options); + return; + case 'status': + nginxStatus(projectRoot, options); + return; + case 'logs': + nginxLogs(extraArgs); + return; + case 'test': + nginxTest(); + return; + case 'reload': + nginxReload(); + return; + case 'open': + await nginxOpen(projectRoot); + return; + default: + throw new Error(t('nginx.unknownCommand', { command })); + } +} + +module.exports = { + NGINX_CONTAINER, + DEFAULT_NGINX_PORT, + resolveNginxMode, + resolveNginxConfigPath, + resolveNginxComposeContext, + ensureNginxConfig, + nginxEntryUrl, + resolveNginxPort, + isNginxContainerRunning, + probeNginxHealth, + checkNginx, + runNginxCommand, +}; diff --git a/cli/lib/paths.js b/cli/lib/paths.js index 12846f9..42351d4 100644 --- a/cli/lib/paths.js +++ b/cli/lib/paths.js @@ -1,13 +1,19 @@ const fs = require('fs'); const path = require('path'); -const { getMonorepoRoot } = require('./root'); +const { ensureOriginalCwd, getMonorepoRoot } = require('./root'); -function getMonorepoServerDir() { - return path.join(getMonorepoRoot(), 'server'); +function resolveProjectRoot(projectRoot) { + return path.resolve(projectRoot || ensureOriginalCwd()); } -function hasMonorepoServerSource() { - return fs.existsSync(path.join(getMonorepoServerDir(), 'src', 'main.ts')); +function getMonorepoServerDir(projectRoot) { + return path.join(resolveProjectRoot(projectRoot), 'server'); +} + +function hasMonorepoServerSource(projectRoot) { + return fs.existsSync( + path.join(getMonorepoServerDir(projectRoot), 'src', 'main.ts') + ); } function getCliPackageRoot() { @@ -28,42 +34,69 @@ function getBundledServerDir() { return path.join(getCliPackageRoot(), 'server'); } -function getServerDir() { - if (hasMonorepoServerSource()) { - return getMonorepoServerDir(); +function hasBundledServerBuild() { + return fs.existsSync(path.join(getBundledServerDir(), 'dist', 'main.js')); +} + +function getServerDir(projectRoot) { + if (hasMonorepoServerSource(projectRoot)) { + return getMonorepoServerDir(projectRoot); } return getBundledServerDir(); } -function getServerBin() { - return path.join(getServerDir(), 'bin', 'reactpress-server.js'); +function getServerBin(projectRoot) { + return path.join(getServerDir(projectRoot), 'bin', 'reactpress-server.js'); } -function getSwaggerPath() { - return path.join(getServerDir(), 'public', 'swagger.json'); +function getSwaggerPath(projectRoot) { + return path.join(getServerDir(projectRoot), 'public', 'swagger.json'); } -function getServerMain() { - return path.join(getServerDir(), 'dist', 'main.js'); +function getServerMain(projectRoot) { + return path.join(getServerDir(projectRoot), 'dist', 'main.js'); } -function isUsingMonorepoServer() { - return hasMonorepoServerSource(); +function isUsingMonorepoServer(projectRoot) { + return hasMonorepoServerSource(projectRoot); } -function getClientBin() { - return path.join(getMonorepoRoot(), 'client', 'bin', 'reactpress-client.js'); +function canStartLocalApi(projectRoot) { + return ( + isUsingMonorepoServer(projectRoot) || + hasBundledServerBuild() + ); +} + +function getClientBin(projectRoot) { + const binPath = path.join( + resolveProjectRoot(projectRoot), + 'client', + 'bin', + 'reactpress-client.js' + ); + if (!fs.existsSync(binPath)) { + const err = new Error( + `Client entry not found: ${binPath}. Run from a ReactPress monorepo root or use reactpress dev --client-only with a remote API.` + ); + err.code = 'REACTPRESS_CLIENT_NOT_FOUND'; + throw err; + } + return binPath; } function getPidFile(projectRoot) { - return path.join(projectRoot, '.reactpress', 'server.pid'); + return path.join(resolveProjectRoot(projectRoot), '.reactpress', 'server.pid'); } module.exports = { getMonorepoRoot, + resolveProjectRoot, getMonorepoServerDir, hasMonorepoServerSource, + hasBundledServerBuild, isUsingMonorepoServer, + canStartLocalApi, getCliPackageRoot, getBundledServerDir, getServerDir, diff --git a/cli/lib/pm2.js b/cli/lib/pm2.js index 129f2a9..1473b06 100644 --- a/cli/lib/pm2.js +++ b/cli/lib/pm2.js @@ -5,9 +5,9 @@ const { t } = require('./i18n'); function startApiWithPm2(projectRoot = ensureOriginalCwd()) { return new Promise((resolve, reject) => { - const child = spawn(process.execPath, [getServerBin(), '--pm2'], { + const child = spawn(process.execPath, [getServerBin(projectRoot), '--pm2'], { stdio: 'inherit', - cwd: getServerDir(), + cwd: getServerDir(projectRoot), env: { ...process.env, REACTPRESS_ORIGINAL_CWD: projectRoot, diff --git a/cli/lib/project-type.js b/cli/lib/project-type.js new file mode 100644 index 0000000..32e5301 --- /dev/null +++ b/cli/lib/project-type.js @@ -0,0 +1,72 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Decide whether a given directory is a ReactPress monorepo checkout (with + * editable `server/src`, `client/`, `toolkit/`) or a standalone project that + * was created with `reactpress init` and relies on the bundled runtime. + * + * @param {string} root absolute project root + * @returns {'monorepo' | 'standalone' | 'unknown'} + */ +function detectProjectType(root) { + if (!root) return 'unknown'; + const abs = path.resolve(root); + + const monorepoMarkers = [ + path.join(abs, 'pnpm-workspace.yaml'), + path.join(abs, 'server', 'src', 'main.ts'), + ]; + if (monorepoMarkers.some((p) => fs.existsSync(p))) { + return 'monorepo'; + } + + if (fs.existsSync(path.join(abs, '.reactpress', 'config.json'))) { + return 'standalone'; + } + + return 'unknown'; +} + +/** + * @param {string} root + */ +function hasClient(root) { + return fs.existsSync(path.join(root, 'client', 'package.json')); +} + +/** + * @param {string} root + */ +function hasServerSource(root) { + return fs.existsSync(path.join(root, 'server', 'src', 'main.ts')); +} + +/** + * @param {string} root + */ +function hasToolkit(root) { + return fs.existsSync(path.join(root, 'toolkit', 'package.json')); +} + +/** + * @param {string} root + */ +function describeProject(root) { + const type = detectProjectType(root); + return { + type, + root, + hasClient: hasClient(root), + hasServerSource: hasServerSource(root), + hasToolkit: hasToolkit(root), + }; +} + +module.exports = { + detectProjectType, + describeProject, + hasClient, + hasServerSource, + hasToolkit, +}; diff --git a/cli/lib/publish.js b/cli/lib/publish.js index 1ae6005..976fcb5 100644 --- a/cli/lib/publish.js +++ b/cli/lib/publish.js @@ -7,6 +7,15 @@ const chalk = require('chalk'); const inquirer = require('inquirer'); const crypto = require('crypto'); const { t } = require('./i18n'); +const { getMonorepoRoot } = require('./root'); + +function getWorkspaceRoot() { + const root = getMonorepoRoot(); + if (fs.existsSync(path.join(root, 'pnpm-workspace.yaml'))) { + return root; + } + return process.cwd(); +} function getPackages() { return [ @@ -67,7 +76,7 @@ function generateHash(filePath) { // Get package content hash function getPackageHash(packagePath) { - const fullPath = path.join(process.cwd(), packagePath); + const fullPath = path.join(getWorkspaceRoot(), packagePath); return generateHash(fullPath); } @@ -75,7 +84,7 @@ function getPackageHash(packagePath) { function hasMeaningfulChangesForBuild(packagePath, packageName) { try { // Create a hash file path to store previous hash in node_modules - const hashFilePath = path.join(process.cwd(), 'node_modules', '.build-cache', `${packageName.replace('/', '_')}.hash`); + const hashFilePath = path.join(getWorkspaceRoot(), 'node_modules', '.build-cache', `${packageName.replace('/', '_')}.hash`); // Generate current hash const currentHash = getPackageHash(packagePath); @@ -98,7 +107,7 @@ function hasMeaningfulChangesForBuild(packagePath, packageName) { function hasMeaningfulChangesForPublish(packagePath, packageName) { try { // Create a hash file path to store previous hash in node_modules - const hashFilePath = path.join(process.cwd(), 'node_modules', '.publish-cache', `${packageName.replace('/', '_')}.hash`); + const hashFilePath = path.join(getWorkspaceRoot(), 'node_modules', '.publish-cache', `${packageName.replace('/', '_')}.hash`); // Generate current hash const currentHash = getPackageHash(packagePath); @@ -120,7 +129,7 @@ function hasMeaningfulChangesForPublish(packagePath, packageName) { // Save package hash (for build) function savePackageHashForBuild(packagePath, packageName) { try { - const hashFilePath = path.join(process.cwd(), 'node_modules', '.build-cache', `${packageName.replace('/', '_')}.hash`); + const hashFilePath = path.join(getWorkspaceRoot(), 'node_modules', '.build-cache', `${packageName.replace('/', '_')}.hash`); // Ensure cache directory exists const cacheDir = path.dirname(hashFilePath); @@ -139,7 +148,7 @@ function savePackageHashForBuild(packagePath, packageName) { // Save package hash (for publish) function savePackageHashForPublish(packagePath, packageName) { try { - const hashFilePath = path.join(process.cwd(), 'node_modules', '.publish-cache', `${packageName.replace('/', '_')}.hash`); + const hashFilePath = path.join(getWorkspaceRoot(), 'node_modules', '.publish-cache', `${packageName.replace('/', '_')}.hash`); // Ensure cache directory exists const cacheDir = path.dirname(hashFilePath); @@ -158,7 +167,7 @@ function savePackageHashForPublish(packagePath, packageName) { // Get current versions function getCurrentVersion(packagePath) { try { - const pkgPath = path.join(process.cwd(), packagePath, 'package.json'); + const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); return pkg.version; } catch (error) { @@ -168,10 +177,12 @@ function getCurrentVersion(packagePath) { // Increment version based on type function incrementVersion(version, type) { - const parts = version.split('-')[0].split('.'); - const major = parseInt(parts[0]); - const minor = parseInt(parts[1]); - const patch = parseInt(parts[2]); + const base = String(version).split('-')[0]; + const parts = base.split('.').map((p) => parseInt(p, 10)); + while (parts.length < 3) parts.push(0); + const major = Number.isFinite(parts[0]) ? parts[0] : 0; + const minor = Number.isFinite(parts[1]) ? parts[1] : 0; + const patch = Number.isFinite(parts[2]) ? parts[2] : 0; switch (type) { case 'major': @@ -246,7 +257,7 @@ function getNextAvailableVersion(packageName, currentVersion, versionType) { function updateVersion(packagePath, newVersion) { console.log(chalk.blue(`\n✏️ Updating version to ${newVersion}...`)); - const pkgPath = path.join(process.cwd(), packagePath, 'package.json'); + const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); const oldVersion = pkg.version; @@ -260,7 +271,7 @@ function updateVersion(packagePath, newVersion) { function fixWorkspaceDependenciesForBuild(packagePath) { console.log(chalk.blue(`🔧 Fixing workspace dependencies for build: ${packagePath}...`)); - const pkgPath = path.join(process.cwd(), packagePath, 'package.json'); + const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); // Fix dependencies @@ -291,7 +302,7 @@ function fixWorkspaceDependenciesForBuild(packagePath) { function restoreWorkspaceDependenciesAfterBuild(packagePath) { console.log(chalk.blue(`🔄 Restoring workspace dependencies after build: ${packagePath}...`)); - const pkgPath = path.join(process.cwd(), packagePath, 'package.json'); + const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); // Restore dependencies @@ -319,7 +330,7 @@ function restoreWorkspaceDependenciesAfterBuild(packagePath) { function fixWorkspaceDependenciesForPublish(packagePath, packageVersions) { console.log(chalk.blue(`🔧 Fixing workspace dependencies for publish: ${packagePath}...`)); - const pkgPath = path.join(process.cwd(), packagePath, 'package.json'); + const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); // Fix dependencies @@ -350,7 +361,7 @@ function fixWorkspaceDependenciesForPublish(packagePath, packageVersions) { function restoreWorkspaceDependenciesAfterPublish(packagePath) { console.log(chalk.blue(`🔄 Restoring workspace dependencies for ${packagePath}...`)); - const pkgPath = path.join(process.cwd(), packagePath, 'package.json'); + const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); // Restore dependencies @@ -383,15 +394,22 @@ function buildPackage(pkg) { fixWorkspaceDependenciesForBuild(pkg.path); try { - if (pkg.path === 'config') { - execSync('pnpm run build', { cwd: path.join(process.cwd(), pkg.path), stdio: 'inherit' }); + const pkgDir = path.join(getWorkspaceRoot(), pkg.path); + if (pkg.path === 'cli') { + execSync('node scripts/sync-bundled-core.mjs', { cwd: pkgDir, stdio: 'inherit' }); + if (fs.existsSync(path.join(pkgDir, 'server', 'package.json'))) { + execSync('pnpm run build', { cwd: path.join(pkgDir, 'server'), stdio: 'inherit' }); + } + } else if (pkg.path === 'server') { + execSync('pnpm run build', { cwd: pkgDir, stdio: 'inherit' }); } else if (pkg.path === 'client') { - execSync('pnpm run prebuild && pnpm run build', { cwd: path.join(process.cwd(), pkg.path), stdio: 'inherit' }); + execSync('pnpm run prebuild && pnpm run build', { cwd: pkgDir, stdio: 'inherit' }); } else if (pkg.path === 'toolkit') { - execSync('pnpm run build', { cwd: path.join(process.cwd(), pkg.path), stdio: 'inherit' }); + execSync('pnpm run build', { cwd: pkgDir, stdio: 'inherit' }); } else if (pkg.path === 'templates/hello-world' || pkg.path === 'templates/twentytwentyfive') { - // Templates don't need to be built, just validate package.json console.log(chalk.gray(' Templates do not require building, skipping...')); + } else if (fs.existsSync(path.join(pkgDir, 'package.json'))) { + execSync('pnpm run build', { cwd: pkgDir, stdio: 'inherit' }); } console.log(chalk.green(`✅ ${pkg.name} built successfully`)); } finally { @@ -410,7 +428,7 @@ function publishPackage(packagePath, packageName, tag = 'latest') { try { const command = `pnpm publish --access public --tag ${tag} --registry https://registry.npmjs.org --no-git-checks`; - execSync(command, { cwd: path.join(process.cwd(), packagePath), stdio: 'inherit' }); + execSync(command, { cwd: path.join(getWorkspaceRoot(), packagePath), stdio: 'inherit' }); console.log(chalk.green(`✅ ${packageName} published successfully!`)); } catch (error) { console.log(chalk.red(`❌ Failed to publish ${packageName}`)); @@ -472,7 +490,7 @@ async function buildPackages() { // Check for meaningful changes in each package for (const pkg of packages) { - if (fs.existsSync(path.join(process.cwd(), pkg.path))) { + if (fs.existsSync(path.join(getWorkspaceRoot(), pkg.path))) { if (hasMeaningfulChangesForBuild(pkg.path, pkg.name)) { packagesToBuild.push(pkg); console.log(chalk.blue(`\n📦 ${pkg.name} has changes, will be built`)); @@ -552,7 +570,7 @@ async function publishPackages() { // Check for meaningful changes in each package for (const pkg of packages) { - if (fs.existsSync(path.join(process.cwd(), pkg.path))) { + if (fs.existsSync(path.join(getWorkspaceRoot(), pkg.path))) { if (hasMeaningfulChangesForPublish(pkg.path, pkg.name)) { packagesToBuild.push(pkg); console.log(chalk.blue(`\n📦 ${pkg.name} has changes, will be built`)); @@ -569,17 +587,15 @@ async function publishPackages() { return; } - // Build packages that have changes for (const pkg of packagesToBuild) { - buildPackage(pkg); - // Save the hash after successful build + await buildPackage(pkg); savePackageHashForPublish(pkg.path, pkg.name); } - + console.log(chalk.green(`\n🎉 ${packagesToBuild.length} package(s) built successfully!`)); return; } - + if (action === 'publish-one') { const { selectedPackage } = await inquirer.prompt([ { @@ -706,7 +722,7 @@ async function publishPackages() { // Check for meaningful changes in each package for (const pkg of packages) { - if (!fs.existsSync(path.join(process.cwd(), pkg.path))) { + if (!fs.existsSync(path.join(getWorkspaceRoot(), pkg.path))) { console.log(chalk.yellow(`⚠️ Package ${pkg.name} directory not found, skipping...`)); continue; } @@ -845,7 +861,7 @@ async function publishPackages() { // Check for meaningful changes in each package for (const pkg of packages) { - if (!fs.existsSync(path.join(process.cwd(), pkg.path))) { + if (!fs.existsSync(path.join(getWorkspaceRoot(), pkg.path))) { console.log(chalk.yellow(`⚠️ Package ${pkg.name} directory not found, skipping...`)); continue; } diff --git a/cli/lib/root.js b/cli/lib/root.js index 5d98fbc..cfb53a7 100644 --- a/cli/lib/root.js +++ b/cli/lib/root.js @@ -1,6 +1,8 @@ const fs = require('fs'); const path = require('path'); +const CLI_PACKAGE_NAME = '@fecommunity/reactpress'; + /** * Install root: monorepo checkout (repo root) or published @fecommunity/reactpress package root. * cli/lib -> ../.. when pnpm-workspace.yaml exists; published lib/ -> .. only. @@ -14,8 +16,51 @@ function getMonorepoRoot() { return packageRoot; } +function isPublishedCliRoot(dir) { + const resolved = path.resolve(dir); + try { + const pkg = JSON.parse( + fs.readFileSync(path.join(resolved, 'package.json'), 'utf8') + ); + if (pkg.name !== CLI_PACKAGE_NAME) return false; + } catch { + return false; + } + return !fs.existsSync(path.join(resolved, 'pnpm-workspace.yaml')); +} + +function isProjectRoot(dir) { + const resolved = path.resolve(dir); + if (isPublishedCliRoot(resolved)) return false; + return ( + fs.existsSync(path.join(resolved, '.reactpress', 'config.json')) || + fs.existsSync(path.join(resolved, 'pnpm-workspace.yaml')) || + fs.existsSync(path.join(resolved, 'server', 'src', 'main.ts')) || + fs.existsSync(path.join(resolved, 'toolkit', 'package.json')) + ); +} + +function findProjectRoot(startDir = process.cwd()) { + let dir = path.resolve(startDir); + while (true) { + if (isProjectRoot(dir)) return dir; + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return null; +} + function getProjectRoot() { - return path.resolve(process.env.REACTPRESS_ORIGINAL_CWD || process.cwd()); + const envRoot = process.env.REACTPRESS_ORIGINAL_CWD; + if (envRoot) { + const resolved = path.resolve(envRoot); + if (isProjectRoot(resolved)) return resolved; + } + const discovered = findProjectRoot(process.cwd()); + if (discovered) return discovered; + if (envRoot) return path.resolve(envRoot); + return path.resolve(process.cwd()); } function ensureOriginalCwd() { @@ -24,10 +69,11 @@ function ensureOriginalCwd() { return root; } -function isMonorepoCheckout(cwd = getMonorepoRoot()) { +function isMonorepoCheckout(cwd) { + const resolved = path.resolve(cwd || process.cwd()); return ( - fs.existsSync(path.join(cwd, 'pnpm-workspace.yaml')) || - fs.existsSync(path.join(cwd, 'server', 'src', 'main.ts')) + fs.existsSync(path.join(resolved, 'pnpm-workspace.yaml')) || + fs.existsSync(path.join(resolved, 'server', 'src', 'main.ts')) ); } @@ -36,4 +82,7 @@ module.exports = { getProjectRoot, ensureOriginalCwd, isMonorepoCheckout, + isProjectRoot, + findProjectRoot, + isPublishedCliRoot, }; diff --git a/cli/lib/spawn.js b/cli/lib/spawn.js index 46b8eba..4753e97 100644 --- a/cli/lib/spawn.js +++ b/cli/lib/spawn.js @@ -1,13 +1,13 @@ const { spawn, spawnSync } = require('child_process'); const path = require('path'); const chalk = require('chalk'); -const { getMonorepoRoot } = require('./root'); +const { ensureOriginalCwd } = require('./root'); const { getCliPackageRoot } = require('./paths'); const { t, resolveLocale } = require('./i18n'); function runSync(command, args, options = {}) { const result = spawnSync(command, args, { - cwd: options.cwd || getMonorepoRoot(), + cwd: options.cwd || ensureOriginalCwd(), stdio: 'inherit', env: { ...process.env, @@ -32,7 +32,7 @@ function runNodeScript(scriptPath, args = [], options = {}) { return new Promise((resolve, reject) => { const child = spawn(process.execPath, [scriptPath, ...args], { stdio: 'inherit', - cwd: options.cwd || getMonorepoRoot(), + cwd: options.cwd || ensureOriginalCwd(), env: { ...process.env, REACTPRESS_LANG: process.env.REACTPRESS_LANG || resolveLocale(), diff --git a/cli/lib/status.js b/cli/lib/status.js index 2021a7f..a14ce81 100644 --- a/cli/lib/status.js +++ b/cli/lib/status.js @@ -1,6 +1,16 @@ const fs = require('fs'); const path = require('path'); -const chalk = require('chalk'); +const { + brand, + icon, + divider, + padRight, + statusPill, + sectionHeader, + terminalWidth, + gradientText, + palette, +} = require('../ui/theme'); const { loadServerSiteUrl, loadClientSiteUrl, @@ -25,6 +35,10 @@ function envFileStatus(projectRoot) { }; } +function fieldRow(label, value) { + return ` ${brand.muted(padRight(label, 10))} ${value}`; +} + async function printUnifiedStatus(projectRoot = ensureOriginalCwd()) { const env = envFileStatus(projectRoot); const apiUrl = loadServerSiteUrl(projectRoot); @@ -37,71 +51,81 @@ async function printUnifiedStatus(projectRoot = ensureOriginalCwd()) { checkHealth(healthUrl), ]); - const apiSource = isUsingMonorepoServer() + const apiSource = isUsingMonorepoServer(projectRoot) ? t('status.apiSource.monorepo') : t('status.apiSource.bundle'); + const w = Math.min(terminalWidth() - 4, 52); + const httpOn = { on: t('status.apiOnline'), off: t('status.apiOffline') }; + console.log(''); - console.log(chalk.bold.cyan(t('status.title'))); - console.log(chalk.gray(' ─────────────────────────────────────')); - console.log(t('status.dir', { path: projectRoot })); - console.log(t('status.apiSource', { source: apiSource })); - console.log( - ` ${chalk.bold('Config')} ${ - env.config - ? chalk.green(t('status.configOk')) - : chalk.yellow(t('status.configBad')) - }`, - ); + console.log(` ${gradientText(t('status.title'), [palette.primary, palette.accent], { bold: true })}`); + console.log(` ${divider(w)}`); + + console.log(sectionHeader(t('status.section.project'))); + console.log(fieldRow(t('status.field.dir'), brand.dim(projectRoot))); + console.log(fieldRow(t('status.field.source'), brand.accent(apiSource))); console.log( - ` ${chalk.bold('.env')} ${ - env.env ? chalk.green(t('status.envOk')) : chalk.yellow(t('status.envBad')) - }`, + fieldRow( + t('status.field.config'), + env.config ? brand.success(t('status.configOk')) : brand.warn(t('status.configBad')) + ) ); - console.log(chalk.gray(' ─────────────────────────────────────')); - console.log(chalk.bold(' API')); - console.log(` URL ${apiUrl}`); console.log( - ` HTTP ${ - apiHttp ? chalk.green(t('status.apiOnline')) : chalk.red(t('status.apiOffline')) - }`, + fieldRow( + t('status.field.env'), + env.env ? brand.success(t('status.envOk')) : brand.warn(t('status.envBad')) + ) ); + + console.log(''); + console.log(sectionHeader(t('status.section.api'))); + console.log(fieldRow(t('status.field.url'), brand.dim(apiUrl))); + console.log(fieldRow(t('status.field.http'), statusPill(apiHttp, httpOn))); console.log( - ` Health ${ + fieldRow( + t('status.field.health'), health.ok - ? chalk.green(`${healthUrl} ✓`) - : chalk.gray(t('status.apiUnreachable', { url: healthUrl })) - }`, + ? `${icon.ok} ${brand.dim(healthUrl)}` + : brand.dim(t('status.apiUnreachable', { url: healthUrl })) + ) ); if (health.ok && health.data?.data) { const db = health.data.data.database; + const dbOk = db === 'up'; console.log( - ` Database ${ - db === 'up' - ? chalk.green(t('status.dbUp')) - : chalk.red(db === 'down' ? t('status.dbDown') : '—') - }`, + fieldRow( + t('status.field.database'), + statusPill(dbOk, { on: t('status.dbUp'), off: t('status.dbDown') }) + ) ); } + const pidAlive = pid && isProcessRunning(pid); console.log( - ` PID ${pid ?? '—'} ${ - pid && isProcessRunning(pid) ? chalk.green(t('status.pidRunning')) : '' - }`, - ); - console.log(chalk.bold(t('status.frontend'))); - console.log(` URL ${clientUrl}`); - console.log( - ` HTTP ${ - clientHttp ? chalk.green(t('status.apiOnline')) : chalk.gray(t('status.apiOffline')) - }`, + fieldRow( + t('status.field.pid'), + `${brand.dim(pid ?? '—')}${pidAlive ? ` ${brand.success(t('status.pidRunning'))}` : ''}` + ) ); - console.log(chalk.gray(' ─────────────────────────────────────')); - console.log(chalk.bold(t('status.docker'))); + + console.log(''); + console.log(sectionHeader(t('status.section.frontend'))); + console.log(fieldRow(t('status.field.url'), brand.dim(clientUrl))); + console.log(fieldRow(t('status.field.http'), statusPill(clientHttp, httpOn))); + + console.log(''); + console.log(sectionHeader(t('status.section.docker'))); console.log( - ` Engine ${ - isDockerRunning() ? chalk.green(t('status.dockerUp')) : chalk.red(t('status.dockerDown')) - }`, + fieldRow( + t('status.field.engine'), + statusPill(isDockerRunning(), { + on: t('status.dockerUp'), + off: t('status.dockerDown'), + }) + ) ); + + console.log(` ${divider(w)}`); console.log(''); } diff --git a/cli/package.json b/cli/package.json index 358c4bd..ad79a22 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@fecommunity/reactpress", - "version": "3.0.1", + "version": "3.0.3", "description": "ReactPress 3.0 — zero-config CMS: one package, one reactpress command", "author": "fecommunity", "license": "MIT", @@ -35,6 +35,7 @@ "LICENSE" ], "scripts": { + "test": "node --test tests", "prepare": "node scripts/sync-bundled-core.mjs", "prepack": "node scripts/sync-bundled-core.mjs" }, diff --git a/cli/tests/build.test.js b/cli/tests/build.test.js new file mode 100644 index 0000000..5b35247 --- /dev/null +++ b/cli/tests/build.test.js @@ -0,0 +1,32 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const { resolveBuildInvocation, TARGETS } = require('../lib/build'); +const { createMonorepoFixture, createStandaloneProject, rmDir } = require('./helpers/tmp-project'); + +describe('lib/build', () => { + it('resolves toolkit build to toolkit/ directory in monorepo', () => { + const root = createMonorepoFixture(); + try { + const inv = resolveBuildInvocation('build:toolkit', root); + assert.ok(inv); + assert.match(inv.cwd, /toolkit$/); + } finally { + rmDir(root); + } + }); + + it('skips client build when client/ is missing', () => { + const root = createStandaloneProject(); + try { + const inv = resolveBuildInvocation('build:client', root); + assert.equal(inv, null); + } finally { + rmDir(root); + } + }); + + it('exposes known targets', () => { + assert.ok(TARGETS.includes('all')); + assert.ok(TARGETS.includes('toolkit')); + }); +}); diff --git a/cli/tests/docker.test.js b/cli/tests/docker.test.js new file mode 100644 index 0000000..0614277 --- /dev/null +++ b/cli/tests/docker.test.js @@ -0,0 +1,28 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('path'); +const { resolveComposeContext } = require('../lib/docker'); +const { createStandaloneProject, createMonorepoFixture, rmDir } = require('./helpers/tmp-project'); + +describe('lib/docker', () => { + it('uses .reactpress/docker-compose.yml for standalone projects', () => { + const root = createStandaloneProject(); + try { + const ctx = resolveComposeContext(root); + assert.equal(ctx.type, 'standalone'); + assert.ok(ctx.composeFile.endsWith(path.join('.reactpress', 'docker-compose.yml'))); + } finally { + rmDir(root); + } + }); + + it('uses docker-compose.dev.yml for monorepo when present', () => { + const root = createMonorepoFixture(); + try { + const ctx = resolveComposeContext(root); + assert.equal(ctx.type, 'monorepo'); + } finally { + rmDir(root); + } + }); +}); diff --git a/cli/tests/helpers/tmp-project.js b/cli/tests/helpers/tmp-project.js new file mode 100644 index 0000000..5909bcc --- /dev/null +++ b/cli/tests/helpers/tmp-project.js @@ -0,0 +1,77 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +function createStandaloneProject() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'reactpress-test-')); + const reactpressDir = path.join(root, '.reactpress'); + fs.mkdirSync(reactpressDir, { recursive: true }); + + fs.writeFileSync( + path.join(reactpressDir, 'config.json'), + JSON.stringify( + { + version: 1, + database: { mode: 'embedded-docker', containerName: 'reactpress_cli_db' }, + server: { + port: 3002, + clientUrl: 'http://localhost:3001', + serverUrl: 'http://localhost:3002', + }, + }, + null, + 2 + ) + ); + + fs.writeFileSync( + path.join(root, '.env'), + [ + 'DB_HOST=127.0.0.1', + 'DB_PORT=3307', + 'DB_USER=reactpress', + 'DB_PASSWD=reactpress', + 'DB_DATABASE=reactpress', + 'CLIENT_SITE_URL=http://localhost:3001', + 'SERVER_SITE_URL=http://localhost:3002', + 'SERVER_PORT=3002', + 'SERVER_API_PREFIX=/api', + '', + ].join('\n') + ); + + fs.copyFileSync( + path.join(__dirname, '../../templates/docker-compose.yml'), + path.join(reactpressDir, 'docker-compose.yml') + ); + + return root; +} + +function createMonorepoFixture() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'reactpress-mono-')); + fs.writeFileSync(path.join(root, 'pnpm-workspace.yaml'), 'packages:\n - server\n - client\n'); + fs.mkdirSync(path.join(root, 'server', 'src'), { recursive: true }); + fs.writeFileSync(path.join(root, 'server', 'src', 'main.ts'), 'export {};\n'); + fs.writeFileSync( + path.join(root, 'server', 'package.json'), + JSON.stringify({ name: 'server', scripts: { build: 'echo build' } }) + ); + fs.mkdirSync(path.join(root, 'client')); + fs.writeFileSync( + path.join(root, 'client', 'package.json'), + JSON.stringify({ name: 'client', scripts: { dev: 'echo dev' } }) + ); + fs.mkdirSync(path.join(root, 'toolkit')); + fs.writeFileSync( + path.join(root, 'toolkit', 'package.json'), + JSON.stringify({ name: 'toolkit', scripts: { build: 'echo build' } }) + ); + return root; +} + +function rmDir(dir) { + fs.rmSync(dir, { recursive: true, force: true }); +} + +module.exports = { createStandaloneProject, createMonorepoFixture, rmDir }; diff --git a/cli/tests/http.test.js b/cli/tests/http.test.js new file mode 100644 index 0000000..8b8d7d0 --- /dev/null +++ b/cli/tests/http.test.js @@ -0,0 +1,23 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const { + loadServerSiteUrl, + loadClientSiteUrl, + getApiPrefix, + getHealthUrl, +} = require('../lib/http'); +const { createStandaloneProject, rmDir } = require('./helpers/tmp-project'); + +describe('lib/http', () => { + it('reads URLs and health path from .env', () => { + const root = createStandaloneProject(); + try { + assert.equal(loadServerSiteUrl(root), 'http://localhost:3002'); + assert.equal(loadClientSiteUrl(root), 'http://localhost:3001'); + assert.equal(getApiPrefix(root), '/api'); + assert.equal(getHealthUrl(root), 'http://localhost:3002/api/health'); + } finally { + rmDir(root); + } + }); +}); diff --git a/cli/tests/i18n.test.js b/cli/tests/i18n.test.js new file mode 100644 index 0000000..d6e511d --- /dev/null +++ b/cli/tests/i18n.test.js @@ -0,0 +1,20 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const { t, setLocale, getLocale } = require('../lib/i18n'); + +describe('lib/i18n', () => { + it('translates known keys in en and zh', () => { + setLocale('en'); + assert.match(t('cli.description'), /ReactPress/); + setLocale('zh'); + assert.match(t('cli.description'), /ReactPress/); + assert.match(t('menu.dev'), /开发|dev/i); + setLocale('en'); + assert.equal(getLocale(), 'en'); + }); + + it('interpolates variables', () => { + setLocale('en'); + assert.match(t('menu.opening', { url: 'http://x' }), /http:\/\/x/); + }); +}); diff --git a/cli/tests/nginx.test.js b/cli/tests/nginx.test.js new file mode 100644 index 0000000..540b6bb --- /dev/null +++ b/cli/tests/nginx.test.js @@ -0,0 +1,59 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); +const { + resolveNginxConfigPath, + resolveNginxComposeContext, + ensureNginxConfig, +} = require('../lib/nginx'); +const { createStandaloneProject, createMonorepoFixture, rmDir } = require('./helpers/tmp-project'); + +describe('lib/nginx', () => { + it('resolves dev config at repo root for monorepo', () => { + const root = createMonorepoFixture(); + try { + const configPath = resolveNginxConfigPath(root, 'dev'); + assert.equal(configPath, path.join(root, 'nginx.dev.conf')); + } finally { + rmDir(root); + } + }); + + it('resolves dev config under .reactpress for standalone', () => { + const root = createStandaloneProject(); + try { + const configPath = resolveNginxConfigPath(root, 'dev'); + assert.equal(configPath, path.join(root, '.reactpress', 'nginx.dev.conf')); + } finally { + rmDir(root); + } + }); + + it('ensureNginxConfig writes template when missing', () => { + const root = createStandaloneProject(); + try { + const target = path.join(root, '.reactpress', 'nginx.dev.conf'); + if (fs.existsSync(target)) fs.unlinkSync(target); + const { configPath, created } = ensureNginxConfig(root, { mode: 'dev' }); + assert.equal(created, true); + assert.equal(configPath, target); + assert.ok(fs.existsSync(target)); + assert.ok(fs.readFileSync(target, 'utf8').includes('host.docker.internal')); + } finally { + rmDir(root); + } + }); + + it('uses docker-compose.dev.yml for monorepo dev nginx', () => { + const root = createMonorepoFixture(); + try { + fs.writeFileSync(path.join(root, 'docker-compose.dev.yml'), 'services:\n nginx:\n image: nginx\n'); + const ctx = resolveNginxComposeContext(root, 'dev'); + assert.equal(ctx.composeFile, path.join(root, 'docker-compose.dev.yml')); + assert.equal(ctx.service, 'nginx'); + } finally { + rmDir(root); + } + }); +}); diff --git a/cli/tests/parity-pack.test.js b/cli/tests/parity-pack.test.js new file mode 100644 index 0000000..9754d0a --- /dev/null +++ b/cli/tests/parity-pack.test.js @@ -0,0 +1,43 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const CLI_ROOT = path.join(__dirname, '..'); + +/** Files that must ship in the npm tarball and exist locally (publish/local parity). */ +const REQUIRED_SHIPPED = [ + 'package.json', + 'bin/reactpress.js', + 'bin/reactpress-cli-shim.js', + 'lib/root.js', + 'lib/publish.js', + 'lib/project-type.js', + 'ui/interactive.js', + 'ui/banner.js', + 'ui/theme.js', + 'dist/index.js', + 'templates/env.default', + 'templates/config.default.json', +]; + +describe('publish/local file parity', () => { + it('critical runtime files exist on disk', () => { + for (const required of REQUIRED_SHIPPED) { + assert.ok(fs.existsSync(path.join(CLI_ROOT, required)), `missing locally: ${required}`); + } + }); + + it('package.json files[] lists top-level dirs for all shipped assets', () => { + const pkg = JSON.parse(fs.readFileSync(path.join(CLI_ROOT, 'package.json'), 'utf8')); + const declared = new Set(pkg.files || []); + for (const required of REQUIRED_SHIPPED) { + if (required === 'package.json') continue; + const top = required.split('/')[0]; + assert.ok( + declared.has(top) || declared.has(required), + `package.json files[] missing "${top}" (needed for ${required})` + ); + } + }); +}); diff --git a/cli/tests/project-type.test.js b/cli/tests/project-type.test.js new file mode 100644 index 0000000..d4982ad --- /dev/null +++ b/cli/tests/project-type.test.js @@ -0,0 +1,32 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const { + detectProjectType, + describeProject, + hasClient, +} = require('../lib/project-type'); +const { createStandaloneProject, createMonorepoFixture, rmDir } = require('./helpers/tmp-project'); + +describe('lib/project-type', () => { + it('detects standalone projects', () => { + const root = createStandaloneProject(); + try { + assert.equal(detectProjectType(root), 'standalone'); + assert.equal(hasClient(root), false); + } finally { + rmDir(root); + } + }); + + it('detects monorepo checkouts', () => { + const root = createMonorepoFixture(); + try { + assert.equal(detectProjectType(root), 'monorepo'); + const info = describeProject(root); + assert.equal(info.hasClient, true); + assert.equal(info.hasServerSource, true); + } finally { + rmDir(root); + } + }); +}); diff --git a/cli/tests/publish-version.test.js b/cli/tests/publish-version.test.js new file mode 100644 index 0000000..e1051a9 --- /dev/null +++ b/cli/tests/publish-version.test.js @@ -0,0 +1,42 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +// incrementVersion is not exported — mirror logic for regression tests +function incrementVersion(version, type) { + const base = String(version).split('-')[0]; + const parts = base.split('.').map((p) => parseInt(p, 10)); + while (parts.length < 3) parts.push(0); + const major = Number.isFinite(parts[0]) ? parts[0] : 0; + const minor = Number.isFinite(parts[1]) ? parts[1] : 0; + const patch = Number.isFinite(parts[2]) ? parts[2] : 0; + + switch (type) { + case 'major': + return `${major + 1}.0.0`; + case 'minor': + return `${major}.${minor + 1}.0`; + case 'patch': + return `${major}.${minor}.${patch + 1}`; + case 'beta': { + const match = version.match(/^(.*)-beta\.(\d+)$/); + if (match) return `${match[1]}-beta.${parseInt(match[2], 10) + 1}`; + return `${version}-beta.1`; + } + default: + return version; + } +} + +describe('publish version bump', () => { + it('bumps patch', () => { + assert.equal(incrementVersion('3.0.3', 'patch'), '3.0.4'); + }); + + it('handles two-segment versions', () => { + assert.equal(incrementVersion('3.0', 'patch'), '3.0.1'); + }); + + it('bumps beta', () => { + assert.equal(incrementVersion('3.0.0-beta.1', 'beta'), '3.0.0-beta.2'); + }); +}); diff --git a/cli/tests/root.test.js b/cli/tests/root.test.js new file mode 100644 index 0000000..7b9dc67 --- /dev/null +++ b/cli/tests/root.test.js @@ -0,0 +1,45 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('path'); +const { + findProjectRoot, + isProjectRoot, + isPublishedCliRoot, + getMonorepoRoot, +} = require('../lib/root'); +const { createStandaloneProject, createMonorepoFixture, rmDir } = require('./helpers/tmp-project'); + +describe('lib/root', () => { + it('findProjectRoot discovers standalone .reactpress project', () => { + const root = createStandaloneProject(); + try { + assert.equal(findProjectRoot(root), root); + assert.equal(isProjectRoot(root), true); + } finally { + rmDir(root); + } + }); + + it('isPublishedCliRoot is false for user projects', () => { + const root = createStandaloneProject(); + try { + assert.equal(isPublishedCliRoot(root), false); + } finally { + rmDir(root); + } + }); + + it('getMonorepoRoot resolves to repo root in workspace', () => { + const mono = getMonorepoRoot(); + assert.ok(path.basename(mono) !== 'lib'); + }); + + it('findProjectRoot discovers monorepo via pnpm-workspace.yaml', () => { + const root = createMonorepoFixture(); + try { + assert.equal(findProjectRoot(root), root); + } finally { + rmDir(root); + } + }); +}); diff --git a/cli/ui/banner.js b/cli/ui/banner.js index 3a80de3..c4fa9f7 100644 --- a/cli/ui/banner.js +++ b/cli/ui/banner.js @@ -1,22 +1,441 @@ -const chalk = require('chalk'); -const { brand } = require('./theme'); -const { getMonorepoRoot } = require('../lib/root'); +const os = require('os'); const path = require('path'); +const chalk = require('chalk'); +const { + brand, + icon, + palette, + visibleLength, + padRight, + terminalWidth, + gradientText, + pulseBar, + statusLights, +} = require('./theme'); const { t } = require('../lib/i18n'); -function printBanner() { - const version = require(path.join(getMonorepoRoot(), 'package.json')).version; +/** + * "REACTPRESS" rendered in the ANSI Shadow font. + * Each row is exactly 81 single-cell columns, so we can size the surrounding + * cyber-card deterministically without measuring per-glyph widths. + */ +const TECH_LOGO = [ + '██████╗ ███████╗ █████╗ ██████╗████████╗██████╗ ██████╗ ███████╗███████╗███████╗', + '██╔══██╗██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝██╔════╝', + '██████╔╝█████╗ ███████║██║ ██║ ██████╔╝██████╔╝█████╗ ███████╗███████╗', + '██╔══██╗██╔══╝ ██╔══██║██║ ██║ ██╔═══╝ ██╔══██╗██╔══╝ ╚════██║╚════██║', + '██║ ██║███████╗██║ ██║╚██████╗ ██║ ██║ ██║ ██║███████╗███████║███████║', + '╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝', +]; + +const LOGO_WIDTH = 81; +const LOGO_GRADIENTS = [ + [palette.pink, palette.primary], + [palette.pink, palette.primary], + [palette.primary, palette.accent], + [palette.primary, palette.accent], + [palette.accent, palette.primary], + [palette.accent, palette.primary], +]; + +const REPO_URL = 'https://github.com/fecommunity/reactpress'; +/** + * Shorter, human-friendly form of REPO_URL shown beneath the title bar. + * The clickable hyperlink still resolves to the full https:// URL via + * `hyperlink()`, so users can `cmd+click` from any modern terminal. + */ +const REPO_DISPLAY = 'github.com/fecommunity/reactpress'; + +/** + * Wrap text in an OSC-8 hyperlink escape so terminals that support it (iTerm2, + * Warp, WezTerm, modern macOS Terminal, VS Code, GNOME Terminal, Kitty, …) + * render the label as a clickable link. We only emit the escape sequence when + * stdout is a real TTY — otherwise (CI logs, file redirects, dumb terminals) + * we fall back to the plain styled label so users never see the raw `]8;;`. + */ +function hyperlink(url, label) { + if (!process.stdout.isTTY) return label; + if (process.env.TERM === 'dumb') return label; + return `\u001B]8;;${url}\u0007${label}\u001B]8;;\u0007`; +} + +function safeReadCliVersion() { + try { + return require(path.join(__dirname, '..', 'package.json')).version; + } catch { + return 'dev'; + } +} + +function homify(p) { + if (!p) return p; + const home = os.homedir(); + if (home && p.startsWith(home)) { + return '~' + p.slice(home.length); + } + return p; +} + +function renderLogoLines() { + return TECH_LOGO.map((line, i) => gradientText(line, LOGO_GRADIENTS[i])); +} + +function modeChip(type) { + if (type === 'monorepo') { + return chalk + .bgHex(palette.primary) + .hex('#0B1220') + .bold(` ${t('banner.mode.monorepo')} `); + } + if (type === 'standalone') { + return chalk + .bgHex(palette.accent) + .hex('#0B1220') + .bold(` ${t('banner.mode.standalone')} `); + } + return chalk + .bgHex(palette.gray) + .hex('#0B1220') + .bold(` ${t('banner.mode.uninitialized')} `); +} + +/** + * Decide how "ready" the welcome banner should look. When a fully + * initialized project is detected we render the pulse bar at 100% and + * report `ONLINE` status, instead of the static 70% placeholder that used + * to make `doctor` runs look incomplete even when everything passed. + */ +function bannerReadyState(options) { + const type = options && options.project && options.project.type; + if (type === 'monorepo' || type === 'standalone') { + return { ratio: 1, ready: true }; + } + return { ratio: 0.4, ready: false }; +} + +/** + * Build the top edge of the cyber-card with a centered title block: + * ╔══════════[ REACTPRESS · v3.0.3 ]══════════╗ + */ +function brandedTopBorder(version, width) { + const titleBlock = + brand.primary('[') + + ' ' + + gradientText('REACTPRESS', [palette.primary, palette.accent], { bold: true }) + + ' ' + + brand.muted('·') + + ' ' + + brand.accent(`v${version}`) + + ' ' + + brand.primary(']'); + const dashTotal = Math.max(0, width - 2 - visibleLength(titleBlock)); + const left = Math.floor(dashTotal / 2); + const right = dashTotal - left; + return ( + brand.primary('╔' + '═'.repeat(left)) + + titleBlock + + brand.primary('═'.repeat(right) + '╗') + ); +} + +function bottomBorder(width) { + return brand.primary('╚' + '═'.repeat(width - 2) + '╝'); +} + +function bodyLine(content, innerWidth) { + const padded = padRight(content, innerWidth); + return brand.primary('║ ') + padded + brand.primary(' ║'); +} + +function emptyBodyLine(innerWidth) { + return bodyLine('', innerWidth); +} + +/** + * A subtle "CRT scan-line" rendered just under the logo. + * ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + */ +function scanline(width) { + return brand.muted('▔'.repeat(width)); +} + +/** + * Width of the left-side banner label column. + * + * Sized to fit our longest English label (`MODE` / `PATH` → 4 cells) + * plus a 2-cell trailing gap, which also accommodates the Chinese + * translations `模式` / `路径` (4 East-Asian cells each). + */ +const LABEL_WIDTH = 6; + +/** + * Centered, dim repo subtitle that sits directly under the top border. + * Replaces the previous in-body `◇ REPO ↗ …` row, which competed visually + * with the operational fields (MODE / PATH / pulse) further down. + */ +function repoSubline(innerWidth) { + const link = + brand.muted('↗ ') + hyperlink(REPO_URL, brand.accent.underline(REPO_DISPLAY)); + const pad = Math.max(0, Math.floor((innerWidth - visibleLength(link)) / 2)); + return ' '.repeat(pad) + link; +} + +/** + * Single-cell-wide chip label, e.g. `◇ MODE ▸ monorepo`. + */ +function infoRow(label, value) { + return ( + brand.accent('◇ ') + + brand.muted(padRight(label, LABEL_WIDTH)) + + ' ' + + brand.primary('▸ ') + + brand.dim(value) + ); +} + +/** + * Render the "command rail" navigation footer: + * ⟫ init ⟫ dev ⟫ build ⟫ deploy ⟫ publish + */ +function commandRail() { + const items = ['init', 'dev', 'build', 'deploy', 'publish']; + return items + .map( + (name) => + brand.primary('⟫ ') + gradientText(name, [palette.primary, palette.accent]) + ) + .join(brand.muted(' ')); +} + +/** + * Wide, full-fat cyber banner: ASCII logo + scan-line + bordered card. + */ +function printWideBanner(version, options) { + const cols = terminalWidth(); + const cardWidth = Math.min(Math.max(LOGO_WIDTH + 8, 88), cols - 2); + const innerWidth = cardWidth - 4; + + const lines = []; + lines.push(''); + lines.push(' ' + brandedTopBorder(version, cardWidth)); + lines.push(' ' + bodyLine(repoSubline(innerWidth), innerWidth)); + lines.push(' ' + emptyBodyLine(innerWidth)); + + const logoIndent = Math.max(0, Math.floor((innerWidth - LOGO_WIDTH) / 2)); + const indent = ' '.repeat(logoIndent); + for (const logoLine of renderLogoLines()) { + lines.push(' ' + bodyLine(indent + logoLine, innerWidth)); + } + + const scanWidth = Math.min(innerWidth - 2, LOGO_WIDTH); + const scanIndent = ' '.repeat(Math.max(0, Math.floor((innerWidth - scanWidth) / 2))); + lines.push(' ' + bodyLine(scanIndent + scanline(scanWidth), innerWidth)); + + lines.push(' ' + emptyBodyLine(innerWidth)); + + const ready = bannerReadyState(options); + const subtitle = + chalk.bold(brand.accent('◆ ')) + + gradientText(t('banner.subtitle').trim(), [palette.accent, palette.primary, palette.pink], { + bold: true, + }); + const stateLabel = ready.ready + ? brand.success(t('banner.systemOnline').trim()) + : brand.warn(t('banner.systemPending').trim()); + const right = + statusLights(ready.ready ? 'online' : 'pending') + + ' ' + + brand.dim(t('banner.systemLabel').trim() + ' ') + + stateLabel; + lines.push(' ' + bodyLine(subtitle + spacer(subtitle, right, innerWidth) + right, innerWidth)); + + lines.push(' ' + emptyBodyLine(innerWidth)); + + if (options.project) { + lines.push( + ' ' + + bodyLine( + brand.accent('◇ ') + + brand.muted(padRight(t('banner.label.mode').trim(), LABEL_WIDTH)) + + ' ' + + modeChip(options.project.type), + innerWidth + ) + ); + } + if (options.projectRoot) { + lines.push( + ' ' + + bodyLine( + infoRow(t('banner.label.path').trim(), homify(options.projectRoot)), + innerWidth + ) + ); + } + + const pulseWidth = Math.min(28, innerWidth - 18); + if (pulseWidth > 8) { + const filled = Math.max(1, Math.min(pulseWidth, Math.round(pulseWidth * ready.ratio))); + const pulse = pulseBar(pulseWidth, filled); + const pulseStatus = ready.ready + ? t('banner.pulseReady').trim() + : t('banner.pulsePending').trim(); + const pulseLine = + brand.accent('◇ ') + + brand.muted(padRight(t('banner.pulseLabel').trim(), LABEL_WIDTH)) + + ' ' + + pulse + + ' ' + + (ready.ready ? brand.success(pulseStatus) : brand.warn(pulseStatus)); + lines.push(' ' + bodyLine(pulseLine, innerWidth)); + } + + lines.push(' ' + emptyBodyLine(innerWidth)); + lines.push(' ' + bottomBorder(cardWidth)); + lines.push(' ' + commandRail()); + lines.push(''); + + for (const line of lines) console.log(line); +} + +/** + * Pad between a left-aligned and a right-aligned segment so they sit on the + * same line of the cyber card. + */ +function spacer(left, right, innerWidth) { + const used = visibleLength(left) + visibleLength(right); + const gap = Math.max(2, innerWidth - used); + return ' '.repeat(gap); +} + +/** + * Compact cyber banner for terminals that cannot host the full ASCII logo. + */ +function printCompactBanner(version, options) { + const cols = terminalWidth(); + const cardWidth = Math.min(cols - 2, 76); + const innerWidth = cardWidth - 4; + + const lines = []; + lines.push(''); + lines.push(' ' + brandedTopBorder(version, cardWidth)); + lines.push(' ' + bodyLine(repoSubline(innerWidth), innerWidth)); + lines.push(' ' + emptyBodyLine(innerWidth)); + + const ready = bannerReadyState(options); + const wordmark = + brand.primary('▌▍▎ ') + + gradientText('REACTPRESS', [palette.pink, palette.primary, palette.accent], { + bold: true, + }) + + brand.primary(' ▎▍▌'); + const lights = statusLights(ready.ready ? 'online' : 'pending'); + lines.push( + ' ' + bodyLine(wordmark + spacer(wordmark, lights, innerWidth) + lights, innerWidth) + ); + + const subtitle = + chalk.bold(brand.accent('◆ ')) + brand.dim(t('banner.subtitle').trim()); + lines.push(' ' + bodyLine(subtitle, innerWidth)); + lines.push(' ' + emptyBodyLine(innerWidth)); + + if (options.project) { + lines.push( + ' ' + + bodyLine( + brand.accent('◇ ') + + brand.muted(padRight(t('banner.label.mode').trim(), LABEL_WIDTH)) + + ' ' + + modeChip(options.project.type), + innerWidth + ) + ); + } + if (options.projectRoot) { + lines.push( + ' ' + + bodyLine( + infoRow(t('banner.label.path').trim(), homify(options.projectRoot)), + innerWidth + ) + ); + } + + lines.push(' ' + emptyBodyLine(innerWidth)); + lines.push(' ' + bottomBorder(cardWidth)); + lines.push(' ' + commandRail()); + lines.push(''); + + for (const line of lines) console.log(line); +} + +/** + * Single-line banner for ultra-narrow terminals (CI logs, embedded shells). + */ +function printMinimalBanner(version, options) { + const ready = bannerReadyState(options); + const wordmark = gradientText('REACTPRESS', [palette.pink, palette.primary, palette.accent], { + bold: true, + }); console.log(''); - console.log(brand.primary(' ╭─────────────────────────────────────────╮')); + console.log(` ${brand.primary('▌▍▎')} ${wordmark} ${brand.muted('·')} ${brand.accent(`v${version}`)} ${statusLights(ready.ready ? 'online' : 'pending')}`); + console.log(` ${brand.dim(t('banner.subtitle').trim())}`); + if (options.project) { + console.log(` ${modeChip(options.project.type)}`); + } + if (options.projectRoot) { + console.log(` ${icon.bullet} ${brand.dim(homify(options.projectRoot))}`); + } console.log( - brand.primary(' │') + - chalk.bold.white(' ReactPress') + - brand.muted(t('banner.subtitle')) + - brand.primary('│'), + ` ${brand.muted('↗')} ${hyperlink(REPO_URL, brand.accent.underline(REPO_URL))}` ); - console.log(brand.primary(' ╰─────────────────────────────────────────╯')); - console.log(brand.muted(` v${version} · init · dev · build · deploy · publish`)); console.log(''); } -module.exports = { printBanner }; +/** + * Print the top-of-screen banner. Adaptive to terminal width: collapses to a + * single-line greeting on very narrow terminals, otherwise renders a bordered + * cyber-card with the full ANSI Shadow logo when there is room. + * + * @param {{ + * projectRoot?: string, + * project?: { type: string, hasClient: boolean, hasServerSource: boolean } + * }} [options] + */ +function printBanner(options = {}) { + const version = safeReadCliVersion(); + const cols = terminalWidth(); + + if (cols < 64) { + printMinimalBanner(version, options); + return; + } + + if (cols < LOGO_WIDTH + 10) { + printCompactBanner(version, options); + return; + } + + printWideBanner(version, options); +} + +/** + * Box helper retained for backwards compatibility: a few callers still + * import `box` from this module to wrap arbitrary multi-line content. + */ +function box(lines, { width } = {}) { + const innerWidth = width + ? width - 4 + : lines.reduce((max, line) => Math.max(max, visibleLength(line)), 0); + + const horizontal = '═'.repeat(innerWidth + 2); + const top = brand.primary(` ╔${horizontal}╗`); + const bottom = brand.primary(` ╚${horizontal}╝`); + const body = lines.map((line) => { + const padded = padRight(line, innerWidth); + return brand.primary(' ║ ') + padded + brand.primary(' ║'); + }); + return [top, ...body, bottom]; +} + +module.exports = { printBanner, visibleLength, padRight, box }; diff --git a/cli/ui/interactive.js b/cli/ui/interactive.js index 700391f..282a2b0 100644 --- a/cli/ui/interactive.js +++ b/cli/ui/interactive.js @@ -1,55 +1,231 @@ +const fs = require('fs'); +const path = require('path'); const inquirer = require('inquirer'); +const ora = require('ora'); const open = require('open'); const { printBanner } = require('./banner'); -const { brand, label } = require('./theme'); +const { + brand, + icon, + label, + ok, + fail, + sectionHeader, + statusPill, + padRight, +} = require('./theme'); const { ensureOriginalCwd } = require('../lib/root'); +const { describeProject, hasClient } = require('../lib/project-type'); const { ensureProjectEnvironment } = require('../lib/bootstrap'); const { runDev } = require('../lib/dev'); const { runApiDev } = require('../lib/api-dev'); const { runLifecycleCommand } = require('../lib/lifecycle'); const { runDockerCommand } = require('../lib/docker'); +const { runNginxCommand } = require('../lib/nginx'); const { printUnifiedStatus } = require('../lib/status'); const { runDoctor } = require('../lib/doctor'); const { runBuild, TARGETS } = require('../lib/build'); const { runNodeScript } = require('../lib/spawn'); const { getClientBin } = require('../lib/paths'); -const { loadClientSiteUrl } = require('../lib/http'); +const { loadClientSiteUrl, loadServerSiteUrl, isHttpResponding } = require('../lib/http'); +const { isDockerRunning } = require('../lib/docker'); const { t } = require('../lib/i18n'); -function getMenuActions() { - return [ - { name: t('menu.dev'), value: 'dev' }, - { name: t('menu.init'), value: 'init' }, - { name: t('menu.status'), value: 'status' }, - { name: t('menu.doctor'), value: 'doctor' }, - new inquirer.Separator(), - { name: t('menu.devApi'), value: 'dev:api' }, - { name: t('menu.devClient'), value: 'dev:client' }, - { name: t('menu.serverStart'), value: 'server:start' }, - { name: t('menu.serverStop'), value: 'server:stop' }, - { name: t('menu.serverRestart'), value: 'server:restart' }, - new inquirer.Separator(), - { name: t('menu.build'), value: 'build' }, - { name: t('menu.dockerStart'), value: 'docker:start' }, - { name: t('menu.dockerUp'), value: 'docker:up' }, - { name: t('menu.dockerStop'), value: 'docker:stop' }, +function menuSection(title) { + return new inquirer.Separator(sectionHeader(title)); +} + +function formatChoice(key, text, hint) { + const keyCol = key ? brand.primary(padRight(key, 2)) : ' '; + const hintPart = hint ? brand.dim(` ${hint}`) : ''; + return `${keyCol} ${text}${hintPart}`; +} + +function assignShortcuts(items) { + let n = 0; + return items.map((item) => { + if (item instanceof inquirer.Separator || item.type === 'separator') { + return item; + } + n += 1; + const key = n <= 9 ? String(n) : ''; + return { + ...item, + name: formatChoice(key, item._label || item.name, item._hint), + short: item.value, + }; + }); +} + +function choice(labelKey, value, hintKey) { + return { + _label: t(labelKey), + _hint: hintKey ? t(hintKey) : '', + value, + }; +} + +function getMenuActions(project) { + const standalone = project.type === 'standalone'; + const monorepo = project.type === 'monorepo'; + const showClient = hasClient(project.root); + + const items = [ + menuSection(t('menu.section.run')), + choice('menu.dev', 'dev', 'menu.hint.dev'), + choice('menu.init', 'init', 'menu.hint.init'), + choice('menu.status', 'status', 'menu.hint.status'), + choice('menu.doctor', 'doctor', 'menu.hint.doctor'), + menuSection(t('menu.section.lifecycle')), + choice('menu.devApi', 'dev:api', 'menu.hint.devApi'), + ]; + + if (showClient) { + items.push(choice('menu.devClient', 'dev:client', 'menu.hint.devClient')); + } + + items.push( + choice('menu.serverStart', 'server:start', 'menu.hint.serverStart'), + choice('menu.serverStop', 'server:stop', 'menu.hint.serverStop'), + choice('menu.serverRestart', 'server:restart', 'menu.hint.serverRestart'), + menuSection(t('menu.section.build')), + choice('menu.build', 'build', 'menu.hint.build') + ); + + if (monorepo) { + items.push( + choice('menu.dockerStart', 'docker:start', 'menu.hint.dockerStart'), + choice('menu.dockerUp', 'docker:up', 'menu.hint.dockerUp'), + choice('menu.dockerStop', 'docker:stop', 'menu.hint.dockerStop') + ); + } else if (standalone) { + items.push( + choice('menu.dockerUp', 'docker:up', 'menu.hint.dockerUp'), + choice('menu.dockerStop', 'docker:stop', 'menu.hint.dockerStop') + ); + } + + items.push( + menuSection(t('menu.section.tools')), + choice('menu.nginxUp', 'nginx:up', 'menu.hint.nginxUp'), + choice('menu.nginxOpen', 'nginx:open', 'menu.hint.nginxOpen'), + choice('menu.nginxReload', 'nginx:reload', 'menu.hint.nginxReload'), + choice('menu.openAdmin', 'open:admin', 'menu.hint.openAdmin') + ); + + if (monorepo) { + items.push(choice('menu.publish', 'publish', 'menu.hint.publish')); + } + + items.push( new inquirer.Separator(), - { name: t('menu.openAdmin'), value: 'open:admin' }, - { name: t('menu.publish'), value: 'publish' }, - { name: t('menu.exit'), value: 'exit' }, + choice('menu.exit', 'exit', 'menu.hint.exit') + ); + + return assignShortcuts(items); +} + +function parseEnvFile(projectRoot) { + const envPath = path.join(projectRoot, '.env'); + const env = {}; + try { + if (!fs.existsSync(envPath)) return env; + for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) { + const m = line.match(/^([A-Z_]+)=(.*)$/); + if (m) env[m[1]] = m[2].trim().replace(/^['"]|['"]$/g, ''); + } + } catch { + // ignore + } + return env; +} + +async function probeDatabase(projectRoot) { + try { + const mysql = require('mysql2/promise'); + const env = parseEnvFile(projectRoot); + const conn = await mysql.createConnection({ + host: env.DB_HOST || '127.0.0.1', + port: Number(env.DB_PORT || 3306), + user: env.DB_USER || 'reactpress', + password: env.DB_PASSWD || env.DB_PASSWORD || 'reactpress', + database: env.DB_DATABASE || 'reactpress', + connectTimeout: 2000, + }); + await conn.ping(); + await conn.end(); + return true; + } catch { + return false; + } +} + +async function fetchContextStatus(projectRoot) { + const apiUrl = loadServerSiteUrl(projectRoot); + const [apiOk, dockerOk, dbOk] = await Promise.all([ + isHttpResponding(apiUrl, 1500), + Promise.resolve(isDockerRunning()), + probeDatabase(projectRoot), + ]); + return { apiOk, dbOk, dockerOk }; +} + +function printStatusPanel(status) { + const on = { on: t('menu.statusOn'), off: t('menu.statusOff') }; + const db = { + on: t('menu.statusReady'), + off: t('menu.statusNotReady'), + pending: t('menu.statusChecking'), + }; + const docker = { on: t('menu.statusYes'), off: t('menu.statusNo') }; + + console.log(sectionHeader(t('menu.statusHeader'))); + const rows = [ + [t('menu.statusLabelApi'), statusPill(status.apiOk, on)], + [t('menu.statusLabelDb'), statusPill(status.dbOk, db)], + [t('menu.statusLabelDocker'), statusPill(status.dockerOk, docker)], ]; + for (const [name, pill] of rows) { + console.log(` ${brand.muted(padRight(name, 10))} ${pill}`); + } + console.log(''); } -async function runMenuAction(action, projectRoot) { +async function printContextStatus(projectRoot) { + const spinner = ora({ + text: brand.dim(t('menu.statusChecking')), + color: 'magenta', + spinner: 'dots', + }).start(); + const status = await fetchContextStatus(projectRoot); + spinner.stop(); + printStatusPanel(status); + return status; +} + +async function withSpinner(text, fn) { + const spinner = ora({ text, color: 'magenta', spinner: 'dots' }).start(); + try { + const result = await fn(); + spinner.succeed(); + return result; + } catch (err) { + spinner.fail(); + throw err; + } +} + +async function runMenuAction(action, projectRoot, project) { switch (action) { case 'dev': console.log(label(t('menu.startingDev'))); await runDev(projectRoot); return false; case 'init': { - console.log(label(t('menu.initProject'))); - const result = await ensureProjectEnvironment(projectRoot); - console.log(brand.success(result.message || t('menu.done'))); + const result = await withSpinner(t('menu.initProject'), () => + ensureProjectEnvironment(projectRoot) + ); + console.log(ok(result.message || t('menu.done'))); return true; } case 'status': @@ -67,15 +243,21 @@ async function runMenuAction(action, projectRoot) { await runNodeScript(getClientBin(), [], { cwd: projectRoot }); return false; case 'server:start': { - const code = await runLifecycleCommand('start', projectRoot); + const code = await withSpinner(t('menu.startingApi'), () => + runLifecycleCommand('start', projectRoot) + ); if (code !== 0) process.exit(code); return true; } case 'server:stop': - await runLifecycleCommand('stop', projectRoot); + await withSpinner(t('menu.stoppingApi'), async () => { + await runLifecycleCommand('stop', projectRoot); + }); return true; case 'server:restart': { - const code = await runLifecycleCommand('restart', projectRoot); + const code = await withSpinner(t('menu.restartingApi'), () => + runLifecycleCommand('restart', projectRoot) + ); if (code !== 0) process.exit(code); return true; } @@ -92,6 +274,7 @@ async function runMenuAction(action, projectRoot) { type: 'list', name: 'target', message: t('menu.buildTarget'), + pageSize: 12, choices: buildChoices, }, ]); @@ -102,10 +285,23 @@ async function runMenuAction(action, projectRoot) { await runDockerCommand('start', projectRoot); return false; case 'docker:up': - await runDockerCommand('up', projectRoot); + await withSpinner(t('docker.starting'), () => runDockerCommand('up', projectRoot)); return true; case 'docker:stop': - await runDockerCommand('down', projectRoot); + await withSpinner(t('docker.stopping'), async () => { + await runDockerCommand('down', projectRoot); + }); + return true; + case 'nginx:up': + await withSpinner(t('cli.nginx.up'), async () => { + await runNginxCommand('up', projectRoot); + }); + return true; + case 'nginx:open': + await runNginxCommand('open', projectRoot); + return true; + case 'nginx:reload': + await runNginxCommand('reload', projectRoot); return true; case 'open:admin': { const url = loadClientSiteUrl(projectRoot); @@ -121,7 +317,6 @@ async function runMenuAction(action, projectRoot) { return true; } case 'exit': - console.log(brand.muted(t('menu.goodbye'))); return false; default: return true; @@ -130,7 +325,12 @@ async function runMenuAction(action, projectRoot) { async function runInteractiveLoop() { const projectRoot = ensureOriginalCwd(); - printBanner(); + const project = describeProject(projectRoot); + + printBanner({ projectRoot, project }); + await printContextStatus(projectRoot); + console.log(` ${brand.dim(t('menu.shortcuts'))}`); + console.log(''); let loop = true; while (loop) { @@ -138,9 +338,10 @@ async function runInteractiveLoop() { { type: 'list', name: 'action', - message: t('menu.prompt'), - pageSize: 14, - choices: getMenuActions(), + message: `${brand.primary(t('menu.actionPrefix'))} ${brand.dim('›')}`, + pageSize: 20, + loop: false, + choices: getMenuActions(project), }, ]); @@ -150,24 +351,15 @@ async function runInteractiveLoop() { } try { - const stay = await runMenuAction(action, projectRoot); - if (!stay) { - break; - } + const stay = await runMenuAction(action, projectRoot, project); + if (!stay) break; - if (action !== 'status') { - const { again } = await inquirer.prompt([ - { - type: 'confirm', - name: 'again', - message: t('menu.back'), - default: true, - }, - ]); - loop = again; + if (action !== 'status' && action !== 'doctor') { + console.log(''); + await printContextStatus(projectRoot); } } catch (err) { - console.error(brand.error(` ${err.message || err}`)); + console.error(fail(err.message || err)); const { retry } = await inquirer.prompt([ { type: 'confirm', @@ -177,8 +369,12 @@ async function runInteractiveLoop() { }, ]); loop = retry; + if (loop) { + console.log(''); + await printContextStatus(projectRoot); + } } } } -module.exports = { runInteractiveLoop, runMenuAction }; +module.exports = { runInteractiveLoop, runMenuAction, getMenuActions }; diff --git a/cli/ui/theme.js b/cli/ui/theme.js index d752db2..7b06f7a 100644 --- a/cli/ui/theme.js +++ b/cli/ui/theme.js @@ -1,25 +1,265 @@ const chalk = require('chalk'); +/** + * ReactPress CLI visual identity — a single source of truth so banners, + * menus, status, doctor, build output all share the same colours and glyphs. + */ +const palette = { + primary: '#7C5CFF', + accent: '#22D3EE', + pink: '#F472B6', + green: '#22C55E', + amber: '#F59E0B', + red: '#EF4444', + gray: '#6B7280', + dim: '#9CA3AF', +}; + const brand = { - primary: chalk.hex('#6366f1'), - accent: chalk.hex('#22d3ee'), - success: chalk.green, - warn: chalk.yellow, - error: chalk.red, - muted: chalk.gray, + primary: chalk.hex(palette.primary), + accent: chalk.hex(palette.accent), + pink: chalk.hex(palette.pink), + success: chalk.hex(palette.green), + warn: chalk.hex(palette.amber), + error: chalk.hex(palette.red), + muted: chalk.hex(palette.gray), + dim: chalk.hex(palette.dim), bold: chalk.bold, }; +const icon = { + ok: brand.success('✓'), + fail: brand.error('✗'), + warn: brand.warn('⚠'), + info: brand.accent('ℹ'), + arrow: brand.primary('›'), + pointer: brand.primary('▸'), + bullet: brand.muted('·'), + dotOn: brand.success('●'), + dotOff: brand.muted('○'), + dotPending: brand.warn('◐'), + dotInfo: brand.accent('●'), + spark: brand.primary('✱'), + link: brand.muted('↗'), +}; + +/** + * Whether a Unicode code point should occupy two terminal cells. + * + * Covers the common "East Asian Wide / Full-width" ranges that show up in + * Chinese / Japanese / Korean text plus full-width punctuation. We + * deliberately do not pull in a heavy dependency like `string-width` to keep + * the CLI's startup cheap. + */ +function isWideCodePoint(cp) { + return ( + (cp >= 0x1100 && cp <= 0x115f) || + (cp >= 0x2e80 && cp <= 0x303e) || + (cp >= 0x3041 && cp <= 0x33ff) || + (cp >= 0x3400 && cp <= 0x4dbf) || + (cp >= 0x4e00 && cp <= 0x9fff) || + (cp >= 0xa000 && cp <= 0xa4cf) || + (cp >= 0xac00 && cp <= 0xd7a3) || + (cp >= 0xf900 && cp <= 0xfaff) || + (cp >= 0xfe30 && cp <= 0xfe4f) || + (cp >= 0xff00 && cp <= 0xff60) || + (cp >= 0xffe0 && cp <= 0xffe6) || + (cp >= 0x1f300 && cp <= 0x1f64f) || + (cp >= 0x1f900 && cp <= 0x1f9ff) || + (cp >= 0x20000 && cp <= 0x2fffd) || + (cp >= 0x30000 && cp <= 0x3fffd) + ); +} + +/** + * Visible terminal-cell width of a string, after stripping ANSI colour codes + * and accounting for East Asian wide characters (which occupy 2 cells). + */ +function visibleLength(text) { + const stripped = String(text) + .replace(/\u001b\[[0-9;]*m/g, '') + .replace(/\u001b\]8;[^\u0007\u001b]*(?:\u0007|\u001b\\)/g, ''); + let width = 0; + for (const ch of stripped) { + const cp = ch.codePointAt(0); + if (cp === undefined) continue; + if (cp < 0x20 || (cp >= 0x7f && cp < 0xa0)) continue; + width += isWideCodePoint(cp) ? 2 : 1; + } + return width; +} + +function padRight(text, width) { + const len = visibleLength(text); + if (len >= width) return text; + return text + ' '.repeat(width - len); +} + +function padLeft(text, width) { + const len = visibleLength(text); + if (len >= width) return text; + return ' '.repeat(width - len) + text; +} + +function terminalWidth(fallback = 80) { + const cols = Number(process.stdout.columns) || fallback; + return Math.max(48, Math.min(120, cols)); +} + +function divider(width = 44, char = '─', colorize = brand.muted) { + return colorize(char.repeat(width)); +} + +/** + * Cyberpunk-flavoured progress-bar style decoration. + * `filled` segments use the primary colour, the trailing track stays muted. + */ +function pulseBar(width = 24, filled = Math.ceil(width * 0.7)) { + const f = Math.max(0, Math.min(width, filled)); + const head = brand.primary('▰'.repeat(f)); + const tail = brand.muted('▱'.repeat(Math.max(0, width - f))); + return `${head}${tail}`; +} + +/** + * Three-light status indicator used in the top-right of the banner. + * Mimics the running-light cluster you'd see on a server rack. + */ +function statusLights(state = 'online') { + if (state === 'offline') { + return `${brand.muted('●')} ${brand.muted('●')} ${brand.muted('●')}`; + } + if (state === 'pending') { + return `${brand.warn('●')} ${brand.warn('●')} ${brand.muted('○')}`; + } + return `${brand.success('●')} ${brand.warn('●')} ${brand.muted('○')}`; +} + +function hex2rgb(h) { + const s = h.replace('#', ''); + return { + r: parseInt(s.substring(0, 2), 16), + g: parseInt(s.substring(2, 4), 16), + b: parseInt(s.substring(4, 6), 16), + }; +} + +function rgb2hex(r, g, b) { + const pad = (n) => n.toString(16).padStart(2, '0'); + return `#${pad(r)}${pad(g)}${pad(b)}`; +} + +function mixHex(a, b, t) { + const pa = hex2rgb(a); + const pb = hex2rgb(b); + const r = Math.round(pa.r + (pb.r - pa.r) * t); + const g = Math.round(pa.g + (pb.g - pa.g) * t); + const bl = Math.round(pa.b + (pb.b - pa.b) * t); + return rgb2hex(r, g, bl); +} + +/** + * Paint a string with a left→right linear gradient across `colors` (hex). + * Falls back to plain text when stdout does not support truecolor. + */ +function gradientText(text, colors = [palette.primary, palette.accent], { bold = false } = {}) { + if (!text) return ''; + const supports = chalk.supportsColor && chalk.supportsColor.has16m; + if (!supports || colors.length < 2) { + const c = chalk.hex(colors[0] || palette.primary); + return bold ? c.bold(text) : c(text); + } + const chars = [...String(text)]; + const n = Math.max(chars.length - 1, 1); + return chars + .map((ch, i) => { + const ratio = i / n; + const idx = ratio * (colors.length - 1); + const lo = Math.floor(idx); + const hi = Math.min(colors.length - 1, lo + 1); + const local = idx - lo; + const c = chalk.hex(mixHex(colors[lo], colors[hi], local)); + return bold ? c.bold(ch) : c(ch); + }) + .join(''); +} + function label(text) { - return brand.primary(`› ${text}`); + return `${icon.arrow} ${brand.primary(text)}`; } function ok(text) { - return brand.success(`✓ ${text}`); + return `${icon.ok} ${brand.success(text)}`; } function fail(text) { - return brand.error(`✗ ${text}`); + return `${icon.fail} ${brand.error(text)}`; +} + +function warn(text) { + return `${icon.warn} ${brand.warn(text)}`; +} + +function info(text) { + return `${icon.info} ${brand.accent(text)}`; +} + +function chip(text, color = brand.primary) { + return color(`[ ${text} ]`); +} + +function kv(key, value, { keyWidth = 10, valueColor = (s) => s } = {}) { + return `${brand.muted(padRight(key, keyWidth))} ${valueColor(value)}`; } -module.exports = { brand, label, ok, fail }; +/** + * Render a 3-state status pill, e.g. `● online` / `○ offline` / `◐ pending`. + * + * @param {boolean | 'pending'} state + * @param {{ on?: string, off?: string, pending?: string }} labels + */ +function statusPill(state, labels = {}) { + if (state === 'pending') { + return `${icon.dotPending} ${brand.warn(labels.pending || 'pending')}`; + } + if (state === true) { + return `${icon.dotOn} ${brand.success(labels.on || 'online')}`; + } + return `${icon.dotOff} ${brand.dim(labels.off || 'offline')}`; +} + +/** + * Render a single-line section header: ` ── Title ────────────`. + */ +function sectionHeader(title, { width } = {}) { + const w = width ?? terminalWidth(); + const prefix = brand.muted('── '); + const t = brand.bold(brand.primary(title)); + const usedLen = visibleLength(prefix) + visibleLength(t) + 2; + const fillLen = Math.max(3, w - usedLen - 2); + const fill = brand.muted('─'.repeat(fillLen)); + return ` ${prefix}${t} ${fill}`; +} + +module.exports = { + palette, + brand, + icon, + label, + ok, + fail, + warn, + info, + chip, + kv, + statusPill, + sectionHeader, + visibleLength, + padRight, + padLeft, + terminalWidth, + divider, + gradientText, + pulseBar, + statusLights, +}; diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 6b0184e..5d7933c 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -36,7 +36,7 @@ services: image: nginx:alpine container_name: reactpress_nginx ports: - - "8080:80" + - "${NGINX_PORT:-80}:80" volumes: - ./nginx.dev.conf:/etc/nginx/conf.d/default.conf networks: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 2196089..321609b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -42,7 +42,7 @@ services: image: nginx:alpine container_name: reactpress_nginx ports: - - "8080:80" + - "${NGINX_PORT:-80}:80" depends_on: - client extra_hosts: diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 1f68d16..68292a9 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -51,6 +51,11 @@ const config: Config = { onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'warn', + headTags: [ + {tagName: 'meta', attributes: {property: 'og:type', content: 'website'}}, + {tagName: 'meta', attributes: {property: 'og:site_name', content: 'ReactPress'}}, + ], + // Even if you don't use internationalization, you can use this field to set // useful metadata like html lang. For example, if your site is Chinese, you // may want to replace "en" with "zh-Hans". @@ -63,6 +68,40 @@ const config: Config = { [ '@docusaurus/preset-classic', { + sitemap: { + lastmod: 'date', + changefreq: 'weekly', + priority: 0.5, + ignorePatterns: ['/tags/**', '/markdown-page'], + filename: 'sitemap.xml', + createSitemapItems: async (params) => { + const {defaultCreateSitemapItems, ...rest} = params; + const items = await defaultCreateSitemapItems(rest); + return items + .filter((item) => !item.url.includes('/markdown-page')) + .map((item) => { + const pathname = new URL(item.url).pathname; + if (pathname === '/' || pathname === '/zh/') { + return {...item, priority: 1.0, changefreq: 'daily' as const}; + } + if (pathname.endsWith('/docs/intro')) { + return {...item, priority: 0.9}; + } + if (pathname.includes('/blog/changelog')) { + return {...item, priority: 0.8}; + } + if ( + pathname.includes('/blog/archive') || + pathname.includes('/blog/authors') || + pathname.endsWith('/blog/tags') || + pathname.includes('/blog/tags/') + ) { + return {...item, priority: 0.3}; + } + return item; + }); + }, + }, docs: { path: './tutorial', sidebarPath: './sidebars.ts', @@ -178,7 +217,14 @@ const config: Config = { darkTheme: prismThemes.dracula, }, metadata: [ - { name: 'keywords', content: 'reactpress, blog, cms, next.js, react, nest.js' }, + { + name: 'keywords', + content: + 'reactpress, blog, cms, next.js, react, nest.js, headless cms, content management, 博客, 内容管理', + }, + {name: 'robots', content: 'index, follow, max-image-preview:large'}, + {name: 'googlebot', content: 'index, follow'}, + {name: 'twitter:card', content: 'summary_large_image'}, ], } satisfies Preset.ThemeConfig, }; diff --git a/docs/package.json b/docs/package.json index 2daad1f..6652871 100644 --- a/docs/package.json +++ b/docs/package.json @@ -5,6 +5,7 @@ "scripts": { "docusaurus": "docusaurus", "dev": "docusaurus start", + "preview": "docusaurus build && docusaurus serve", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", diff --git a/docs/src/pages/Home/CallToAction/index.tsx b/docs/src/components/Home/CallToAction/index.tsx similarity index 100% rename from docs/src/pages/Home/CallToAction/index.tsx rename to docs/src/components/Home/CallToAction/index.tsx diff --git a/docs/src/pages/Home/CallToAction/styles.module.css b/docs/src/components/Home/CallToAction/styles.module.css similarity index 100% rename from docs/src/pages/Home/CallToAction/styles.module.css rename to docs/src/components/Home/CallToAction/styles.module.css diff --git a/docs/src/pages/Home/Hero/Devices.tsx b/docs/src/components/Home/Hero/Devices.tsx similarity index 100% rename from docs/src/pages/Home/Hero/Devices.tsx rename to docs/src/components/Home/Hero/Devices.tsx diff --git a/docs/src/pages/Home/Hero/FloorBackground.tsx b/docs/src/components/Home/Hero/FloorBackground.tsx similarity index 100% rename from docs/src/pages/Home/Hero/FloorBackground.tsx rename to docs/src/components/Home/Hero/FloorBackground.tsx diff --git a/docs/src/pages/Home/Hero/GridBackground.tsx b/docs/src/components/Home/Hero/GridBackground.tsx similarity index 100% rename from docs/src/pages/Home/Hero/GridBackground.tsx rename to docs/src/components/Home/Hero/GridBackground.tsx diff --git a/docs/src/pages/Home/Hero/index.tsx b/docs/src/components/Home/Hero/index.tsx similarity index 91% rename from docs/src/pages/Home/Hero/index.tsx rename to docs/src/components/Home/Hero/index.tsx index 5c93d78..bea123d 100644 --- a/docs/src/pages/Home/Hero/index.tsx +++ b/docs/src/components/Home/Hero/index.tsx @@ -1,9 +1,9 @@ import React from 'react'; import Link from '@docusaurus/Link'; -import Logo from '@site/src/pages/Home/Logo'; -import GridBackground from '@site/src/pages/Home/Hero/GridBackground'; -import FloorBackground from '@site/src/pages/Home/Hero/FloorBackground'; -import Devices from '@site/src/pages/Home/Hero/Devices'; +import Logo from '@site/src/components/Home/Logo'; +import GridBackground from '@site/src/components/Home/Hero/GridBackground'; +import FloorBackground from '@site/src/components/Home/Hero/FloorBackground'; +import Devices from '@site/src/components/Home/Hero/Devices'; import CliCommandBlock from '@site/src/components/CliCommandBlock'; import styles from './styles.module.css'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; diff --git a/docs/src/pages/Home/Hero/styles.module.css b/docs/src/components/Home/Hero/styles.module.css similarity index 100% rename from docs/src/pages/Home/Hero/styles.module.css rename to docs/src/components/Home/Hero/styles.module.css diff --git a/docs/src/pages/Home/Highlights/index.tsx b/docs/src/components/Home/Highlights/index.tsx similarity index 100% rename from docs/src/pages/Home/Highlights/index.tsx rename to docs/src/components/Home/Highlights/index.tsx diff --git a/docs/src/pages/Home/Highlights/styles.module.css b/docs/src/components/Home/Highlights/styles.module.css similarity index 100% rename from docs/src/pages/Home/Highlights/styles.module.css rename to docs/src/components/Home/Highlights/styles.module.css diff --git a/docs/src/pages/Home/Logo.tsx b/docs/src/components/Home/Logo.tsx similarity index 100% rename from docs/src/pages/Home/Logo.tsx rename to docs/src/components/Home/Logo.tsx diff --git a/docs/src/components/Home/index.tsx b/docs/src/components/Home/index.tsx new file mode 100644 index 0000000..f862950 --- /dev/null +++ b/docs/src/components/Home/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import Hero from '@site/src/components/Home/Hero'; +import Highlights from '@site/src/components/Home/Highlights'; +import CallToAction from '@site/src/components/Home/CallToAction'; + +export default function Home() { + return ( + <> + + + + + ); +} diff --git a/docs/src/css/customTheme.scss b/docs/src/css/customTheme.scss index cc56d86..021d40b 100644 --- a/docs/src/css/customTheme.scss +++ b/docs/src/css/customTheme.scss @@ -46,43 +46,45 @@ --ifm-navbar-sidebar-width: 340px; } - --logo: #087ea4; - --home-hero-floor-background: rgb(236, 248, 250); - --home-hero-floor-background-bottom: white; - --home-button-primary: #087ea4; - --home-button-primary-text: white; - --home-button-primary-hover: #4e98b6; - --home-button-secondary: white; - --home-button-secondary-text: #404756; - --home-button-secondary-border: #bcc1cd; - --home-button-secondary-hover: #f8f9fa; - --home-hero-devices-border: #5e687e; - --home-hero-devices-stop: #66728b; - --home-hero-devices-skeleton-element: #cfe6ee; - --home-hero-devices-background: #fff; - --home-hero-devices-icon: #000; - --home-hero-devices-icon-border: #e5e7eb; - --home-hero-floor: #acddec; - --home-hero-grid-icon-background: #f8f9fa; - --home-hero-grid-icon: #e1e4e8; - --home-hero-grid-grid: #ced4da; - --home-section-top: white; - --home-section-bottom: #efeff2; - --home-secondary-text: #404756; - --home-feature-background-1: #f1dbfc; - --home-feature-background-2: #e1eefc; - --home-feature-background-3: #d4f3e7; - --home-border: #e5e7eb; - --home-background: #fff; - --home-secondary-background: #f6f7f9; - --home-text: #1c1e21; - --home-code-red: #d73a49; - --home-code-purple: #6f42c1; - --home-code-green: #22863a; - --home-code-diff-removed: #f8514926; - --home-code-diff-removed-word: #f8514966; - --home-code-diff-added: #2ea04326; - --home-code-diff-added-word: #2ea04366; + & { + --logo: #087ea4; + --home-hero-floor-background: rgb(236, 248, 250); + --home-hero-floor-background-bottom: white; + --home-button-primary: #087ea4; + --home-button-primary-text: white; + --home-button-primary-hover: #4e98b6; + --home-button-secondary: white; + --home-button-secondary-text: #404756; + --home-button-secondary-border: #bcc1cd; + --home-button-secondary-hover: #f8f9fa; + --home-hero-devices-border: #5e687e; + --home-hero-devices-stop: #66728b; + --home-hero-devices-skeleton-element: #cfe6ee; + --home-hero-devices-background: #fff; + --home-hero-devices-icon: #000; + --home-hero-devices-icon-border: #e5e7eb; + --home-hero-floor: #acddec; + --home-hero-grid-icon-background: #f8f9fa; + --home-hero-grid-icon: #e1e4e8; + --home-hero-grid-grid: #ced4da; + --home-section-top: white; + --home-section-bottom: #efeff2; + --home-secondary-text: #404756; + --home-feature-background-1: #f1dbfc; + --home-feature-background-2: #e1eefc; + --home-feature-background-3: #d4f3e7; + --home-border: #e5e7eb; + --home-background: #fff; + --home-secondary-background: #f6f7f9; + --home-text: #1c1e21; + --home-code-red: #d73a49; + --home-code-purple: #6f42c1; + --home-code-green: #22863a; + --home-code-diff-removed: #f8514926; + --home-code-diff-removed-word: #f8514966; + --home-code-diff-added: #2ea04326; + --home-code-diff-added-word: #2ea04366; + } @media (max-width: 900px) { --home-hero-grid-icon: transparent; @@ -116,43 +118,45 @@ html[data-theme="dark"] { --docsearch-container-background: rgba(0, 0, 0, 0.6); } - --logo: #58c4dc; - --home-hero-floor-background: #151517; - --home-hero-floor-background-bottom: #1b1b1d; - --home-button-primary: #58c4dc; - --home-button-primary-text: #1b1b1d; - --home-button-primary-hover: #71d6ec; - --home-button-secondary: #1b1b1d; - --home-button-secondary-text: #f6f7f9; - --home-button-secondary-border: #4e5668; - --home-button-secondary-hover: #2b2b2d; - --home-hero-devices-border: #404756; - --home-hero-devices-stop: #4a5160; - --home-hero-devices-skeleton-element: #404756; - --home-hero-devices-background: #242426; - --home-hero-devices-icon: #fff; - --home-hero-devices-icon-border: #404756; - --home-hero-floor: #30363d; - --home-hero-grid-icon-background: #1e2025; - --home-hero-grid-icon: #282c36; - --home-hero-grid-grid: #30363d; - --home-section-top: #1b1b1d; - --home-section-bottom: #111113; - --home-secondary-text: #c0c1c4; - --home-feature-background-1: #333049; - --home-feature-background-2: #1c3950; - --home-feature-background-3: #1d413e; - --home-border: #404756; - --home-background: #242426; - --home-secondary-background: #2c2c2e; - --home-text: #e3e3e3; - --home-code-red: #f46b78; - --home-code-purple: #a77cf7; - --home-code-green: #74e68f; - --home-code-diff-removed: #f8514926; - --home-code-diff-removed-word: #f8514966; - --home-code-diff-added: #2ea04326; - --home-code-diff-added-word: #2ea04366; + & { + --logo: #58c4dc; + --home-hero-floor-background: #151517; + --home-hero-floor-background-bottom: #1b1b1d; + --home-button-primary: #58c4dc; + --home-button-primary-text: #1b1b1d; + --home-button-primary-hover: #71d6ec; + --home-button-secondary: #1b1b1d; + --home-button-secondary-text: #f6f7f9; + --home-button-secondary-border: #4e5668; + --home-button-secondary-hover: #2b2b2d; + --home-hero-devices-border: #404756; + --home-hero-devices-stop: #4a5160; + --home-hero-devices-skeleton-element: #404756; + --home-hero-devices-background: #242426; + --home-hero-devices-icon: #fff; + --home-hero-devices-icon-border: #404756; + --home-hero-floor: #30363d; + --home-hero-grid-icon-background: #1e2025; + --home-hero-grid-icon: #282c36; + --home-hero-grid-grid: #30363d; + --home-section-top: #1b1b1d; + --home-section-bottom: #111113; + --home-secondary-text: #c0c1c4; + --home-feature-background-1: #333049; + --home-feature-background-2: #1c3950; + --home-feature-background-3: #1d413e; + --home-border: #404756; + --home-background: #242426; + --home-secondary-background: #2c2c2e; + --home-text: #e3e3e3; + --home-code-red: #f46b78; + --home-code-purple: #a77cf7; + --home-code-green: #74e68f; + --home-code-diff-removed: #f8514926; + --home-code-diff-removed-word: #f8514966; + --home-code-diff-added: #2ea04326; + --home-code-diff-added-word: #2ea04366; + } @media (max-width: 900px) { --home-hero-grid-icon: transparent; diff --git a/docs/src/pages/Home/index.tsx b/docs/src/pages/Home/index.tsx deleted file mode 100644 index 5709bce..0000000 --- a/docs/src/pages/Home/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; - -import Hero from '@site/src/pages/Home/Hero'; -import Highlights from '@site/src/pages/Home/Highlights'; -import CallToAction from '@site/src/pages/Home/CallToAction'; - -export default function Home() { - return ( - <> - - - - - ); -} diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index dfe2061..b4996aa 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -6,13 +6,14 @@ */ import Head from '@docusaurus/Head'; -import Home from '@site/src/pages/Home'; +import Home from '@site/src/components/Home'; import Layout from '@theme/Layout'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import { translate } from '@docusaurus/Translate'; const Index = () => { - const { siteConfig } = useDocusaurusContext(); + const {siteConfig} = useDocusaurusContext(); + const canonicalUrl = `${siteConfig.url}${siteConfig.baseUrl}`.replace(/\/$/, '') + '/'; const title = `${siteConfig.title} · ${siteConfig.tagline}`; const description = translate({ message: @@ -24,8 +25,12 @@ const Index = () => { {title} + + + + diff --git a/docs/src/pages/markdown-page.md b/docs/src/pages/markdown-page.md index 9756c5b..ebeccec 100644 --- a/docs/src/pages/markdown-page.md +++ b/docs/src/pages/markdown-page.md @@ -1,5 +1,6 @@ --- title: Markdown page example +unlisted: true --- # Markdown page example diff --git a/docs/static/robots.txt b/docs/static/robots.txt index 6f27bb6..df456fe 100644 --- a/docs/static/robots.txt +++ b/docs/static/robots.txt @@ -1,2 +1,5 @@ User-agent: * -Disallow: \ No newline at end of file +Allow: / + +Sitemap: https://reactpress.surge.sh/sitemap.xml +Sitemap: https://reactpress.surge.sh/zh/sitemap.xml diff --git a/docs/tutorial/intro.md b/docs/tutorial/intro.md index 01f4a7d..2cba26b 100644 --- a/docs/tutorial/intro.md +++ b/docs/tutorial/intro.md @@ -2,6 +2,8 @@ sidebar_position: 1 id: intro title: 介绍 +description: ReactPress 开源发布平台与 CMS:基于 React、Next.js、NestJS,一条命令零配置起站。 +keywords: [reactpress, cms, 博客, next.js, react] --- ## 项目简介 diff --git a/package.json b/package.json index 3e476da..abcd70a 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "publish:build": "node ./cli/bin/reactpress.js publish --build", "prepare": "husky", "precommit": "lint-staged", + "test": "pnpm --dir cli test", + "test:cli": "pnpm --dir cli test", "test:smoke": "node scripts/smoke-api-health.mjs", "test:benchmark": "node scripts/benchmark-cold-start.mjs" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fddafe5..6633ad5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2004,74 +2004,98 @@ packages: '@corex/deepmerge@2.6.148': resolution: {integrity: sha512-6QMz0/2h5C3ua51iAnXMPWFbb1QOU1UvSM4bKBw5mzdT+WtLgjbETBBIQZ+Sh9WvEcGwlAt/DEdRpIC3XlDBMA==} - '@csstools/cascade-layer-name-parser@2.0.4': - resolution: {integrity: sha512-7DFHlPuIxviKYZrOiwVU/PiHLm3lLUR23OMuEEtfEOQTOp9hzQ2JjdY6X5H18RVuUPJqSCI+qNnD5iOLMVE0bA==} + '@csstools/cascade-layer-name-parser@2.0.5': + resolution: {integrity: sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.4 - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/color-helpers@5.0.2': - resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} - '@csstools/css-calc@2.1.2': - resolution: {integrity: sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==} + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.4 - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-color-parser@3.0.8': - resolution: {integrity: sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==} + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.4 - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-parser-algorithms@3.0.4': - resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-tokenizer@3.0.3': - resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@csstools/media-query-list-parser@4.0.2': - resolution: {integrity: sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==} + '@csstools/media-query-list-parser@4.0.3': + resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.4 - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/postcss-cascade-layers@5.0.1': - resolution: {integrity: sha512-XOfhI7GShVcKiKwmPAnWSqd2tBR0uxt+runAxttbSp/LY2U16yAVPmAf7e9q4JJ0d+xMNmpwNDLBXnmRCl3HMQ==} + '@csstools/postcss-alpha-function@1.0.1': + resolution: {integrity: sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-color-function@4.0.8': - resolution: {integrity: sha512-9dUvP2qpZI6PlGQ/sob+95B3u5u7nkYt9yhZFCC7G9HBRHBxj+QxS/wUlwaMGYW0waf+NIierI8aoDTssEdRYw==} + '@csstools/postcss-cascade-layers@5.0.2': + resolution: {integrity: sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-color-mix-function@3.0.8': - resolution: {integrity: sha512-yuZpgWUzqZWQhEqfvtJufhl28DgO9sBwSbXbf/59gejNuvZcoUTRGQZhzhwF4ccqb53YAGB+u92z9+eSKoB4YA==} + '@csstools/postcss-color-function-display-p3-linear@1.0.1': + resolution: {integrity: sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-content-alt-text@2.0.4': - resolution: {integrity: sha512-YItlZUOuZJCBlRaCf8Aucc1lgN41qYGALMly0qQllrxYJhiyzlI6RxOTMUvtWk+KhS8GphMDsDhKQ7KTPfEMSw==} + '@csstools/postcss-color-function@4.0.12': + resolution: {integrity: sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-exponential-functions@2.0.7': - resolution: {integrity: sha512-XTb6Mw0v2qXtQYRW9d9duAjDnoTbBpsngD7sRNLmYDjvwU2ebpIHplyxgOeo6jp/Kr52gkLi5VaK5RDCqzMzZQ==} + '@csstools/postcss-color-mix-function@3.0.12': + resolution: {integrity: sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-color-mix-variadic-function-arguments@1.0.2': + resolution: {integrity: sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-content-alt-text@2.0.8': + resolution: {integrity: sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-contrast-color-function@2.0.12': + resolution: {integrity: sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-exponential-functions@2.0.9': + resolution: {integrity: sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -2082,26 +2106,26 @@ packages: peerDependencies: postcss: ^8.4 - '@csstools/postcss-gamut-mapping@2.0.8': - resolution: {integrity: sha512-/K8u9ZyGMGPjmwCSIjgaOLKfic2RIGdFHHes84XW5LnmrvdhOTVxo255NppHi3ROEvoHPW7MplMJgjZK5Q+TxA==} + '@csstools/postcss-gamut-mapping@2.0.11': + resolution: {integrity: sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-gradients-interpolation-method@5.0.8': - resolution: {integrity: sha512-CoHQ/0UXrvxLovu0ZeW6c3/20hjJ/QRg6lyXm3dZLY/JgvRU6bdbQZF/Du30A4TvowfcgvIHQmP1bNXUxgDrAw==} + '@csstools/postcss-gradients-interpolation-method@5.0.12': + resolution: {integrity: sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-hwb-function@4.0.8': - resolution: {integrity: sha512-LpFKjX6hblpeqyych1cKmk+3FJZ19QmaJtqincySoMkbkG/w2tfbnO5oE6mlnCTXcGUJ0rCEuRHvTqKK0nHYUQ==} + '@csstools/postcss-hwb-function@4.0.12': + resolution: {integrity: sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-ic-unit@4.0.0': - resolution: {integrity: sha512-9QT5TDGgx7wD3EEMN3BSUG6ckb6Eh5gSPT5kZoVtUuAonfPmLDJyPhqR4ntPpMYhUKAMVKAg3I/AgzqHMSeLhA==} + '@csstools/postcss-ic-unit@4.0.4': + resolution: {integrity: sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -2112,14 +2136,14 @@ packages: peerDependencies: postcss: ^8.4 - '@csstools/postcss-is-pseudo-class@5.0.1': - resolution: {integrity: sha512-JLp3POui4S1auhDR0n8wHd/zTOWmMsmK3nQd3hhL6FhWPaox5W7j1se6zXOG/aP07wV2ww0lxbKYGwbBszOtfQ==} + '@csstools/postcss-is-pseudo-class@5.0.3': + resolution: {integrity: sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-light-dark-function@2.0.7': - resolution: {integrity: sha512-ZZ0rwlanYKOHekyIPaU+sVm3BEHCe+Ha0/px+bmHe62n0Uc1lL34vbwrLYn6ote8PHlsqzKeTQdIejQCJ05tfw==} + '@csstools/postcss-light-dark-function@2.0.11': + resolution: {integrity: sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -2148,20 +2172,20 @@ packages: peerDependencies: postcss: ^8.4 - '@csstools/postcss-logical-viewport-units@3.0.3': - resolution: {integrity: sha512-OC1IlG/yoGJdi0Y+7duz/kU/beCwO+Gua01sD6GtOtLi7ByQUpcIqs7UE/xuRPay4cHgOMatWdnDdsIDjnWpPw==} + '@csstools/postcss-logical-viewport-units@3.0.4': + resolution: {integrity: sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-media-minmax@2.0.7': - resolution: {integrity: sha512-LB6tIP7iBZb5CYv8iRenfBZmbaG3DWNEziOnPjGoQX5P94FBPvvTBy68b/d9NnS5PELKwFmmOYsAEIgEhDPCHA==} + '@csstools/postcss-media-minmax@2.0.9': + resolution: {integrity: sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.4': - resolution: {integrity: sha512-AnGjVslHMm5xw9keusQYvjVWvuS7KWK+OJagaG0+m9QnIjZsrysD2kJP/tr/UJIyYtMCtu8OkUd+Rajb4DqtIQ==} + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5': + resolution: {integrity: sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -2172,32 +2196,44 @@ packages: peerDependencies: postcss: ^8.4 - '@csstools/postcss-normalize-display-values@4.0.0': - resolution: {integrity: sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==} + '@csstools/postcss-normalize-display-values@4.0.1': + resolution: {integrity: sha512-TQUGBuRvxdc7TgNSTevYqrL8oItxiwPDixk20qCB5me/W8uF7BPbhRrAvFuhEoywQp/woRsUZ6SJ+sU5idZAIA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-oklab-function@4.0.8': - resolution: {integrity: sha512-+5aPsNWgxohXoYNS1f+Ys0x3Qnfehgygv3qrPyv+Y25G0yX54/WlVB+IXprqBLOXHM1gsVF+QQSjlArhygna0Q==} + '@csstools/postcss-oklab-function@4.0.12': + resolution: {integrity: sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-progressive-custom-properties@4.0.0': - resolution: {integrity: sha512-XQPtROaQjomnvLUSy/bALTR5VCtTVUFwYs1SblvYgLSeTo2a/bMNwUwo2piXw5rTv/FEYiy5yPSXBqg9OKUx7Q==} + '@csstools/postcss-position-area-property@1.0.0': + resolution: {integrity: sha512-fUP6KR8qV2NuUZV3Cw8itx0Ep90aRjAZxAEzC3vrl6yjFv+pFsQbR18UuQctEKmA72K9O27CoYiKEgXxkqjg8Q==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-random-function@1.0.3': - resolution: {integrity: sha512-dbNeEEPHxAwfQJ3duRL5IPpuD77QAHtRl4bAHRs0vOVhVbHrsL7mHnwe0irYjbs9kYwhAHZBQTLBgmvufPuRkA==} + '@csstools/postcss-progressive-custom-properties@4.2.1': + resolution: {integrity: sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-relative-color-syntax@3.0.8': - resolution: {integrity: sha512-eGE31oLnJDoUysDdjS9MLxNZdtqqSxjDXMdISpLh80QMaYrKs7VINpid34tWQ+iU23Wg5x76qAzf1Q/SLLbZVg==} + '@csstools/postcss-property-rule-prelude-list@1.0.0': + resolution: {integrity: sha512-IxuQjUXq19fobgmSSvUDO7fVwijDJaZMvWQugxfEUxmjBeDCVaDuMpsZ31MsTm5xbnhA+ElDi0+rQ7sQQGisFA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-random-function@2.0.1': + resolution: {integrity: sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-relative-color-syntax@3.0.12': + resolution: {integrity: sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -2208,26 +2244,38 @@ packages: peerDependencies: postcss: ^8.4 - '@csstools/postcss-sign-functions@1.1.2': - resolution: {integrity: sha512-4EcAvXTUPh7n6UoZZkCzgtCf/wPzMlTNuddcKg7HG8ozfQkUcHsJ2faQKeLmjyKdYPyOUn4YA7yDPf8K/jfIxw==} + '@csstools/postcss-sign-functions@1.1.4': + resolution: {integrity: sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-stepped-value-functions@4.0.9': + resolution: {integrity: sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-stepped-value-functions@4.0.7': - resolution: {integrity: sha512-rdrRCKRnWtj5FyRin0u/gLla7CIvZRw/zMGI1fVJP0Sg/m1WGicjPVHRANL++3HQtsiXKAbPrcPr+VkyGck0IA==} + '@csstools/postcss-syntax-descriptor-syntax-production@1.0.1': + resolution: {integrity: sha512-GneqQWefjM//f4hJ/Kbox0C6f2T7+pi4/fqTqOFGTL3EjnvOReTqO1qUQ30CaUjkwjYq9qZ41hzarrAxCc4gow==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-text-decoration-shorthand@4.0.2': - resolution: {integrity: sha512-8XvCRrFNseBSAGxeaVTaNijAu+FzUvjwFXtcrynmazGb/9WUdsPCpBX+mHEHShVRq47Gy4peYAoxYs8ltUnmzA==} + '@csstools/postcss-system-ui-font-family@1.0.0': + resolution: {integrity: sha512-s3xdBvfWYfoPSBsikDXbuorcMG1nN1M6GdU0qBsGfcmNR0A/qhloQZpTxjA3Xsyrk1VJvwb2pOfiOT3at/DuIQ==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-trigonometric-functions@4.0.7': - resolution: {integrity: sha512-qTrZgLju3AV7Djhzuh2Bq/wjFqbcypnk0FhHjxW8DWJQcZLS1HecIus4X2/RLch1ukX7b+YYCdqbEnpIQO5ccg==} + '@csstools/postcss-text-decoration-shorthand@4.0.3': + resolution: {integrity: sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-trigonometric-functions@4.0.9': + resolution: {integrity: sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -2238,8 +2286,8 @@ packages: peerDependencies: postcss: ^8.4 - '@csstools/selector-resolve-nested@3.0.0': - resolution: {integrity: sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ==} + '@csstools/selector-resolve-nested@3.1.0': + resolution: {integrity: sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==} engines: {node: '>=18'} peerDependencies: postcss-selector-parser: ^7.0.0 @@ -4165,8 +4213,8 @@ packages: engines: {node: '>= 4.5.0'} hasBin: true - autoprefixer@10.4.20: - resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -4288,6 +4336,11 @@ packages: resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} engines: {node: '>=0.10.0'} + baseline-browser-mapping@2.10.31: + resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} + engines: {node: '>=6.0.0'} + hasBin: true + basic-ftp@5.3.1: resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==} engines: {node: '>=10.0.0'} @@ -4343,6 +4396,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@1.20.5: + resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + bonjour-service@1.3.0: resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==} @@ -4416,6 +4473,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bs-logger@0.2.6: resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} engines: {node: '>= 6'} @@ -4546,6 +4608,9 @@ packages: caniuse-lite@1.0.30001701: resolution: {integrity: sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + capture-exit@2.0.0: resolution: {integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==} engines: {node: 6.* || 8.* || >= 10.*} @@ -4870,6 +4935,10 @@ packages: resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} engines: {node: '>= 0.8.0'} + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + compute-scroll-into-view@3.1.0: resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} @@ -5099,8 +5168,8 @@ packages: peerDependencies: postcss: ^8.0.9 - css-has-pseudo@7.0.2: - resolution: {integrity: sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ==} + css-has-pseudo@7.0.3: + resolution: {integrity: sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -5169,8 +5238,8 @@ packages: css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - cssdb@8.2.3: - resolution: {integrity: sha512-9BDG5XmJrJQQnJ51VFxXCAtpZ5ebDlAREmO8sxMOVU0aSxN/gocbctjIG5LMh3WBUq+xTlb/jw2LoljBEqraTA==} + cssdb@8.9.0: + resolution: {integrity: sha512-J8jOU/hLjaXcO1LldOLraJSQpfLXRKof0I7mtbRyOy2AAXgqst0x9rlgi2qXeD6d0ou3ZLqcPAMqYVbpCbrxEw==} cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} @@ -5624,6 +5693,9 @@ packages: electron-to-chromium@1.5.27: resolution: {integrity: sha512-o37j1vZqCoEgBuWWXLHQgTN/KDKe7zwpiY5CPeq2RvUqOyJw9xnrULzZAEVQ5p4h+zjMk7hgtOoPdnLxr7m/jw==} + electron-to-chromium@1.5.359: + resolution: {integrity: sha512-8lPELWuYZIWk7NDvCNthtmMw/7Q5Wu25NpM4djFMHBmk8DubPAtL4YTOp7ou0e7HyJtwkVlWv8XMLURnrtgJQw==} + elliptic@6.6.1: resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} @@ -6107,6 +6179,10 @@ packages: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + express@4.22.2: + resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} + engines: {node: '>= 0.10.0'} + ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} @@ -6393,8 +6469,8 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} fragment-cache@0.2.1: resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} @@ -6855,6 +6931,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-parser-js@0.5.9: resolution: {integrity: sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==} @@ -6862,8 +6942,8 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} - http-proxy-middleware@2.0.7: - resolution: {integrity: sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==} + http-proxy-middleware@2.0.9: + resolution: {integrity: sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==} engines: {node: '>=12.0.0'} peerDependencies: '@types/express': ^4.17.13 @@ -8414,6 +8494,9 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -8495,6 +8578,11 @@ packages: nan@2.20.0: resolution: {integrity: sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanoid@3.3.4: resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -8529,6 +8617,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -8691,6 +8783,9 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.44: + resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} + node-stream-zip@1.15.0: resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==} engines: {node: '>=0.12.0'} @@ -8713,10 +8808,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - normalize-url@8.0.1: resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} engines: {node: '>=14.16'} @@ -8860,6 +8951,10 @@ packages: resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} engines: {node: '>= 0.8'} + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -9278,8 +9373,8 @@ packages: peerDependencies: postcss: ^8.4.6 - postcss-color-functional-notation@7.0.8: - resolution: {integrity: sha512-S/TpMKVKofNvsxfau/+bw+IA6cSfB6/kmzFj5szUofHOVnFFMB2WwK+Zu07BeMD8T0n+ZnTO5uXiMvAKe2dPkA==} + postcss-color-functional-notation@7.0.12: + resolution: {integrity: sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -9308,20 +9403,20 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss-custom-media@11.0.5: - resolution: {integrity: sha512-SQHhayVNgDvSAdX9NQ/ygcDQGEY+aSF4b/96z7QUX6mqL5yl/JgG/DywcF6fW9XbnCRE+aVYk+9/nqGuzOPWeQ==} + postcss-custom-media@11.0.6: + resolution: {integrity: sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - postcss-custom-properties@14.0.4: - resolution: {integrity: sha512-QnW8FCCK6q+4ierwjnmXF9Y9KF8q0JkbgVfvQEMa93x1GT8FvOiUevWCN2YLaOWyByeDX8S6VFbZEeWoAoXs2A==} + postcss-custom-properties@14.0.6: + resolution: {integrity: sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - postcss-custom-selectors@8.0.4: - resolution: {integrity: sha512-ASOXqNvDCE0dAJ/5qixxPeL1aOVGHGW2JwSy7HyjWNbnWTQCl+fDc968HY1jCmZI0+BaYT5CxsOiUhavpG/7eg==} + postcss-custom-selectors@8.0.5: + resolution: {integrity: sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -9362,8 +9457,8 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss-double-position-gradients@6.0.0: - resolution: {integrity: sha512-JkIGah3RVbdSEIrcobqj4Gzq0h53GG4uqDPsho88SgY84WnpkTpI0k50MFK/sX7XqVisZ6OqUfFnoUO6m1WWdg==} + postcss-double-position-gradients@6.0.4: + resolution: {integrity: sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -9397,8 +9492,8 @@ packages: peerDependencies: postcss: ^8.4 - postcss-lab-function@7.0.8: - resolution: {integrity: sha512-plV21I86Hg9q8omNz13G9fhPtLopIWH06bt/Cb5cs1XnaGU2kUtEitvVd4vtQb/VqCdNUHK5swKn3QFmMRbpDg==} + postcss-lab-function@7.0.12: + resolution: {integrity: sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -9482,8 +9577,8 @@ packages: peerDependencies: postcss: ^8.1.0 - postcss-nesting@13.0.1: - resolution: {integrity: sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ==} + postcss-nesting@13.0.2: + resolution: {integrity: sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -9571,8 +9666,8 @@ packages: peerDependencies: postcss: ^8.4 - postcss-preset-env@10.1.5: - resolution: {integrity: sha512-LQybafF/K7H+6fAs4SIkgzkSCixJy0/h0gubDIAP3Ihz+IQBRwsjyvBnAZ3JUHD+A/ITaxVRPDxn//a3Qy4pDw==} + postcss-preset-env@10.6.1: + resolution: {integrity: sha512-yrk74d9EvY+W7+lO9Aj1QmjWY9q5NsKjK2V9drkOPZB/X6KZ0B3igKsHUYakb7oYVhnioWypQX3xGuePf89f3g==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -9651,6 +9746,10 @@ packages: resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.5.3: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} @@ -9813,6 +9912,10 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + qs@6.5.3: resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} engines: {node: '>=0.6'} @@ -9860,6 +9963,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + rc-animate@2.11.1: resolution: {integrity: sha512-1NyuCGFJG/0Y+9RKh5y/i/AalUCA51opyyS/jO2seELpgymZm2u9QV3xwODwEuzkmeQ1BDPxMLmYLcTJedPlkQ==} peerDependencies: @@ -10208,8 +10315,8 @@ packages: react-lifecycles-compat@3.0.4: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} - react-loadable-ssr-addon-v5-slorber@1.0.1: - resolution: {integrity: sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==} + react-loadable-ssr-addon-v5-slorber@1.0.3: + resolution: {integrity: sha512-GXfh9VLwB5ERaCsU6RULh7tkemeX15aNh6wuMEBtfdyMa7fFG8TXrhXlx1SoEK2Ty/l6XIkzzYIQmyaWW3JgdQ==} engines: {node: '>=10.13.0'} peerDependencies: react-loadable: '*' @@ -10837,8 +10944,8 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - serve-handler@6.1.6: - resolution: {integrity: sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==} + serve-handler@6.1.7: + resolution: {integrity: sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==} serve-index@1.9.1: resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} @@ -11175,6 +11282,10 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.8.1: resolution: {integrity: sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==} @@ -11698,6 +11809,9 @@ packages: tslib@2.7.0: resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tslint@5.20.1: resolution: {integrity: sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==} engines: {node: '>=4.8.0'} @@ -11982,6 +12096,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + update-notifier@6.0.2: resolution: {integrity: sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==} engines: {node: '>=14.16'} @@ -14556,247 +14676,304 @@ snapshots: '@corex/deepmerge@2.6.148': {} - '@csstools/cascade-layer-name-parser@2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + '@csstools/cascade-layer-name-parser@2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 - '@csstools/color-helpers@5.0.2': {} + '@csstools/color-helpers@5.1.0': {} - '@csstools/css-calc@2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-color-parser@3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/color-helpers': 5.0.2 - '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} - '@csstools/css-tokenizer@3.0.3': {} + '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 - '@csstools/media-query-list-parser@4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + '@csstools/postcss-alpha-function@1.0.1(postcss@8.5.14)': dependencies: - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 - '@csstools/postcss-cascade-layers@5.0.1(postcss@8.5.3)': + '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.14)': dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 7.1.0 - '@csstools/postcss-color-function@4.0.8(postcss@8.5.3)': + '@csstools/postcss-color-function-display-p3-linear@1.0.1(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.5.3) - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 - '@csstools/postcss-color-mix-function@3.0.8(postcss@8.5.3)': + '@csstools/postcss-color-function@4.0.12(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.5.3) - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 - '@csstools/postcss-content-alt-text@2.0.4(postcss@8.5.3)': + '@csstools/postcss-color-mix-function@3.0.12(postcss@8.5.14)': dependencies: - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.5.3) - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 - '@csstools/postcss-exponential-functions@2.0.7(postcss@8.5.3)': + '@csstools/postcss-color-mix-variadic-function-arguments@1.0.2(postcss@8.5.14)': dependencies: - '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - postcss: 8.5.3 + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 - '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.3)': + '@csstools/postcss-content-alt-text@2.0.8(postcss@8.5.14)': dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 + + '@csstools/postcss-contrast-color-function@2.0.12(postcss@8.5.14)': + dependencies: + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 + + '@csstools/postcss-exponential-functions@2.0.9(postcss@8.5.14)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.14 + + '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.14)': + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - '@csstools/postcss-gamut-mapping@2.0.8(postcss@8.5.3)': + '@csstools/postcss-gamut-mapping@2.0.11(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - postcss: 8.5.3 + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.14 - '@csstools/postcss-gradients-interpolation-method@5.0.8(postcss@8.5.3)': + '@csstools/postcss-gradients-interpolation-method@5.0.12(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.5.3) - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 - '@csstools/postcss-hwb-function@4.0.8(postcss@8.5.3)': + '@csstools/postcss-hwb-function@4.0.12(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.5.3) - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 - '@csstools/postcss-ic-unit@4.0.0(postcss@8.5.3)': + '@csstools/postcss-ic-unit@4.0.4(postcss@8.5.14)': dependencies: - '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.5.3) - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - '@csstools/postcss-initial@2.0.1(postcss@8.5.3)': + '@csstools/postcss-initial@2.0.1(postcss@8.5.14)': dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - '@csstools/postcss-is-pseudo-class@5.0.1(postcss@8.5.3)': + '@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.14)': dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 7.1.0 - '@csstools/postcss-light-dark-function@2.0.7(postcss@8.5.3)': + '@csstools/postcss-light-dark-function@2.0.11(postcss@8.5.14)': dependencies: - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.5.3) - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 - '@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.3)': + '@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.14)': dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.3)': + '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.14)': dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.3)': + '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.14)': dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.3)': + '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.14)': dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - '@csstools/postcss-logical-viewport-units@3.0.3(postcss@8.5.3)': + '@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.14)': dependencies: - '@csstools/css-tokenizer': 3.0.3 - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/css-tokenizer': 3.0.4 + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 - '@csstools/postcss-media-minmax@2.0.7(postcss@8.5.3)': + '@csstools/postcss-media-minmax@2.0.9(postcss@8.5.14)': dependencies: - '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - '@csstools/media-query-list-parser': 4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - postcss: 8.5.3 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + postcss: 8.5.14 - '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.4(postcss@8.5.3)': + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.14)': dependencies: - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - '@csstools/media-query-list-parser': 4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - postcss: 8.5.3 + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + postcss: 8.5.14 - '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.3)': + '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.14)': dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - '@csstools/postcss-normalize-display-values@4.0.0(postcss@8.5.3)': + '@csstools/postcss-normalize-display-values@4.0.1(postcss@8.5.14)': dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - '@csstools/postcss-oklab-function@4.0.8(postcss@8.5.3)': + '@csstools/postcss-oklab-function@4.0.12(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.5.3) - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 - '@csstools/postcss-progressive-custom-properties@4.0.0(postcss@8.5.3)': + '@csstools/postcss-position-area-property@1.0.0(postcss@8.5.14)': dependencies: - postcss: 8.5.3 + postcss: 8.5.14 + + '@csstools/postcss-progressive-custom-properties@4.2.1(postcss@8.5.14)': + dependencies: + postcss: 8.5.14 postcss-value-parser: 4.2.0 - '@csstools/postcss-random-function@1.0.3(postcss@8.5.3)': + '@csstools/postcss-property-rule-prelude-list@1.0.0(postcss@8.5.14)': dependencies: - '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - postcss: 8.5.3 + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.14 - '@csstools/postcss-relative-color-syntax@3.0.8(postcss@8.5.3)': + '@csstools/postcss-random-function@2.0.1(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.5.3) - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.14 - '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.3)': + '@csstools/postcss-relative-color-syntax@3.0.12(postcss@8.5.14)': dependencies: - postcss: 8.5.3 + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 + + '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.14)': + dependencies: + postcss: 8.5.14 postcss-selector-parser: 7.1.0 - '@csstools/postcss-sign-functions@1.1.2(postcss@8.5.3)': + '@csstools/postcss-sign-functions@1.1.4(postcss@8.5.14)': dependencies: - '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - postcss: 8.5.3 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.14 - '@csstools/postcss-stepped-value-functions@4.0.7(postcss@8.5.3)': + '@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.14)': dependencies: - '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - postcss: 8.5.3 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.14 - '@csstools/postcss-text-decoration-shorthand@4.0.2(postcss@8.5.3)': + '@csstools/postcss-syntax-descriptor-syntax-production@1.0.1(postcss@8.5.14)': dependencies: - '@csstools/color-helpers': 5.0.2 - postcss: 8.5.3 + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.14 + + '@csstools/postcss-system-ui-font-family@1.0.0(postcss@8.5.14)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.14 + + '@csstools/postcss-text-decoration-shorthand@4.0.3(postcss@8.5.14)': + dependencies: + '@csstools/color-helpers': 5.1.0 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - '@csstools/postcss-trigonometric-functions@4.0.7(postcss@8.5.3)': + '@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.14)': dependencies: - '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - postcss: 8.5.3 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.14 - '@csstools/postcss-unset-value@4.0.0(postcss@8.5.3)': + '@csstools/postcss-unset-value@4.0.0(postcss@8.5.14)': dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - '@csstools/selector-resolve-nested@3.0.0(postcss-selector-parser@7.1.0)': + '@csstools/selector-resolve-nested@3.1.0(postcss-selector-parser@7.1.0)': dependencies: postcss-selector-parser: 7.1.0 @@ -14804,9 +14981,9 @@ snapshots: dependencies: postcss-selector-parser: 7.1.0 - '@csstools/utilities@2.0.0(postcss@8.5.3)': + '@csstools/utilities@2.0.0(postcss@8.5.14)': dependencies: - postcss: 8.5.3 + postcss: 8.5.14 '@ctrl/tinycolor@3.6.1': {} @@ -14844,7 +15021,7 @@ snapshots: '@docusaurus/utils': 3.7.0(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) babel-plugin-dynamic-import-node: 2.3.3 fs-extra: 11.3.0 - tslib: 2.7.0 + tslib: 2.8.1 transitivePeerDependencies: - '@swc/core' - acorn @@ -14868,17 +15045,17 @@ snapshots: copy-webpack-plugin: 11.0.0(webpack@5.98.0) css-loader: 6.11.0(webpack@5.98.0) css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.98.0) - cssnano: 6.1.2(postcss@8.5.3) + cssnano: 6.1.2(postcss@8.5.14) file-loader: 6.2.0(webpack@5.98.0) html-minifier-terser: 7.2.0 mini-css-extract-plugin: 2.9.2(webpack@5.98.0) null-loader: 4.0.1(webpack@5.98.0) - postcss: 8.5.3 - postcss-loader: 7.3.4(postcss@8.5.3)(typescript@5.6.3)(webpack@5.98.0) - postcss-preset-env: 10.1.5(postcss@8.5.3) + postcss: 8.5.14 + postcss-loader: 7.3.4(postcss@8.5.14)(typescript@5.6.3)(webpack@5.98.0) + postcss-preset-env: 10.6.1(postcss@8.5.14) react-dev-utils: 12.0.1(eslint@9.36.0(jiti@1.21.7))(typescript@5.6.3)(webpack@5.98.0) - terser-webpack-plugin: 5.3.10(webpack@5.98.0) - tslib: 2.7.0 + terser-webpack-plugin: 5.3.12(webpack@5.98.0) + tslib: 2.8.1 url-loader: 4.1.1(file-loader@6.2.0(webpack@5.98.0))(webpack@5.98.0) webpack: 5.98.0 webpackbar: 6.0.1(webpack@5.98.0) @@ -14934,14 +15111,14 @@ snapshots: react-dom: 19.0.0(react@19.0.0) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)' react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.0.0)' - react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@19.0.0))(webpack@5.98.0) + react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.0.0))(webpack@5.98.0) react-router: 5.3.4(react@19.0.0) react-router-config: 5.1.1(react-router@5.3.4(react@19.0.0))(react@19.0.0) react-router-dom: 5.3.4(react@19.0.0) semver: 7.6.3 - serve-handler: 6.1.6 + serve-handler: 6.1.7 shelljs: 0.8.5 - tslib: 2.7.0 + tslib: 2.8.1 update-notifier: 6.0.2 webpack: 5.98.0 webpack-bundle-analyzer: 4.10.2 @@ -14969,15 +15146,15 @@ snapshots: '@docusaurus/cssnano-preset@3.7.0': dependencies: - cssnano-preset-advanced: 6.1.2(postcss@8.5.3) - postcss: 8.5.3 - postcss-sort-media-queries: 5.2.0(postcss@8.5.3) - tslib: 2.7.0 + cssnano-preset-advanced: 6.1.2(postcss@8.5.14) + postcss: 8.5.14 + postcss-sort-media-queries: 5.2.0(postcss@8.5.14) + tslib: 2.8.1 '@docusaurus/logger@3.7.0': dependencies: chalk: 4.1.2 - tslib: 2.7.0 + tslib: 2.8.1 '@docusaurus/mdx-loader@3.7.0(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: @@ -15001,7 +15178,7 @@ snapshots: remark-frontmatter: 5.0.0 remark-gfm: 4.0.1 stringify-object: 3.3.0 - tslib: 2.7.0 + tslib: 2.8.1 unified: 11.0.5 unist-util-visit: 5.0.0 url-loader: 4.1.1(file-loader@6.2.0(webpack@5.98.0))(webpack@5.98.0) @@ -15537,7 +15714,7 @@ snapshots: '@docusaurus/utils-common@3.7.0(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@docusaurus/types': 3.7.0(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - tslib: 2.7.0 + tslib: 2.8.1 transitivePeerDependencies: - '@swc/core' - acorn @@ -15557,7 +15734,7 @@ snapshots: joi: 17.13.3 js-yaml: 4.1.0 lodash: 4.17.21 - tslib: 2.7.0 + tslib: 2.8.1 transitivePeerDependencies: - '@swc/core' - acorn @@ -15586,7 +15763,7 @@ snapshots: prompts: 2.4.2 resolve-pathname: 3.0.0 shelljs: 0.8.5 - tslib: 2.7.0 + tslib: 2.8.1 url-loader: 4.1.1(file-loader@6.2.0(webpack@5.98.0))(webpack@5.98.0) utility-types: 3.11.0 webpack: 5.98.0 @@ -15888,7 +16065,9 @@ snapshots: source-map: 0.6.1 string-length: 2.0.0 transitivePeerDependencies: + - bufferutil - supports-color + - utf-8-validate '@jest/schemas@29.6.3': dependencies: @@ -15913,7 +16092,9 @@ snapshots: jest-runner: 24.9.0 jest-runtime: 24.9.0 transitivePeerDependencies: + - bufferutil - supports-color + - utf-8-validate '@jest/transform@24.9.0': dependencies: @@ -16447,7 +16628,7 @@ snapshots: dependencies: '@react-native-community/cli-debugger-ui': 14.1.0 '@react-native-community/cli-tools': 14.1.0 - compression: 1.7.4 + compression: 1.8.1 connect: 3.7.0 errorhandler: 1.5.1 nocache: 3.0.4 @@ -18042,7 +18223,7 @@ snapshots: ast-types@0.15.2: dependencies: - tslib: 2.7.0 + tslib: 2.8.1 astral-regex@1.0.0: {} @@ -18068,14 +18249,13 @@ snapshots: atob@2.1.2: {} - autoprefixer@10.4.20(postcss@8.5.3): + autoprefixer@10.5.0(postcss@8.5.14): dependencies: - browserslist: 4.24.4 - caniuse-lite: 1.0.30001701 - fraction.js: 4.3.7 - normalize-range: 0.1.2 + browserslist: 4.28.2 + caniuse-lite: 1.0.30001793 + fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -18257,6 +18437,8 @@ snapshots: mixin-deep: 1.3.2 pascalcase: 0.1.1 + baseline-browser-mapping@2.10.31: {} + basic-ftp@5.3.1: {} batch@0.6.1: {} @@ -18326,6 +18508,23 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@1.20.5: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + bonjour-service@1.3.0: dependencies: fast-deep-equal: 3.1.3 @@ -18453,6 +18652,14 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.4) + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.31 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.359 + node-releases: 2.0.44 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + bs-logger@0.2.6: dependencies: fast-json-stable-stringify: 2.1.0 @@ -18584,7 +18791,7 @@ snapshots: camel-case@4.1.2: dependencies: pascal-case: 3.1.2 - tslib: 2.7.0 + tslib: 2.8.1 camelcase@4.1.0: {} @@ -18598,13 +18805,15 @@ snapshots: caniuse-api@3.0.0: dependencies: - browserslist: 4.23.3 - caniuse-lite: 1.0.30001701 + browserslist: 4.28.2 + caniuse-lite: 1.0.30001793 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 caniuse-lite@1.0.30001701: {} + caniuse-lite@1.0.30001793: {} + capture-exit@2.0.0: dependencies: rsvp: 4.8.5 @@ -18978,6 +19187,18 @@ snapshots: transitivePeerDependencies: - supports-color + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + compute-scroll-into-view@3.1.0: {} concat-map@0.0.1: {} @@ -19107,7 +19328,7 @@ snapshots: core-js-compat@3.41.0: dependencies: - browserslist: 4.24.4 + browserslist: 4.28.2 core-js-pure@3.41.0: {} @@ -19240,30 +19461,30 @@ snapshots: babel-runtime: 6.26.0 component-classes: 1.2.6 - css-blank-pseudo@7.0.1(postcss@8.5.3): + css-blank-pseudo@7.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 7.1.0 - css-declaration-sorter@7.2.0(postcss@8.5.3): + css-declaration-sorter@7.2.0(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - css-has-pseudo@7.0.2(postcss@8.5.3): + css-has-pseudo@7.0.3(postcss@8.5.14): dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 css-loader@6.11.0(webpack@5.98.0): dependencies: - icss-utils: 5.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.3) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.3) - postcss-modules-scope: 3.2.1(postcss@8.5.3) - postcss-modules-values: 4.0.0(postcss@8.5.3) + icss-utils: 5.1.0(postcss@8.5.14) + postcss: 8.5.14 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.14) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.14) + postcss-modules-scope: 3.2.1(postcss@8.5.14) + postcss-modules-values: 4.0.0(postcss@8.5.14) postcss-value-parser: 4.2.0 semver: 7.6.3 optionalDependencies: @@ -19272,18 +19493,18 @@ snapshots: css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.98.0): dependencies: '@jridgewell/trace-mapping': 0.3.25 - cssnano: 6.1.2(postcss@8.5.3) + cssnano: 6.1.2(postcss@8.5.14) jest-worker: 29.7.0 - postcss: 8.5.3 + postcss: 8.5.14 schema-utils: 4.3.0 serialize-javascript: 6.0.2 webpack: 5.98.0 optionalDependencies: clean-css: 5.3.3 - css-prefers-color-scheme@10.0.0(postcss@8.5.3): + css-prefers-color-scheme@10.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 css-select@4.3.0: dependencies: @@ -19315,64 +19536,64 @@ snapshots: css.escape@1.5.1: {} - cssdb@8.2.3: {} + cssdb@8.9.0: {} cssesc@3.0.0: {} - cssnano-preset-advanced@6.1.2(postcss@8.5.3): - dependencies: - autoprefixer: 10.4.20(postcss@8.5.3) - browserslist: 4.23.3 - cssnano-preset-default: 6.1.2(postcss@8.5.3) - postcss: 8.5.3 - postcss-discard-unused: 6.0.5(postcss@8.5.3) - postcss-merge-idents: 6.0.3(postcss@8.5.3) - postcss-reduce-idents: 6.0.3(postcss@8.5.3) - postcss-zindex: 6.0.2(postcss@8.5.3) - - cssnano-preset-default@6.1.2(postcss@8.5.3): - dependencies: - browserslist: 4.23.3 - css-declaration-sorter: 7.2.0(postcss@8.5.3) - cssnano-utils: 4.0.2(postcss@8.5.3) - postcss: 8.5.3 - postcss-calc: 9.0.1(postcss@8.5.3) - postcss-colormin: 6.1.0(postcss@8.5.3) - postcss-convert-values: 6.1.0(postcss@8.5.3) - postcss-discard-comments: 6.0.2(postcss@8.5.3) - postcss-discard-duplicates: 6.0.3(postcss@8.5.3) - postcss-discard-empty: 6.0.3(postcss@8.5.3) - postcss-discard-overridden: 6.0.2(postcss@8.5.3) - postcss-merge-longhand: 6.0.5(postcss@8.5.3) - postcss-merge-rules: 6.1.1(postcss@8.5.3) - postcss-minify-font-values: 6.1.0(postcss@8.5.3) - postcss-minify-gradients: 6.0.3(postcss@8.5.3) - postcss-minify-params: 6.1.0(postcss@8.5.3) - postcss-minify-selectors: 6.0.4(postcss@8.5.3) - postcss-normalize-charset: 6.0.2(postcss@8.5.3) - postcss-normalize-display-values: 6.0.2(postcss@8.5.3) - postcss-normalize-positions: 6.0.2(postcss@8.5.3) - postcss-normalize-repeat-style: 6.0.2(postcss@8.5.3) - postcss-normalize-string: 6.0.2(postcss@8.5.3) - postcss-normalize-timing-functions: 6.0.2(postcss@8.5.3) - postcss-normalize-unicode: 6.1.0(postcss@8.5.3) - postcss-normalize-url: 6.0.2(postcss@8.5.3) - postcss-normalize-whitespace: 6.0.2(postcss@8.5.3) - postcss-ordered-values: 6.0.2(postcss@8.5.3) - postcss-reduce-initial: 6.1.0(postcss@8.5.3) - postcss-reduce-transforms: 6.0.2(postcss@8.5.3) - postcss-svgo: 6.0.3(postcss@8.5.3) - postcss-unique-selectors: 6.0.4(postcss@8.5.3) - - cssnano-utils@4.0.2(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - cssnano@6.1.2(postcss@8.5.3): - dependencies: - cssnano-preset-default: 6.1.2(postcss@8.5.3) + cssnano-preset-advanced@6.1.2(postcss@8.5.14): + dependencies: + autoprefixer: 10.5.0(postcss@8.5.14) + browserslist: 4.28.2 + cssnano-preset-default: 6.1.2(postcss@8.5.14) + postcss: 8.5.14 + postcss-discard-unused: 6.0.5(postcss@8.5.14) + postcss-merge-idents: 6.0.3(postcss@8.5.14) + postcss-reduce-idents: 6.0.3(postcss@8.5.14) + postcss-zindex: 6.0.2(postcss@8.5.14) + + cssnano-preset-default@6.1.2(postcss@8.5.14): + dependencies: + browserslist: 4.28.2 + css-declaration-sorter: 7.2.0(postcss@8.5.14) + cssnano-utils: 4.0.2(postcss@8.5.14) + postcss: 8.5.14 + postcss-calc: 9.0.1(postcss@8.5.14) + postcss-colormin: 6.1.0(postcss@8.5.14) + postcss-convert-values: 6.1.0(postcss@8.5.14) + postcss-discard-comments: 6.0.2(postcss@8.5.14) + postcss-discard-duplicates: 6.0.3(postcss@8.5.14) + postcss-discard-empty: 6.0.3(postcss@8.5.14) + postcss-discard-overridden: 6.0.2(postcss@8.5.14) + postcss-merge-longhand: 6.0.5(postcss@8.5.14) + postcss-merge-rules: 6.1.1(postcss@8.5.14) + postcss-minify-font-values: 6.1.0(postcss@8.5.14) + postcss-minify-gradients: 6.0.3(postcss@8.5.14) + postcss-minify-params: 6.1.0(postcss@8.5.14) + postcss-minify-selectors: 6.0.4(postcss@8.5.14) + postcss-normalize-charset: 6.0.2(postcss@8.5.14) + postcss-normalize-display-values: 6.0.2(postcss@8.5.14) + postcss-normalize-positions: 6.0.2(postcss@8.5.14) + postcss-normalize-repeat-style: 6.0.2(postcss@8.5.14) + postcss-normalize-string: 6.0.2(postcss@8.5.14) + postcss-normalize-timing-functions: 6.0.2(postcss@8.5.14) + postcss-normalize-unicode: 6.1.0(postcss@8.5.14) + postcss-normalize-url: 6.0.2(postcss@8.5.14) + postcss-normalize-whitespace: 6.0.2(postcss@8.5.14) + postcss-ordered-values: 6.0.2(postcss@8.5.14) + postcss-reduce-initial: 6.1.0(postcss@8.5.14) + postcss-reduce-transforms: 6.0.2(postcss@8.5.14) + postcss-svgo: 6.0.3(postcss@8.5.14) + postcss-unique-selectors: 6.0.4(postcss@8.5.14) + + cssnano-utils@4.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + cssnano@6.1.2(postcss@8.5.14): + dependencies: + cssnano-preset-default: 6.1.2(postcss@8.5.14) lilconfig: 3.1.3 - postcss: 8.5.3 + postcss: 8.5.14 csso@5.0.5: dependencies: @@ -19796,6 +20017,8 @@ snapshots: electron-to-chromium@1.5.27: {} + electron-to-chromium@1.5.359: {} + elliptic@6.6.1: dependencies: bn.js: 4.12.3 @@ -20646,6 +20869,42 @@ snapshots: transitivePeerDependencies: - supports-color + express@4.22.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.5 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + ext@1.7.0: dependencies: type: 2.7.3 @@ -20934,7 +21193,7 @@ snapshots: fs-extra: 9.1.0 glob: 7.2.3 memfs: 3.5.3 - minimatch: 3.1.2 + minimatch: 3.1.5 schema-utils: 2.7.0 semver: 7.6.3 tapable: 1.1.3 @@ -20976,7 +21235,7 @@ snapshots: forwarded@0.2.0: {} - fraction.js@4.3.7: {} + fraction.js@5.3.4: {} fragment-cache@0.2.1: dependencies: @@ -21531,7 +21790,7 @@ snapshots: html-minifier-terser: 6.1.0 lodash: 4.17.21 pretty-error: 4.0.0 - tapable: 2.2.1 + tapable: 2.3.3 optionalDependencies: webpack: 5.98.0 @@ -21584,6 +21843,14 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-parser-js@0.5.9: {} http-proxy-agent@7.0.2: @@ -21593,7 +21860,7 @@ snapshots: transitivePeerDependencies: - supports-color - http-proxy-middleware@2.0.7(@types/express@4.17.18): + http-proxy-middleware@2.0.9(@types/express@4.17.18): dependencies: '@types/http-proxy': 1.17.16 http-proxy: 1.18.1 @@ -21655,9 +21922,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.3): + icss-utils@5.1.0(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 idb@7.1.1: {} @@ -23825,7 +24092,7 @@ snapshots: mini-css-extract-plugin@2.9.2(webpack@5.98.0): dependencies: schema-utils: 4.3.0 - tapable: 2.2.1 + tapable: 2.3.3 webpack: 5.98.0 minimalistic-assert@1.0.1: {} @@ -23836,6 +24103,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.11 + minimatch@5.1.6: dependencies: brace-expansion: 2.0.1 @@ -23946,6 +24217,8 @@ snapshots: nan@2.20.0: optional: true + nanoid@3.3.12: {} + nanoid@3.3.4: {} nanoid@3.3.8: {} @@ -23986,6 +24259,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@0.6.4: {} + neo-async@2.6.2: {} netmask@2.1.1: {} @@ -24099,7 +24374,7 @@ snapshots: node-dir@0.1.17: dependencies: - minimatch: 3.1.2 + minimatch: 3.1.5 node-domexception@1.0.0: {} @@ -24189,6 +24464,8 @@ snapshots: node-releases@2.0.19: {} + node-releases@2.0.44: {} + node-stream-zip@1.15.0: {} nodemailer@6.10.1: {} @@ -24211,8 +24488,6 @@ snapshots: normalize-path@3.0.0: {} - normalize-range@0.1.2: {} - normalize-url@8.0.1: {} npm-run-path@2.0.2: @@ -24372,6 +24647,8 @@ snapshots: on-headers@1.0.2: {} + on-headers@1.1.0: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -24567,7 +24844,7 @@ snapshots: param-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.7.0 + tslib: 2.8.1 parent-module@1.0.1: dependencies: @@ -24631,7 +24908,7 @@ snapshots: pascal-case@3.1.2: dependencies: no-case: 3.0.4 - tslib: 2.7.0 + tslib: 2.8.1 pascalcase@0.1.1: {} @@ -24832,399 +25109,407 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-attribute-case-insensitive@7.0.1(postcss@8.5.3): + postcss-attribute-case-insensitive@7.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 7.1.0 - postcss-calc@9.0.1(postcss@8.5.3): + postcss-calc@9.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 - postcss-clamp@4.1.0(postcss@8.5.3): + postcss-clamp@4.1.0(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-color-functional-notation@7.0.8(postcss@8.5.3): + postcss-color-functional-notation@7.0.12(postcss@8.5.14): dependencies: - '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.5.3) - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 - postcss-color-hex-alpha@10.0.0(postcss@8.5.3): + postcss-color-hex-alpha@10.0.0(postcss@8.5.14): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-color-rebeccapurple@10.0.0(postcss@8.5.3): + postcss-color-rebeccapurple@10.0.0(postcss@8.5.14): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-colormin@6.1.0(postcss@8.5.3): + postcss-colormin@6.1.0(postcss@8.5.14): dependencies: - browserslist: 4.23.3 + browserslist: 4.28.2 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-convert-values@6.1.0(postcss@8.5.3): + postcss-convert-values@6.1.0(postcss@8.5.14): dependencies: - browserslist: 4.23.3 - postcss: 8.5.3 + browserslist: 4.28.2 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-custom-media@11.0.5(postcss@8.5.3): + postcss-custom-media@11.0.6(postcss@8.5.14): dependencies: - '@csstools/cascade-layer-name-parser': 2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - '@csstools/media-query-list-parser': 4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - postcss: 8.5.3 + '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + postcss: 8.5.14 - postcss-custom-properties@14.0.4(postcss@8.5.3): + postcss-custom-properties@14.0.6(postcss@8.5.14): dependencies: - '@csstools/cascade-layer-name-parser': 2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-custom-selectors@8.0.4(postcss@8.5.3): + postcss-custom-selectors@8.0.5(postcss@8.5.14): dependencies: - '@csstools/cascade-layer-name-parser': 2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - postcss: 8.5.3 + '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.14 postcss-selector-parser: 7.1.0 - postcss-dir-pseudo-class@9.0.1(postcss@8.5.3): + postcss-dir-pseudo-class@9.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 7.1.0 - postcss-discard-comments@6.0.2(postcss@8.5.3): + postcss-discard-comments@6.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - postcss-discard-duplicates@6.0.3(postcss@8.5.3): + postcss-discard-duplicates@6.0.3(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - postcss-discard-empty@6.0.3(postcss@8.5.3): + postcss-discard-empty@6.0.3(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - postcss-discard-overridden@6.0.2(postcss@8.5.3): + postcss-discard-overridden@6.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - postcss-discard-unused@6.0.5(postcss@8.5.3): + postcss-discard-unused@6.0.5(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 6.1.2 - postcss-double-position-gradients@6.0.0(postcss@8.5.3): + postcss-double-position-gradients@6.0.4(postcss@8.5.14): dependencies: - '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.5.3) - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-focus-visible@10.0.1(postcss@8.5.3): + postcss-focus-visible@10.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 7.1.0 - postcss-focus-within@9.0.1(postcss@8.5.3): + postcss-focus-within@9.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 7.1.0 - postcss-font-variant@5.0.0(postcss@8.5.3): + postcss-font-variant@5.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - postcss-gap-properties@6.0.0(postcss@8.5.3): + postcss-gap-properties@6.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - postcss-image-set-function@7.0.0(postcss@8.5.3): + postcss-image-set-function@7.0.0(postcss@8.5.14): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-lab-function@7.0.8(postcss@8.5.3): + postcss-lab-function@7.0.12(postcss@8.5.14): dependencies: - '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.5.3) - '@csstools/utilities': 2.0.0(postcss@8.5.3) - postcss: 8.5.3 + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/utilities': 2.0.0(postcss@8.5.14) + postcss: 8.5.14 - postcss-loader@7.3.4(postcss@8.5.3)(typescript@5.6.3)(webpack@5.98.0): + postcss-loader@7.3.4(postcss@8.5.14)(typescript@5.6.3)(webpack@5.98.0): dependencies: cosmiconfig: 8.3.6(typescript@5.6.3) jiti: 1.21.7 - postcss: 8.5.3 + postcss: 8.5.14 semver: 7.6.3 webpack: 5.98.0 transitivePeerDependencies: - typescript - postcss-logical@8.1.0(postcss@8.5.3): + postcss-logical@8.1.0(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-merge-idents@6.0.3(postcss@8.5.3): + postcss-merge-idents@6.0.3(postcss@8.5.14): dependencies: - cssnano-utils: 4.0.2(postcss@8.5.3) - postcss: 8.5.3 + cssnano-utils: 4.0.2(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-merge-longhand@6.0.5(postcss@8.5.3): + postcss-merge-longhand@6.0.5(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - stylehacks: 6.1.1(postcss@8.5.3) + stylehacks: 6.1.1(postcss@8.5.14) - postcss-merge-rules@6.1.1(postcss@8.5.3): + postcss-merge-rules@6.1.1(postcss@8.5.14): dependencies: - browserslist: 4.23.3 + browserslist: 4.28.2 caniuse-api: 3.0.0 - cssnano-utils: 4.0.2(postcss@8.5.3) - postcss: 8.5.3 + cssnano-utils: 4.0.2(postcss@8.5.14) + postcss: 8.5.14 postcss-selector-parser: 6.1.2 - postcss-minify-font-values@6.1.0(postcss@8.5.3): + postcss-minify-font-values@6.1.0(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-minify-gradients@6.0.3(postcss@8.5.3): + postcss-minify-gradients@6.0.3(postcss@8.5.14): dependencies: colord: 2.9.3 - cssnano-utils: 4.0.2(postcss@8.5.3) - postcss: 8.5.3 + cssnano-utils: 4.0.2(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-minify-params@6.1.0(postcss@8.5.3): + postcss-minify-params@6.1.0(postcss@8.5.14): dependencies: - browserslist: 4.23.3 - cssnano-utils: 4.0.2(postcss@8.5.3) - postcss: 8.5.3 + browserslist: 4.28.2 + cssnano-utils: 4.0.2(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-minify-selectors@6.0.4(postcss@8.5.3): + postcss-minify-selectors@6.0.4(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 6.1.2 - postcss-modules-extract-imports@3.1.0(postcss@8.5.3): + postcss-modules-extract-imports@3.1.0(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - postcss-modules-local-by-default@4.2.0(postcss@8.5.3): + postcss-modules-local-by-default@4.2.0(postcss@8.5.14): dependencies: - icss-utils: 5.1.0(postcss@8.5.3) - postcss: 8.5.3 + icss-utils: 5.1.0(postcss@8.5.14) + postcss: 8.5.14 postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.3): + postcss-modules-scope@3.2.1(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 7.1.0 - postcss-modules-values@4.0.0(postcss@8.5.3): + postcss-modules-values@4.0.0(postcss@8.5.14): dependencies: - icss-utils: 5.1.0(postcss@8.5.3) - postcss: 8.5.3 + icss-utils: 5.1.0(postcss@8.5.14) + postcss: 8.5.14 - postcss-nesting@13.0.1(postcss@8.5.3): + postcss-nesting@13.0.2(postcss@8.5.14): dependencies: - '@csstools/selector-resolve-nested': 3.0.0(postcss-selector-parser@7.1.0) + '@csstools/selector-resolve-nested': 3.1.0(postcss-selector-parser@7.1.0) '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 7.1.0 - postcss-normalize-charset@6.0.2(postcss@8.5.3): + postcss-normalize-charset@6.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - postcss-normalize-display-values@6.0.2(postcss@8.5.3): + postcss-normalize-display-values@6.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-normalize-positions@6.0.2(postcss@8.5.3): + postcss-normalize-positions@6.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@6.0.2(postcss@8.5.3): + postcss-normalize-repeat-style@6.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-normalize-string@6.0.2(postcss@8.5.3): + postcss-normalize-string@6.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@6.0.2(postcss@8.5.3): + postcss-normalize-timing-functions@6.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@6.1.0(postcss@8.5.3): + postcss-normalize-unicode@6.1.0(postcss@8.5.14): dependencies: - browserslist: 4.23.3 - postcss: 8.5.3 + browserslist: 4.28.2 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-normalize-url@6.0.2(postcss@8.5.3): + postcss-normalize-url@6.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@6.0.2(postcss@8.5.3): + postcss-normalize-whitespace@6.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-opacity-percentage@3.0.0(postcss@8.5.3): + postcss-opacity-percentage@3.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - postcss-ordered-values@6.0.2(postcss@8.5.3): + postcss-ordered-values@6.0.2(postcss@8.5.14): dependencies: - cssnano-utils: 4.0.2(postcss@8.5.3) - postcss: 8.5.3 + cssnano-utils: 4.0.2(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-overflow-shorthand@6.0.0(postcss@8.5.3): + postcss-overflow-shorthand@6.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-page-break@3.0.4(postcss@8.5.3): + postcss-page-break@3.0.4(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - postcss-place@10.0.0(postcss@8.5.3): + postcss-place@10.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-preset-env@10.1.5(postcss@8.5.3): - dependencies: - '@csstools/postcss-cascade-layers': 5.0.1(postcss@8.5.3) - '@csstools/postcss-color-function': 4.0.8(postcss@8.5.3) - '@csstools/postcss-color-mix-function': 3.0.8(postcss@8.5.3) - '@csstools/postcss-content-alt-text': 2.0.4(postcss@8.5.3) - '@csstools/postcss-exponential-functions': 2.0.7(postcss@8.5.3) - '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.3) - '@csstools/postcss-gamut-mapping': 2.0.8(postcss@8.5.3) - '@csstools/postcss-gradients-interpolation-method': 5.0.8(postcss@8.5.3) - '@csstools/postcss-hwb-function': 4.0.8(postcss@8.5.3) - '@csstools/postcss-ic-unit': 4.0.0(postcss@8.5.3) - '@csstools/postcss-initial': 2.0.1(postcss@8.5.3) - '@csstools/postcss-is-pseudo-class': 5.0.1(postcss@8.5.3) - '@csstools/postcss-light-dark-function': 2.0.7(postcss@8.5.3) - '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.3) - '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.3) - '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.3) - '@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.3) - '@csstools/postcss-logical-viewport-units': 3.0.3(postcss@8.5.3) - '@csstools/postcss-media-minmax': 2.0.7(postcss@8.5.3) - '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.4(postcss@8.5.3) - '@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.3) - '@csstools/postcss-normalize-display-values': 4.0.0(postcss@8.5.3) - '@csstools/postcss-oklab-function': 4.0.8(postcss@8.5.3) - '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.5.3) - '@csstools/postcss-random-function': 1.0.3(postcss@8.5.3) - '@csstools/postcss-relative-color-syntax': 3.0.8(postcss@8.5.3) - '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.3) - '@csstools/postcss-sign-functions': 1.1.2(postcss@8.5.3) - '@csstools/postcss-stepped-value-functions': 4.0.7(postcss@8.5.3) - '@csstools/postcss-text-decoration-shorthand': 4.0.2(postcss@8.5.3) - '@csstools/postcss-trigonometric-functions': 4.0.7(postcss@8.5.3) - '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.3) - autoprefixer: 10.4.20(postcss@8.5.3) - browserslist: 4.24.4 - css-blank-pseudo: 7.0.1(postcss@8.5.3) - css-has-pseudo: 7.0.2(postcss@8.5.3) - css-prefers-color-scheme: 10.0.0(postcss@8.5.3) - cssdb: 8.2.3 - postcss: 8.5.3 - postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.3) - postcss-clamp: 4.1.0(postcss@8.5.3) - postcss-color-functional-notation: 7.0.8(postcss@8.5.3) - postcss-color-hex-alpha: 10.0.0(postcss@8.5.3) - postcss-color-rebeccapurple: 10.0.0(postcss@8.5.3) - postcss-custom-media: 11.0.5(postcss@8.5.3) - postcss-custom-properties: 14.0.4(postcss@8.5.3) - postcss-custom-selectors: 8.0.4(postcss@8.5.3) - postcss-dir-pseudo-class: 9.0.1(postcss@8.5.3) - postcss-double-position-gradients: 6.0.0(postcss@8.5.3) - postcss-focus-visible: 10.0.1(postcss@8.5.3) - postcss-focus-within: 9.0.1(postcss@8.5.3) - postcss-font-variant: 5.0.0(postcss@8.5.3) - postcss-gap-properties: 6.0.0(postcss@8.5.3) - postcss-image-set-function: 7.0.0(postcss@8.5.3) - postcss-lab-function: 7.0.8(postcss@8.5.3) - postcss-logical: 8.1.0(postcss@8.5.3) - postcss-nesting: 13.0.1(postcss@8.5.3) - postcss-opacity-percentage: 3.0.0(postcss@8.5.3) - postcss-overflow-shorthand: 6.0.0(postcss@8.5.3) - postcss-page-break: 3.0.4(postcss@8.5.3) - postcss-place: 10.0.0(postcss@8.5.3) - postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.3) - postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.3) - postcss-selector-not: 8.0.1(postcss@8.5.3) - - postcss-pseudo-class-any-link@10.0.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 + postcss-preset-env@10.6.1(postcss@8.5.14): + dependencies: + '@csstools/postcss-alpha-function': 1.0.1(postcss@8.5.14) + '@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.14) + '@csstools/postcss-color-function': 4.0.12(postcss@8.5.14) + '@csstools/postcss-color-function-display-p3-linear': 1.0.1(postcss@8.5.14) + '@csstools/postcss-color-mix-function': 3.0.12(postcss@8.5.14) + '@csstools/postcss-color-mix-variadic-function-arguments': 1.0.2(postcss@8.5.14) + '@csstools/postcss-content-alt-text': 2.0.8(postcss@8.5.14) + '@csstools/postcss-contrast-color-function': 2.0.12(postcss@8.5.14) + '@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.14) + '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.14) + '@csstools/postcss-gamut-mapping': 2.0.11(postcss@8.5.14) + '@csstools/postcss-gradients-interpolation-method': 5.0.12(postcss@8.5.14) + '@csstools/postcss-hwb-function': 4.0.12(postcss@8.5.14) + '@csstools/postcss-ic-unit': 4.0.4(postcss@8.5.14) + '@csstools/postcss-initial': 2.0.1(postcss@8.5.14) + '@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.14) + '@csstools/postcss-light-dark-function': 2.0.11(postcss@8.5.14) + '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.14) + '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.14) + '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.14) + '@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.14) + '@csstools/postcss-logical-viewport-units': 3.0.4(postcss@8.5.14) + '@csstools/postcss-media-minmax': 2.0.9(postcss@8.5.14) + '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.14) + '@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.14) + '@csstools/postcss-normalize-display-values': 4.0.1(postcss@8.5.14) + '@csstools/postcss-oklab-function': 4.0.12(postcss@8.5.14) + '@csstools/postcss-position-area-property': 1.0.0(postcss@8.5.14) + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.14) + '@csstools/postcss-property-rule-prelude-list': 1.0.0(postcss@8.5.14) + '@csstools/postcss-random-function': 2.0.1(postcss@8.5.14) + '@csstools/postcss-relative-color-syntax': 3.0.12(postcss@8.5.14) + '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.14) + '@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.14) + '@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.14) + '@csstools/postcss-syntax-descriptor-syntax-production': 1.0.1(postcss@8.5.14) + '@csstools/postcss-system-ui-font-family': 1.0.0(postcss@8.5.14) + '@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.14) + '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.14) + '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.14) + autoprefixer: 10.5.0(postcss@8.5.14) + browserslist: 4.28.2 + css-blank-pseudo: 7.0.1(postcss@8.5.14) + css-has-pseudo: 7.0.3(postcss@8.5.14) + css-prefers-color-scheme: 10.0.0(postcss@8.5.14) + cssdb: 8.9.0 + postcss: 8.5.14 + postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.14) + postcss-clamp: 4.1.0(postcss@8.5.14) + postcss-color-functional-notation: 7.0.12(postcss@8.5.14) + postcss-color-hex-alpha: 10.0.0(postcss@8.5.14) + postcss-color-rebeccapurple: 10.0.0(postcss@8.5.14) + postcss-custom-media: 11.0.6(postcss@8.5.14) + postcss-custom-properties: 14.0.6(postcss@8.5.14) + postcss-custom-selectors: 8.0.5(postcss@8.5.14) + postcss-dir-pseudo-class: 9.0.1(postcss@8.5.14) + postcss-double-position-gradients: 6.0.4(postcss@8.5.14) + postcss-focus-visible: 10.0.1(postcss@8.5.14) + postcss-focus-within: 9.0.1(postcss@8.5.14) + postcss-font-variant: 5.0.0(postcss@8.5.14) + postcss-gap-properties: 6.0.0(postcss@8.5.14) + postcss-image-set-function: 7.0.0(postcss@8.5.14) + postcss-lab-function: 7.0.12(postcss@8.5.14) + postcss-logical: 8.1.0(postcss@8.5.14) + postcss-nesting: 13.0.2(postcss@8.5.14) + postcss-opacity-percentage: 3.0.0(postcss@8.5.14) + postcss-overflow-shorthand: 6.0.0(postcss@8.5.14) + postcss-page-break: 3.0.4(postcss@8.5.14) + postcss-place: 10.0.0(postcss@8.5.14) + postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.14) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.14) + postcss-selector-not: 8.0.1(postcss@8.5.14) + + postcss-pseudo-class-any-link@10.0.1(postcss@8.5.14): + dependencies: + postcss: 8.5.14 postcss-selector-parser: 7.1.0 - postcss-reduce-idents@6.0.3(postcss@8.5.3): + postcss-reduce-idents@6.0.3(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-reduce-initial@6.1.0(postcss@8.5.3): + postcss-reduce-initial@6.1.0(postcss@8.5.14): dependencies: - browserslist: 4.23.3 + browserslist: 4.28.2 caniuse-api: 3.0.0 - postcss: 8.5.3 + postcss: 8.5.14 - postcss-reduce-transforms@6.0.2(postcss@8.5.3): + postcss-reduce-transforms@6.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-replace-overflow-wrap@4.0.0(postcss@8.5.3): + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 - postcss-selector-not@8.0.1(postcss@8.5.3): + postcss-selector-not@8.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 7.1.0 postcss-selector-parser@6.1.2: @@ -25237,27 +25522,27 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-sort-media-queries@5.2.0(postcss@8.5.3): + postcss-sort-media-queries@5.2.0(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 sort-css-media-queries: 2.2.0 - postcss-svgo@6.0.3(postcss@8.5.3): + postcss-svgo@6.0.3(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-value-parser: 4.2.0 svgo: 3.3.2 - postcss-unique-selectors@6.0.4(postcss@8.5.3): + postcss-unique-selectors@6.0.4(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss-selector-parser: 6.1.2 postcss-value-parser@4.2.0: {} - postcss-zindex@6.0.2(postcss@8.5.3): + postcss-zindex@6.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.3 + postcss: 8.5.14 postcss@8.4.14: dependencies: @@ -25265,6 +25550,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.5.3: dependencies: nanoid: 3.3.8 @@ -25434,6 +25725,10 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.15.2: + dependencies: + side-channel: 1.1.0 + qs@6.5.3: {} qs@6.7.0: {} @@ -25479,6 +25774,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + rc-animate@2.11.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2): dependencies: babel-runtime: 6.26.0 @@ -25884,7 +26186,7 @@ snapshots: dependencies: '@babel/code-frame': 7.26.2 address: 1.2.2 - browserslist: 4.23.3 + browserslist: 4.28.2 chalk: 4.1.2 cross-spawn: 7.0.6 detect-port-alt: 1.1.6 @@ -25983,7 +26285,7 @@ snapshots: react-lifecycles-compat@3.0.4: {} - react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@19.0.0))(webpack@5.98.0): + react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@19.0.0))(webpack@5.98.0): dependencies: '@babel/runtime': 7.26.0 react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.0.0)' @@ -26232,7 +26534,7 @@ snapshots: ast-types: 0.15.2 esprima: 4.0.1 source-map: 0.6.1 - tslib: 2.7.0 + tslib: 2.8.1 rechoir@0.6.2: dependencies: @@ -26270,7 +26572,7 @@ snapshots: recursive-readdir@2.2.3: dependencies: - minimatch: 3.1.2 + minimatch: 3.1.5 redent@3.0.0: dependencies: @@ -26820,12 +27122,12 @@ snapshots: dependencies: randombytes: 2.1.0 - serve-handler@6.1.6: + serve-handler@6.1.7: dependencies: bytes: 3.0.0 content-disposition: 0.5.2 mime-types: 2.1.18 - minimatch: 3.1.2 + minimatch: 3.1.5 path-is-inside: 1.0.2 path-to-regexp: 3.3.0 range-parser: 1.2.0 @@ -27224,6 +27526,8 @@ snapshots: statuses@2.0.1: {} + statuses@2.0.2: {} + std-env@3.8.1: {} stdin-discarder@0.2.2: {} @@ -27431,10 +27735,10 @@ snapshots: optionalDependencies: '@babel/core': 7.26.9 - stylehacks@6.1.1(postcss@8.5.3): + stylehacks@6.1.1(postcss@8.5.14): dependencies: - browserslist: 4.23.3 - postcss: 8.5.3 + browserslist: 4.28.2 + postcss: 8.5.14 postcss-selector-parser: 6.1.2 stylis@4.3.4: {} @@ -27800,6 +28104,8 @@ snapshots: tslib@2.7.0: {} + tslib@2.8.1: {} + tslint@5.20.1(typescript@4.1.6): dependencies: '@babel/code-frame': 7.26.2 @@ -28067,6 +28373,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + update-notifier@6.0.2: dependencies: boxen: 7.1.1 @@ -28276,7 +28588,7 @@ snapshots: webpack-bundle-analyzer@4.10.2: dependencies: '@discoveryjs/json-ext': 0.5.7 - acorn: 8.12.1 + acorn: 8.15.0 acorn-walk: 8.3.4 commander: 7.2.0 debounce: 1.2.1 @@ -28313,13 +28625,13 @@ snapshots: bonjour-service: 1.3.0 chokidar: 3.6.0 colorette: 2.0.20 - compression: 1.7.4 + compression: 1.8.1 connect-history-api-fallback: 2.0.0 default-gateway: 6.0.3 - express: 4.21.2 + express: 4.22.2 graceful-fs: 4.2.11 html-entities: 2.5.2 - http-proxy-middleware: 2.0.7(@types/express@4.17.18) + http-proxy-middleware: 2.0.9(@types/express@4.17.18) ipaddr.js: 2.2.0 launch-editor: 2.10.0 open: 8.4.2