diff --git a/.env b/.env deleted file mode 100644 index f8bd3b74..00000000 --- a/.env +++ /dev/null @@ -1,13 +0,0 @@ -# ReactPress — copy to .env and run `pnpm init` to sync from .reactpress/config.json -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_USER=reactpress -DB_PASSWD=reactpress -DB_DATABASE=reactpress - - -# Client Config -CLIENT_SITE_URL=http://localhost:3001 - -# Server Config -SERVER_SITE_URL=http://localhost:3002 \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..adce81c2 --- /dev/null +++ b/.env.example @@ -0,0 +1,34 @@ +# ReactPress — local development template +# Copy to `.env` in the repo root: cp .env.example .env +# Do not commit `.env` (secrets and machine-specific paths belong there only). +# `reactpress init` usually generates this for you; use this file when contributing to the monorepo. + +# --- Database (default: embedded SQLite, no Docker) --- +# DB_TYPE=sqlite uses DB_DATABASE as the SQLite file path. +# For MySQL via Docker, use reactpress init / embedded-docker and DB_HOST/DB_PORT/DB_USER/DB_PASSWD instead. +DB_TYPE=sqlite +DB_DATABASE=.reactpress/reactpress.db + +# --- API server (NestJS) --- +SERVER_PORT=3002 +SERVER_SITE_URL=http://localhost:3002 +SERVER_API_PREFIX=/api + +# --- Visitor theme (Next.js) public URL --- +CLIENT_SITE_URL=http://localhost:3001 + +# --- Media uploads (relative to project root) --- +REACTPRESS_UPLOAD_DIR=uploads + +# --- Bootstrap admin (first start only; change after login) --- +ADMIN_USER=admin +ADMIN_PASSWD=admin + +# --- CLI / server UI language: en | zh --- +REACTPRESS_LANG=en + +# --- Docs site (Docusaurus) — optional overrides --- +# DOCS_SITE_URL=https://docs.gaoredu.com +# DOCS_ALGOLIA_APP_ID= # default baked into docs/src/seo/algolia.ts +# DOCS_ALGOLIA_API_KEY= +# DOCS_ALGOLIA_INDEX_NAME= diff --git a/.eslintrc.js b/.eslintrc.js index 8102f728..1923bb0c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,19 +7,63 @@ module.exports = { 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', ], + parserOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + }, overrides: [ { - files: ['**/*.{ts,tsx,js,jsx}'], + files: ['web/**/*.{ts,tsx}'], + parserOptions: { + project: ['./web/tsconfig.eslint.json'], + tsconfigRootDir: __dirname, + }, + }, + { + files: ['toolkit/**/*.ts'], + parserOptions: { + project: ['./toolkit/tsconfig.json'], + tsconfigRootDir: __dirname, + }, + }, + { + files: ['server/**/*.ts'], parserOptions: { - project: ['./client/tsconfig.json'], + project: ['./server/tsconfig.json'], tsconfigRootDir: __dirname, - sourceType: 'module', + }, + }, + { + files: ['docs/**/*.{ts,tsx}'], + parserOptions: { + project: ['./docs/tsconfig.eslint.json'], + tsconfigRootDir: __dirname, + }, + }, + { + files: ['themes/hello-world/**/*.{ts,tsx}'], + parserOptions: { + project: ['./themes/hello-world/tsconfig.eslint.json'], + tsconfigRootDir: __dirname, + }, + }, + { + // Next.js regenerates next-env.d.ts with /// for route types. + files: ['**/next-env.d.ts'], + rules: { + '@typescript-eslint/triple-slash-reference': 'off', }, }, ], settings: { react: { - version: '17.0', + version: 'detect', + }, + 'import/resolver': { + node: { + paths: ['./'], + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, }, }, env: { @@ -43,12 +87,23 @@ module.exports = { '@typescript-eslint/explicit-module-boundary-types': 0, '@typescript-eslint/ban-types': 0, 'react-hooks/rules-of-hooks': 2, - 'react-hooks/exhaustive-deps': 2, + 'react-hooks/exhaustive-deps': 1, 'react/prop-types': 0, 'react/react-in-jsx-scope': 0, - 'prettier/prettier': 'error', - 'simple-import-sort/imports': 'error', - 'simple-import-sort/exports': 'error', + // Prettier 2.x cannot parse modern TS (`import type`, inline `type` imports). + // web uses `vp fmt` (Oxfmt); keep formatting out of ESLint to avoid false IDE errors. + 'prettier/prettier': 0, + 'simple-import-sort/imports': 'off', + 'simple-import-sort/exports': 'off', }, - ignorePatterns: ['dist/', 'node_modules', 'scripts'], + ignorePatterns: [ + 'dist/', + 'node_modules', + 'scripts', + 'examples', + '**/.next', + 'toolkit/dist', + 'server/dist', + 'web/src/routeTree.gen.ts', + ], }; diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index f0c95982..6fe85361 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -14,7 +14,7 @@ contact_links: url: https://github.com/fecommunity/reactpress/issues about: Search open issues before filing a duplicate (CMS, API, CLI, admin, toolkit) - name: Proposed roadmap issues - url: https://github.com/fecommunity/reactpress/blob/master/docs/proposed-reactpress-upstream-issues.md + url: https://github.com/fecommunity/reactpress/issues?q=label%3A%22help+wanted%22 about: Curated core and community issues you can pick up - name: Contributing guide url: https://github.com/fecommunity/reactpress/blob/master/CONTRIBUTING.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index ac179a1a..05cbfe0b 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -23,5 +23,5 @@ Other approaches you rejected and why. **Additional context** - **Area:** Plugin / Theme / Desktop / Docs / Integration / Other -- **Related epic:** +- **Related epic:** - Screenshots, sketches, or links — optional diff --git a/.github/ISSUE_TEMPLATE/help_wanted.md b/.github/ISSUE_TEMPLATE/help_wanted.md index eb93f0e0..ea210134 100644 --- a/.github/ISSUE_TEMPLATE/help_wanted.md +++ b/.github/ISSUE_TEMPLATE/help_wanted.md @@ -6,7 +6,7 @@ labels: help wanted assignees: '' --- - + **What do you want to work on?** diff --git a/.github/workflows/build-desktop-reusable.yml b/.github/workflows/build-desktop-reusable.yml new file mode 100644 index 00000000..a6321fa6 --- /dev/null +++ b/.github/workflows/build-desktop-reusable.yml @@ -0,0 +1,111 @@ +# Reusable desktop installer build (macOS / Windows / Linux). +# +# Callers: +# - deploy-ecs.yml — parallel with server deploy; artifacts kept in Actions +# - release-desktop.yml — attach installers to GitHub Release +name: Build Desktop (reusable) + +on: + workflow_call: + inputs: + attach-to-github-release: + description: Upload installers to the GitHub Release for this workflow run + type: boolean + default: false + artifact-retention-days: + description: Days to keep workflow artifacts when not attaching to a Release + type: number + default: 30 + +permissions: + contents: write + +jobs: + build: + name: Build (${{ matrix.platform }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + platform: macos + - os: windows-latest + platform: windows + - os: ubuntu-latest + platform: linux + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - name: Install Linux build dependencies + if: matrix.platform == 'linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libgtk-3-dev libnotify-dev libnss3 libxss1 libxtst6 \ + libatspi2.0-0 libdrm2 libgbm1 libasound2t64 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build desktop installer + env: + CSC_IDENTITY_AUTO_DISCOVERY: 'false' + run: node ./desktop/scripts/build-desktop.mjs --force + + - name: Collect release artifacts + shell: bash + run: | + mkdir -p desktop/release/upload + shopt -s nullglob + for pattern in \ + "desktop/release/*.dmg" \ + "desktop/release/*.zip" \ + "desktop/release/*.exe" \ + "desktop/release/*.AppImage" \ + "desktop/release/*.deb" \ + "desktop/release/*.rpm" + do + for file in $pattern; do + cp "$file" desktop/release/upload/ + done + done + ls -la desktop/release/upload/ + + - name: Upload platform artifacts + uses: actions/upload-artifact@v4 + with: + name: reactpress-desktop-${{ matrix.platform }} + path: desktop/release/upload/* + if-no-files-found: error + retention-days: ${{ inputs.attach-to-github-release && 7 || inputs.artifact-retention-days }} + + attach-release: + name: Attach to GitHub Release + if: inputs.attach-to-github-release + needs: build + runs-on: ubuntu-latest + steps: + - name: Download all platform artifacts + uses: actions/download-artifact@v4 + with: + path: release-assets + merge-multiple: true + + - name: List release assets + run: ls -la release-assets/ + + - name: Upload to GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: release-assets/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f95a744..f6635663 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,12 +13,10 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 cache: pnpm - name: Install dependencies @@ -28,6 +26,9 @@ jobs: run: pnpm run build:docs env: DOCS_SITE_URL: ${{ vars.DOCS_SITE_URL || 'https://docs.gaoredu.com' }} + DOCS_ALGOLIA_APP_ID: ${{ vars.DOCS_ALGOLIA_APP_ID }} + DOCS_ALGOLIA_API_KEY: ${{ vars.DOCS_ALGOLIA_API_KEY }} + DOCS_ALGOLIA_INDEX_NAME: ${{ vars.DOCS_ALGOLIA_INDEX_NAME }} build-and-smoke: runs-on: ubuntu-latest @@ -50,22 +51,17 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Build toolkit - run: pnpm run build:toolkit - - - name: Build server - run: pnpm run build:server + - name: Production build + run: pnpm run build - name: Create test .env run: | diff --git a/.github/workflows/deploy-ecs.yml b/.github/workflows/deploy-ecs.yml new file mode 100644 index 00000000..3795db3d --- /dev/null +++ b/.github/workflows/deploy-ecs.yml @@ -0,0 +1,121 @@ +# Build on GitHub → upload release tarball → SSH deploy on ECS. +# Also builds desktop installers (macOS / Windows / Linux) in parallel; artifacts +# are kept on the workflow run. Publish a GitHub Release to attach them publicly. +# +# Required GitHub Secrets (Settings → Secrets and variables → Actions): +# ECS_HOST ECS public IP or domain +# ECS_USER SSH user, e.g. root or deploy +# ECS_SSH_KEY Private key (PEM), full content including BEGIN/END lines +# ECS_APP_DIR App root on server, e.g. /opt/reactpress +# +# Optional Secrets: +# ECS_SSH_PORT Default 22 +# NGINX_ENTRY_URL Public site URL written into deploy env, e.g. https://blog.example.com +# +# One-time ECS setup: +# 1. Clone repo to ECS_APP_DIR, create .env (DB, ports, domain — never commit) +# 2. Install Node 24, pnpm, PM2, Docker; run `pnpm run deploy` once manually +# 3. Add GitHub Actions deploy key / user SSH public key to ECS ~/.ssh/authorized_keys +# +name: Deploy ECS + +on: + workflow_dispatch: + push: + branches: [main, master] + +concurrency: + group: deploy-ecs-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Production build + pack + run: pnpm run build + + - name: Locate release tarball + id: pack + run: | + TAR="$(ls -1 dist/reactpress-release-*.tar.gz | tail -1)" + echo "path=$TAR" >> "$GITHUB_OUTPUT" + echo "name=$(basename "$TAR")" >> "$GITHUB_OUTPUT" + + - name: Upload release artifact + uses: actions/upload-artifact@v4 + with: + name: reactpress-release + path: ${{ steps.pack.outputs.path }} + retention-days: 7 + + build-desktop: + uses: ./.github/workflows/build-desktop-reusable.yml + with: + attach-to-github-release: false + artifact-retention-days: 30 + + deploy: + needs: build + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + environment: production + steps: + - name: Download release artifact + uses: actions/download-artifact@v4 + with: + name: reactpress-release + path: dist + + - name: Resolve tarball name + id: tar + run: | + TAR="$(ls -1 dist/reactpress-release-*.tar.gz | tail -1)" + echo "path=$TAR" >> "$GITHUB_OUTPUT" + echo "name=$(basename "$TAR")" >> "$GITHUB_OUTPUT" + + - name: Prepare upload directory + run: | + mkdir -p upload + cp "${{ steps.tar.outputs.path }}" "upload/${{ steps.tar.outputs.name }}" + + - name: Copy release to ECS + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.ECS_HOST }} + username: ${{ secrets.ECS_USER }} + key: ${{ secrets.ECS_SSH_KEY }} + port: ${{ secrets.ECS_SSH_PORT || 22 }} + source: upload/${{ steps.tar.outputs.name }} + target: ${{ secrets.ECS_APP_DIR }}/dist/ + + - name: Deploy on ECS + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.ECS_HOST }} + username: ${{ secrets.ECS_USER }} + key: ${{ secrets.ECS_SSH_KEY }} + port: ${{ secrets.ECS_SSH_PORT || 22 }} + command_timeout: 30m + script: | + set -e + cd "${{ secrets.ECS_APP_DIR }}" + git fetch origin "${{ github.ref_name }}" --depth=1 + git checkout "${{ github.sha }}" + export NGINX_ENTRY_URL="${{ secrets.NGINX_ENTRY_URL }}" + pnpm run deploy -- "dist/${{ steps.tar.outputs.name }}" diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml new file mode 100644 index 00000000..b263fe85 --- /dev/null +++ b/.github/workflows/release-desktop.yml @@ -0,0 +1,14 @@ +name: Release Desktop + +on: + release: + types: [published] + +permissions: + contents: write + +jobs: + build-desktop: + uses: ./.github/workflows/build-desktop-reusable.yml + with: + attach-to-github-release: true diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index 4b8df181..c3600446 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -1,33 +1,30 @@ -name: Node.js Package +name: Release on: release: types: [created] jobs: - build: + validate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - run: npm ci - - run: npm test + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 - publish-gpr: - needs: build - runs-on: ubuntu-latest - permissions: - packages: write - contents: read - steps: - - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: - node-version: 20 - registry-url: https://npm.pkg.github.com/ - - run: npm ci - - run: npm publish - env: - NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Production build + run: pnpm run build + + - name: Test + run: pnpm test + + - name: Build publishable packages + run: pnpm run publish:build diff --git a/.gitignore b/.gitignore index 0b7e6763..222aa688 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,27 @@ +# 官方主题模板(运行时副本在 .reactpress/runtime/,已由 .reactpress/ 忽略) +themes/* +!themes/hello-world/ +!themes/theme-starter/ +!themes/.gitkeep +!themes/README.md + node_modules .DS_Store .idea .next +.next-preview .cursor +.brand-export/ + +# AI / coding-agent instruction files (local .cursor only — do not commit) +AGENTS.md +CLAUDE.md +GEMINI.md +**/AGENTS.md +**/CLAUDE.md +**/GEMINI.md +.github/copilot-instructions.md +.aiassistant/ .env.prod .reactpress/ @@ -14,16 +33,37 @@ node_modules sitemap.xml lib -!cli/lib +!cli/src/lib/ +!themes/hello-world/**/lib/ +!themes/theme-starter/**/lib/ +cli/out/ +cli/toolkit/ +cli/admin-dist/ +cli/plugins/ +cli/server/node_modules/ dist dist-ssr coverage logs test-results +web/dist +web/playwright-report +web/.tanstack + +# Electron desktop (desktop/) +desktop/out/ +desktop/.cache/ +desktop/release/ +desktop/*.dmg +desktop/*.zip +desktop/*.exe +desktop/*.AppImage +desktop/*.blockmap +desktop/builder-debug.yml +desktop/builder-effective-config.yaml + .pnpm-store tsconfig.tsbuildinfo -.pnpm-store - # Production (root output only; do not use bare `build` — toolkit/src/theme/build is source) @@ -43,6 +83,7 @@ docs/build # Misc .DS_Store +.env .env.local .env.development.local .env.test.local diff --git a/.npmrc b/.npmrc index 953b26a8..580aec53 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,3 @@ strict-peer-dependencies=false -engine-strict = true +engine-strict = false engine=node >=18.20.4 \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..8fdd954d --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index bf3b6e6e..4f6abb20 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ lib dist .next -node_modules \ No newline at end of file +node_modules +web/src/routeTree.gen.ts \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 0b0cebb6..370c31cc 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,7 +2,7 @@ "singleQuote": true, "quoteProps": "consistent", "bracketSpacing": true, - "jsxBracketSameLine": false, + "bracketSameLine": false, "arrowParens": "always", "trailingComma": "es5", "tabWidth": 2, diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..50c388e1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,28 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.preferences.includePackageJsonAutoImports": "auto", + "eslint.useFlatConfig": false, + "eslint.workingDirectories": [{ "mode": "auto" }], + "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], + "eslint.rules.customizations": [ + { "rule": "react-hooks/exhaustive-deps", "severity": "warn" }, + { "rule": "@typescript-eslint/no-inferrable-types", "severity": "warn" } + ], + "editor.formatOnSave": false, + "[typescript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "[javascript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "files.associations": { + "*.css": "css" + } +} diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..a888c993 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,1107 @@ +# ReactPress System Architecture + +> ReactPress 4.0 — modern full-stack CMS / blog publishing platform on React +> Core principle: **Admin manages content · Themes manage presentation · Plugins manage logic · API manages data · Toolkit manages contracts** + +--- + +## Table of contents + +- [1. Overview](#1-overview) +- [2. Architecture](#2-architecture) +- [3. Design principles & decisions](#3-design-principles--decisions) +- [4. Monorepo package structure](#4-monorepo-package-structure) +- [5. Runtime & ports](#5-runtime--ports) +- [6. Data flow & dependency rules](#6-data-flow--dependency-rules) +- [7. Maintainability](#7-maintainability) +- [8. Extensibility](#8-extensibility) +- [9. Technical choices](#9-technical-choices) +- [10. Cost & multi-platform](#10-cost--multi-platform) +- [11. Server (backend API)](#11-server-backend-api) +- [12. Web (admin SPA)](#12-web-admin-spa) +- [13. Themes (visitor frontend)](#13-themes-visitor-frontend) +- [14. Toolkit (shared contract layer)](#14-toolkit-shared-contract-layer) +- [15. CLI (orchestration)](#15-cli-orchestration) +- [16. Auth & security](#16-auth--security) +- [17. Configuration](#17-configuration) +- [18. Deployment](#18-deployment) +- [19. Local development](#19-local-development) +- [20. Plugins](#20-plugins) +- [21. Desktop client](#21-desktop-client) +- [22. Evolution & roadmap](#22-evolution--roadmap) +- [23. Acceptance criteria](#23-acceptance-criteria) +- [24. References](#24-references) + +--- + +## 1. Overview + +ReactPress is a WordPress-like content platform with a clear separation of concerns: + +| Domain | Capabilities | +|--------|--------------| +| Content | Articles, categories, tags, comments, static pages | +| Media | Upload, media library, storage (local / OSS) | +| Appearance | Theme install / activate / preview, site customization | +| Extensions | Plugin install / enable / configure | +| System | Users & permissions, site settings, import/export, analytics | + +### Non-functional goals + +| Goal | Target | +|------|--------| +| Admin responsiveness | Shell stays mounted; route switches feel < 100ms; list views cache on revisit | +| Visitor SEO | Core pages SSR/ISR; Lighthouse SEO ≥ 90 | +| Multi-device | One responsive web app for desktop / tablet / mobile | +| Data consistency | All frontends access API only through toolkit | + +--- + +## 2. Architecture + +ReactPress uses a **Monorepo + multi-process** model: content management, visitor presentation, and API services are decoupled; toolkit keeps types and contracts aligned. + +```mermaid +flowchart TB + subgraph Presentation["Presentation (replaceable, extensible)"] + Web["web — Admin SPA"] + Desktop["desktop — Electron shell (loads web/dist)"] + Theme["themes/* — Visitor SSR"] + PluginUI["plugins/*/admin — Plugin Admin slots"] + end + + subgraph Contract["Contract (stable, shared)"] + Toolkit["toolkit
api · types · react · admin · theme · plugin"] + end + + subgraph Platform["Platform (cannot be bypassed)"] + Server["server — REST · auth · Hook · extension registry"] + CLI["cli — process orchestration · scaffolding"] + DB[(MySQL / SQLite)] + end + + Web --> Toolkit + Desktop --> Web + Theme --> Toolkit + PluginUI --> Toolkit + Toolkit -->|HTTP /api| Server + Server -->|Hook| PluginServer["plugins/*/dist — Server modules"] + Server --> DB + CLI --> Web + CLI --> Theme + CLI --> Server + CLI --> Desktop +``` + +### Responsibility matrix + +| Package | Single responsibility | Rendering | SEO | +|---------|----------------------|-----------|-----| +| **server** | Business rules, persistence, auth, extension lifecycle | — | — | +| **web** | Admin UI | Vite CSR SPA | No | +| **themes/** | Visitor site | Next.js SSR/SSG/ISR | Yes | +| **toolkit** | API client, types, React integration, extension schemas | — | — | +| **plugins/** | Incremental logic (Hook + optional Admin UI) | Server + Admin slots | Plugin-dependent | +| **desktop/** | Electron shell, local API orchestration, IPC | Loads `web/dist` | No | +| **cli** | Local dev / deploy orchestration | — | — | +| **docs** | Project docs (Docusaurus) | SSG | — | + +### Architecture red lines + +- **No visitor pages in Admin**; **no admin routes in themes** (new themes must follow this) +- All frontends (web / themes / plugins) **depend on toolkit only** for API access +- **server must not depend on any frontend package** + +--- + +## 3. Design principles & decisions + +All trade-offs follow this priority: + +```mermaid +flowchart LR + M[Maintainability] --> E[Extensibility] + E --> T[Technical fit] + T --> C[Low cost] +``` + +| Principle | Meaning | How it lands | +|-------------|---------|--------------| +| **Maintainability** | Change one place, test one place, clear boundaries | Layering + Feature Modules + single API client + OpenAPI codegen | +| **Extensibility** | Core changes rarely; third parties can attach | Registry + Hook + manifest contracts | +| **Technical fit** | Match tech to scenario; avoid stack bloat | Admin = SPA, public pages = SSR, business logic in Server | +| **Low cost** | Few processes, few repos, little duplication | Monorepo + shared toolkit; responsive web instead of native apps | + +### Key decision summary + +| Decision | Choice | Maintainability | Extensibility | Fit | Cost | +|----------|--------|-----------------|---------------|-----|------| +| API access | toolkit as sole entry | ★★★ | ★★ | ★★★ | Low | +| Admin | Vite SPA | ★★ | ★★ | ★★★ | Low | +| Visitor | Next.js SSR/ISR | ★★ | ★★★ | ★★★ | Medium | +| Module layout | Feature Module + Registry | ★★★ | ★★★ | ★★ | Low | +| Plugins | Hook + manifest | ★★ | ★★★ | ★★★ | Medium | +| Themes | Separate process + `theme.json` | ★★ | ★★★ | ★★★ | Medium | +| List state | URL searchParams | ★★★ | ★★ | ★★★ | Low | +| Multi-device | Responsive web + Electron shell | ★★★ | ★★ | ★★★ | Medium | +| Types | OpenAPI codegen | ★★★ | ★★ | ★★★ | Low | + +--- + +## 4. Monorepo package structure + +Managed with **pnpm workspace** (`pnpm-workspace.yaml`): + +```yaml +packages: + - 'cli' # Global CLI (@fecommunity/reactpress) + - 'server' # NestJS API + - 'web' # Admin SPA + - 'desktop' # Electron shell + - 'docs' # Docusaurus docs site + - 'toolkit' # Shared API contract layer + - 'themes' # Theme registry + - 'themes/*' # Official theme templates + - 'plugins' # Plugin registry + - 'plugins/*' # Official plugins +``` + +### Repository tree (core) + +``` +reactpress/ +├── cli/ # CLI with bundled server +├── server/ # NestJS API source +├── web/ # Vite Admin SPA +├── desktop/ # Electron (local SQLite + Admin SPA) +├── toolkit/ # OpenAPI SDK + React integration +├── themes/ +│ ├── hello-world/ # Starter theme (local) +│ └── theme-starter/ # npm official theme catalog anchor +├── plugins/ +│ ├── hello-world/ # Auto summary plugin +│ ├── seo/ # SEO enhancement +│ └── image-optimizer/ # Image batch optimization +├── docs/ # Docusaurus +├── public/ # Marketing / brand assets +├── scripts/ # Build, deploy, smoke tests +├── docker-compose.*.yml +├── nginx*.conf +├── .reactpress/ # Runtime: active-theme.json, runtime/, plugins/ +└── package.json +``` + +### npm package mapping + +| Directory | npm package | Notes | +|-----------|-------------|-------| +| `cli/` | `@fecommunity/reactpress` | 4.0 main package; global `reactpress` command | +| `web/` | `@fecommunity/reactpress-web` | Admin SPA | +| `server/` | `@fecommunity/reactpress-server` | Monorepo source; standalone npm deprecated — use CLI bundled API | +| `toolkit/` | `@fecommunity/reactpress-toolkit` | Shared SDK | +| `themes/hello-world` | `@fecommunity/reactpress-template-hello-world` | Starter theme | + +--- + +## 5. Runtime & ports + +CLI orchestrates independent processes in local development: + +| Process | Default port | Stack | Notes | +|---------|--------------|-------|-------| +| **web** | 3000 | Vite + React | Admin entry | +| **active theme** | 3001 | Next.js | Current visitor theme | +| **server** | 3002 | NestJS | REST API (prefix `/api`) | +| **preview theme** | 3003 | Next.js | Admin iframe preview of non-active theme | +| **MySQL** | 3306 | MySQL 5.7 | Full-stack dev / default production persistence | +| **desktop local API** | 3002 | NestJS + SQLite | Embedded API in `pnpm dev:desktop` (same port as standard API) | +| **nginx** (optional) | 80 / 8080 | nginx | Unified reverse proxy | + +Three core processes (Admin, theme, API) deploy and scale independently — traffic patterns differ, so separation beats a monolithic Next app. + +```mermaid +flowchart LR + Browser["Browser"] + Nginx["nginx :80"] + Web["web :3000"] + Theme["theme :3001"] + Preview["preview :3003"] + API["server :3002"] + DB[(MySQL :3306)] + + Browser -->|"/admin"| Nginx + Browser -->|"/"| Nginx + Nginx -->|"/admin/"| Web + Nginx -->|"/"| Theme + Nginx -->|"/api"| API + Web -->|"/api proxy"| API + Theme -->|toolkit HTTP| API + Preview -->|toolkit HTTP| API + API --> DB +``` + +--- + +## 6. Data flow & dependency rules + +### Typical request paths + +**Admin write:** + +``` +web page → toolkit createClient() → POST /api/article → server ArticleService → DB +``` + +**Visitor read:** + +``` +theme getServerSideProps → toolkit fetchSingleArticle() → GET /api/article/:id → server → DB +``` + +**Theme management:** + +``` +web Appearance → GET /api/extension/themes → server ThemeService + → Setting.globalSetting (activeTheme / mods) + → .reactpress/active-theme.json + → CLI restarts Next.js theme process +``` + +### Sequence: publish article + +```mermaid +sequenceDiagram + participant U as Admin + participant W as web SPA + participant T as toolkit + participant S as server + participant H as Hook / plugin + + U->>W: Edit and publish + W->>T: useMutation → api.article.update + T->>S: PUT /article/:id + S->>H: applyFilters('article.beforePublish') + H-->>S: mutated payload + S->>S: persist + S->>H: doAction('article.afterPublish') + S-->>T: 200 + data + T-->>W: invalidateQueries + W-->>U: success notification +``` + +### Sequence: visitor article page + +```mermaid +sequenceDiagram + participant V as Visitor + participant Th as theme Next.js + participant T as toolkit/theme + participant S as server + + V->>Th: GET /article/hello-world + Th->>T: fetchArticle (SSR) + T->>S: GET /article/by-slug/hello-world + S-->>T: article + meta + T-->>Th: typed data + Th-->>V: full HTML + JSON-LD +``` + +### Dependency rules (hard constraints) + +``` +web / themes / plugins → toolkit only +toolkit → HTTP + stdlib only (no Ant Design / Next deps) +server → no frontend packages +plugins/server → server Hook + DI interfaces only +cli → orchestrates server/web/themes; not imported by business code +``` + +--- + +## 7. Maintainability + +### Single data entry: toolkit + +**Problem:** Multiple hand-rolled HTTP layers → type drift, inconsistent errors, N places to change on API updates. + +**Solution:** One client factory for the whole platform: + +```typescript +export function createClient(options: ClientOptions) { + const http = createHttpClient(options); + return { + article: new Article(http), + file: new File(http), + extension: new Extension(http), + // … mirrors server controllers + }; +} +``` + +**Benefits:** + +- Server API change → run codegen → TypeScript errors pinpoint callers +- Error codes, auth, retry logic written once +- New modules (web / theme / plugin) with zero HTTP boilerplate + +### Feature Modules (vertical slices) + +Each domain is self-contained: + +``` +web/src/modules/article/ +├── index.ts # public export: register(admin) +├── routes.tsx # TanStack Router routes +├── pages/ # thin pages composing hooks + components +├── components/ # module-private UI +├── hooks/ # data + URL state +├── schemas/ # Zod form + API boundary validation +└── permissions.ts # module permission declarations +``` + +**Forbidden between modules:** direct import of another module's internal components. +**Allowed:** Registry for menus/settings/permissions; toolkit hooks for shared server data. + +### URL as state + +List filters, pagination, and sort live in URL searchParams: + +``` +/article?page=2&status=published&sort=-createdAt&keyword=react +``` + +| Benefit | Why | +|---------|-----| +| Shareable | Admins copy links to restore views | +| Testable | E2E does not depend on component state | +| Cacheable | React Query uses URL params as queryKey | +| Device-agnostic | Desktop / mobile share the same data logic | + +### Codegen boundaries + +| Generated (no hand edits) | Hand-written | +|---------------------------|--------------| +| `toolkit/api/*` | `toolkit/react/hooks/*` | +| `toolkit/types/*` | `toolkit/admin/components/*` | +| OpenAPI spec | Feature Module business UI | + +--- + +## 8. Extensibility + +Modeled after WordPress `add_action` / `add_filter`, constrained by TypeScript manifests. + +### Extension model + +| Type | Extends | Carrier | +|------|---------|---------| +| **Theme** | Visitor UI | Independent Next.js package + `theme.json` | +| **Plugin** | Business logic + optional Admin UI | Server module + optional `admin/index.ts` | + +**Hooks** (in-process, can mutate) vs **Webhooks** (outbound HTTP) are separate. + +### Manifest contracts + +**theme.json** (flat structure — `templates` at root): + +```json +{ + "id": "hello-world", + "name": "Hello World", + "version": "1.0.0", + "requires": ">=3.5.0", + "templates": { + "home": "pages/index.tsx", + "single": "pages/article/[id].tsx", + "archive": "pages/category/[category].tsx" + }, + "supports": { "menus": ["primary", "footer"], "darkMode": true } +} +``` + +**plugin.json:** + +```json +{ + "id": "seo", + "name": "SEO Enhancement", + "version": "1.0.0", + "server": { "module": "./dist/index.js" }, + "admin": { + "slots": { "subscribe": ["article.editor.meta.afterSummary"] } + }, + "settings": { "schema": { "type": "object" } } +} +``` + +Schemas live in `toolkit/extension`; CLI validates on install — invalid packages fail at startup, not at runtime. + +### Server Hook + +```typescript +interface HookService { + applyFilters(name: string, value: T, ctx?: unknown): Promise; + doAction(name: string, payload?: unknown): Promise; +} +``` + +| Hook | When | +|------|------| +| `article.beforePublish` | Mutate fields before publish | +| `article.afterPublish` | Notify, index after publish | +| `comment.beforeCreate` | Spam filter | +| `setting.beforeSave` | Validate extension config | + +### Admin Registry + +```typescript +interface AdminModule { + id: string; + register(ctx: AdminContext): void; +} + +interface AdminContext { + menu: MenuRegistry; + settings: SettingsRegistry; + permissions: PermissionRegistry; + routes: RouteRegistry; +} +``` + +Core modules and plugins use the same API — new official features = new module + `register()`, no Shell edits. + +### Theme switching strategy + +| Phase | Strategy | Rationale | +|-------|----------|-----------| +| MVP (current) | Update `activeTheme` + restart theme process | Simple, stable SSR, no runtime federation | +| Later | Hot swap / multi-theme preview | Only when product requires it | + +### Permission model + +```typescript +type Permission = + | 'article:read' | 'article:write' | 'article:publish' + | 'media:manage' | 'page:manage' + | 'user:manage' | 'setting:manage' + | 'extension:manage'; +``` + +- **Server:** Guard checks JWT + Permission +- **Web:** `usePermission()` + route-level `` +- **Plugins:** manifest declares `permissions`; merged into roles on activate + +String capabilities beat hard-coded `role === 'admin'`. + +### Plugin three-layer model + +| Layer | Path | Role | +|-------|------|------| +| Registry | `plugins/` + `plugins/package.json` | What can be installed | +| Materialized | `.reactpress/plugins/{id}/` | Installed copy with `dist/` | +| Active | `Setting.globalSetting.plugins` | Enabled list + per-plugin config | + +| Action | Effect | +|--------|--------| +| Install | Materialize to `.reactpress/plugins/` | +| Enable | Hot-load `server.module` → `register(hooks, ctx)` | +| Disable | Remove hooks; optional `deactivate()` | +| Config | JSON Schema validation then reload | + +Built-in plugins: `hello-world`, `seo`, `image-optimizer`. See [plugins/README.md](./plugins/README.md). + +--- + +## 9. Technical choices + +### Rendering by scenario + +| Scenario | Tech | Why | +|----------|------|-----| +| Admin | **Vite + React SPA** | No SEO; small CSR bundle, fast HMR, static deploy | +| Visitor theme | **Next.js SSR/SSG/ISR** | Crawlers and social previews need full HTML | +| API | **NestJS REST** | Mature modules; OpenAPI codegen chain | + +**Not chosen:** + +| Approach | Why not | +|----------|---------| +| Admin on Next.js | No SSR/RSC benefit; extra routing + server complexity | +| Admin + theme in one app | Coupled responsibilities, bundle bloat, cannot deploy separately | +| GraphQL instead of REST | Existing Swagger pipeline; GraphQL adds schema maintenance | +| Micro-frontends (qiankun, etc.) | Team/scale mismatch; Registry + dynamic import is enough | + +### Admin frontend stack + +| Layer | Choice | Role | +|-------|--------|------| +| Build | Vite+ (`vp dev/build`) | Fast dev, native ESM | +| Routing | TanStack Router | Type-safe, file routes, searchParams first-class | +| Server state | TanStack Query | Cache, retry, optimistic mutations | +| Client state | Zustand (auth/settings only) | Light persistence; avoid global store abuse | +| UI | Ant Design 6 | Complete admin components, responsive grid | +| Validation | Zod | Unified form + API boundary | + +State split: **URL for list state · React Query for server data · Zustand for session/UI prefs**. + +### Admin performance + +| Technique | Mechanism | +|-----------|-----------| +| Persistent shell | Layout route stays mounted; only `` swaps | +| Route-level code split | Each module is its own chunk | +| Lazy heavy deps | Rich text, charts via `React.lazy()` | +| List cache | `staleTime: 30s` for instant back-navigation | +| Prefetch | Sidebar hover preloads next route chunk | + +### Visitor SEO + +| Page type | Mode | +|-----------|------| +| Home, article, archives | ISR `revalidate: 60` | +| About, privacy | SSG | +| Search | SSR | +| Comment submit | CSR island | + +`toolkit/theme` provides `fetchArticle`, `buildPageMeta`, `buildJsonLd` — theme authors call helpers, not SEO boilerplate. + +--- + +## 10. Cost & multi-platform + +### Cost model + +| Cost type | Control strategy | +|-----------|-------------------| +| Development | Monorepo + toolkit reuse; Feature Module templates for CRUD | +| Operations | Admin static hosting; theme = standard Next deploy; API single process | +| Multi-device | Responsive web — no native iOS/Android Admin | +| Extensions | manifest + Registry — no core PR for third-party features | +| Learning curve | Stack converges on React + Nest; theme authors need Next + toolkit only | + +### Responsive web (one codebase, three viewports) + +Breakpoints align with Ant Design (single standard across repo): + +| Breakpoint | Width | Admin | Theme | +|------------|-------|-------|-------| +| `< md` | < 768px | Drawer nav; table → cards | Single column | +| `md–lg` | 768–992px | Collapsed sidebar | Two columns | +| `≥ lg` | ≥ 992px | Fixed sidebar + wide table | Sidebar + main | + +Shared components in `toolkit/admin`: `ResponsiveTable`, `ResponsiveFilterToolbar`, `ResponsiveFormModal`. + +**Principle:** API has no device fields; differences are UI-only. + +**Progressive path:** + +1. Default: responsive web (zero extra engineering) +2. Optional: **Electron desktop** (same `web/dist`) +3. Optional: PWA caches shell static assets only +4. Future: Capacitor wraps `web/dist` without rewriting UI + +### What we deliberately skip (cost control) + +| Skip | Reason | +|------|--------| +| Native mobile Admin app | Responsive web covers most ops | +| Electron-embedded duplicate Admin UI | Shell only loads `web/dist` | +| Plugin marketplace sandbox (v1) | Local dir + admin trust model is enough | +| Theme runtime federation | Separate process + restart is simpler | +| Multi-DB / multi-tenant (v1) | Single-site CMS first | +| Custom ORM / UI library | TypeORM + Ant Design | + +--- + +## 11. Server (backend API) + +### Stack + +| Layer | Technology | +|-------|------------| +| Framework | NestJS 6 | +| ORM | TypeORM 0.2 | +| Database | MySQL (default); **SQLite** for desktop local mode (`DB_TYPE=sqlite`) | +| Auth | Passport + JWT, API Key | +| Docs | Swagger at `/api` | +| Other | helmet, compression, rate-limit, log4js, nodemailer, ali-oss | + +### Module layout + +``` +server/src/modules/ +├── article/ category/ tag/ comment/ page/ file/ # content +├── user/ auth/ # identity +├── setting/ smtp/ # config +├── view/ search/ knowledge/ # data +├── extension/ # theme + plugin lifecycle +├── hook/ # Action/Filter registry +├── api-key/ webhook/ health/ +└── … +``` + +Each module: thin controller → service (business + Hook calls) → entity. **extension** manages install/activate state, not domain business logic. + +### Domain entities (15) + +User, Article, ArticleRevision, Category, Tag, Comment, Page, Knowledge, File, Setting, SMTP, Search, View, ApiKey, Webhook. + +### API conventions + +- Global prefix: `/api` (`SERVER_API_PREFIX` configurable) +- Unified response: `{ statusCode, success, data }` (except `/health`) + +### Startup paths + +1. **First install:** Express wizard in `main.ts` (`/test-db`, `/install` writes `.env`) → NestJS bootstrap +2. **Normal / production:** Direct `starter.ts` bootstrap + +--- + +## 12. Web (admin SPA) + +### Stack + +| Category | Technology | +|----------|------------| +| Build | Vite+ | +| UI | React 18 + Ant Design 6 | +| Routing | TanStack Router (file routes) | +| Data | TanStack Query + Zustand (auth persist) | +| Editor | Monaco + Showdown (Markdown) | +| i18n | i18next | +| Testing | MSW + Playwright E2E | + +### Directory structure + +``` +web/src/ +├── routes/ # TanStack file routes +│ ├── login/ +│ └── _auth/ # authenticated routes +│ ├── dashboard/ article/ media/ page/ +│ ├── appearance/ settings/ plugins/ data/ +├── modules/ # feature domains (mirror routes) +├── shell/ # bootstrap, permissions, Admin Registry +├── shared/ components/ mocks/ stores/ hooks/ i18n/ +``` + +### Admin route map + +| Module | Route | APIs | +|--------|-------|------| +| Dashboard | `/` | view, article stats | +| Articles | `/article`, `/article/editor/:id?` | article, category, tag | +| Comments | `/article/comment` | comment | +| Media | `/media` | file | +| Pages | `/page`, `/page/editor/:id?` | page | +| Appearance | `/appearance/themes`, `/appearance/customize` | extension, setting | +| Plugins | `/plugins`, `/plugins/:id/settings` | extension | +| Users | `/users`, `/profile` | user | +| Settings | `/settings/:tab` | setting, smtp, api-key, webhook | +| Data | `/data/analytics`, `/data/export`, `/data/import` | view, search, export | + +Settings use routes (not tab query params). Plugins insert tabs via `settings.registerTab({ id, title, path, permission })`. + +### Module registration example + +```typescript +export const articleModule: AdminModule = { + id: 'article', + register({ menu, permissions }) { + menu.register({ + id: 'content', + title: 'Content', + children: [ + { id: 'article.list', title: 'Articles', path: '/article' }, + { id: 'article.new', title: 'New article', path: '/article/editor' }, + { id: 'article.comment', title: 'Comments', path: '/article/comment' }, + ], + }); + permissions.register(['article:read', 'article:write', 'article:publish']); + }, +}; +``` + +Shell `bootstrap()` registers core modules, then loads active plugins. Menu order uses `sort`, not import order. + +### API connection + +- Dev: `VITE_API_BASE_URL=/api`, Vite proxy to `:3002` +- Client: `getToolkitClient()` → `@fecommunity/reactpress-toolkit/react` +- Mock: `VITE_AUTH_MODE=mock` + MSW +- Live: `VITE_AUTH_MODE=server` + +--- + +## 13. Themes (visitor frontend) + +Since 3.0, visitor sites are independent Next.js packages under `themes/` (replacing legacy `client/`). + +### Package structure (hello-world) + +``` +themes/hello-world/ +├── theme.json # manifest (id, templates, customizer) +├── pages/_app.tsx # createThemeApp(manifest) +├── pages/index.tsx, article/[id].tsx, … +├── components/ styles/ next.config.js +``` + +### WordPress mapping + +| WordPress | ReactPress | +|-----------|------------| +| `style.css` header | `theme.json` | +| `functions.php` | `pages/_app.tsx` → `createThemeApp()` | +| Template hierarchy | `theme.json` → `templates` + `pages/*` | +| Customizer | `appearance.sections` + Formily + `useThemeMod` | + +### Official themes + +| Theme | Source | Role | +|-------|--------|------| +| **hello-world** | local | Minimal Pages Router starter | +| **reactpress-theme-starter** | npm (`theme-starter` anchor) | Full theme: search, knowledge base, comments, dark mode | + +### Theme lifecycle + +```mermaid +sequenceDiagram + participant Admin as web Admin + participant API as server ThemeService + participant FS as filesystem + participant CLI as cli theme-dev + participant Next as Next.js theme + + Admin->>API: POST /extension/themes/install + API->>FS: copy themes/{id} → .reactpress/runtime/{id} + Admin->>API: POST /extension/themes/activate + API->>FS: write active-theme.json + CLI->>Next: start theme :3001 + Admin->>API: beginPreviewSession + API->>FS: write preview-theme.json + CLI->>Next: start preview :3003 +``` + +See [themes/README.md](./themes/README.md). + +--- + +## 14. Toolkit (shared contract layer) + +Single API contract layer for the platform; generated from server OpenAPI. + +### Structure + +``` +toolkit/src/ +├── api/ types/ # generated from Swagger +├── react/ # createClient(), resolveApiBaseUrl(), runtime detection +├── theme/ ui/ # SSR helpers, headless components +├── admin/ plugin/ # Registry, plugin SDK +├── extension/ # theme.json / plugin.json JSON Schema +├── config/ utils/ +``` + +### Export paths + +| Path | Use | +|------|-----| +| `@fecommunity/reactpress-toolkit` | Main entry | +| `@fecommunity/reactpress-toolkit/react` | React client factory | +| `@fecommunity/reactpress-toolkit/theme` | Theme SSR | +| `@fecommunity/reactpress-toolkit/plugin/server` | Plugin Hook SDK | +| `@fecommunity/reactpress-toolkit/plugin/admin` | Plugin Admin registration | + +### Regenerate + +```bash +pnpm run generate:swagger # server → swagger.json +pnpm run build:toolkit # regenerate api/types +``` + +See [toolkit/README.md](./toolkit/README.md). + +--- + +## 15. CLI (orchestration) + +Published as `@fecommunity/reactpress` — zero-config project lifecycle. + +### Core commands + +| Command | Description | +|---------|-------------| +| `reactpress init` | Init project (`.env` + `.reactpress/config.json`; `--local` = SQLite) | +| `reactpress dev` | Full-stack dev (API + web + theme + Docker MySQL) | +| `reactpress dev --api-only` | API only (headless) | +| `reactpress dev --web-only` | Admin + API | +| `reactpress build` / `start` | Production build / start | +| `reactpress doctor` / `status` | Diagnostics / status | +| `reactpress plugin list/install` | Plugin registry | +| `reactpress theme list/add` | Theme catalog | +| `reactpress desktop dev` | Desktop dev (SQLite + Admin + Electron) | + +### CLI layout (4.0 TypeScript) + +``` +cli/ +├── bin/ # thin entry → out/bin/ +├── src/ # TypeScript source +├── out/ # compiled (gitignored) +│ ├── bin/ core/ ui/ +│ └── lib/ # dev/build/docker/theme/plugin orchestration +├── server/ # bundled NestJS runtime for npm +└── templates/ # init scaffolds +``` + +Server resolution: monorepo `server/` if present, else `cli/server/` bundled copy. + +See [cli/README.md](./cli/README.md). + +--- + +## 16. Auth & security + +### JWT (admin / user sessions) + +- Login: `POST /api/auth/login` → token (4h default) +- Protected routes: `@UseGuards(JwtAuthGuard)` + Bearer +- Roles: `@Roles('admin')` + `RolesGuard` (admin / visitor) + +### API Key (headless / integrations) + +- Header: `X-API-Key` or `Authorization: Bearer ` +- Scopes: `read` / `write` + +### Other + +- GitHub OAuth: `POST /api/auth/github` +- Passwords: bcrypt via `User.comparePassword()` + +--- + +## 17. Configuration + +**`.reactpress/config.json`** is the source of truth; `.env` is synced on `init`. **`--local`** uses SQLite with `config.local.json` / `env.local.default`. + +| File | Purpose | +|------|---------| +| `.reactpress/config.json` | Project config | +| `.reactpress/active-theme.json` | Active theme id | +| `.reactpress/preview-theme.json` | Preview theme id | +| `.reactpress/runtime/{id}/` | Materialized theme copy | +| `.reactpress/plugins/{id}/` | Materialized plugin copy | +| `.env` | DB, ports, secrets | + +| Variable | Default | +|----------|---------| +| `SERVER_PORT` | `3002` | +| `REACTPRESS_API_URL` | `http://localhost:3002/api` | + +--- + +## 18. Deployment + +### Development (recommended) + +- App processes on host (`pnpm dev`) +- Docker for **MySQL + nginx** only +- nginx forwards to host via `host.docker.internal` + +### Production options + +| Mode | Notes | +|------|-------| +| **PM2** | `pnpm build` → `pnpm start` | +| **Docker** | MySQL container + nginx; API can run on host | +| **Vercel** | Theme / Admin static deploy | + +### nginx routes (dev) + +| Path | Target | +|------|--------| +| `/` | Theme `:3001` | +| `/admin/` | Admin `:3000` | +| `/api` | API `:3002` | + +--- + +## 19. Local development + +```mermaid +flowchart LR + subgraph init [First time] + A[pnpm install] --> B[reactpress init] + B --> C[.env + .reactpress/config.json] + end + subgraph dev [Daily] + D[pnpm dev] --> E[toolkit build] + E --> F[server :3002] + F --> G[web :3000] + G --> H[theme :3001] + end + init --> dev +``` + +```bash +pnpm install +pnpm dev # API + Admin + theme + MySQL +pnpm dev:web:local # Admin + SQLite API (no Docker) +pnpm dev:desktop # Electron + SQLite +pnpm build:plugins # compile official plugins +pnpm build # toolkit → server → web → themes +``` + +After API changes: + +```bash +pnpm run generate:swagger && pnpm run build:toolkit +``` + +--- + +## 20. Plugins + +**Themes handle presentation; plugins handle logic.** Server-side Hooks extend business rules; optional Admin UI via slots. + +Covered in [§8 Extensibility](#8-extensibility) and [plugins/README.md](./plugins/README.md). + +--- + +## 21. Desktop client + +Electron shell loads the same Admin SPA as the browser — **no duplicate business UI**. + +```mermaid +flowchart TB + subgraph DesktopApp["desktop/ — Electron"] + Main[Main: window · tray · IPC · local API spawn] + Preload[Preload: contextBridge] + Renderer[Renderer: web/dist] + end + Renderer --> Toolkit[toolkit] + Toolkit --> API[server :3002] +``` + +| Layer | Responsibility | +|-------|----------------| +| **web** | All Admin UI (same as browser) | +| **toolkit** | API client, auth, `getRuntime()` / `getDesktopApi()` | +| **desktop** | Main/Preload only: window, IPC, SQLite API spawn, config | +| **server** | REST API; local mode spawned by Main, remote mode connects externally | + +### Modes + +| Mode | Description | +|------|-------------| +| **Local (default)** | Main spawns embedded API (SQLite, default `:3002`); default `admin` / `admin` | +| **Remote** | Connect to existing ReactPress API; sync local content to remote site | + +### Load modes + +| Mode | Use case | +|------|----------| +| **A. Bundled (production)** | `file://` or custom protocol → `web/dist/index.html` | +| **B. Remote URL** | Load `https://admin.example.com` (enterprise intranet) | +| **C. Dev** | `http://localhost:3000` (Vite dev server) | + +### Security (Electron) + +| Rule | Required | +|------|----------| +| `contextIsolation: true` | Yes | +| `nodeIntegration: false` in renderer | Yes | +| Preload whitelist IPC channels | Yes | +| `webSecurity: true` | Yes | +| Remote URL allowlist | When using mode B | + +### Desktop roadmap + +| Phase | Content | Status | +|-------|---------|--------| +| D0 | Scaffold; dev loads Vite; prod loads `web/dist` | ✅ 4.0 | +| D1 | Local SQLite, remote API switch, login, macOS/Windows packages | ✅ 4.0 MVP | +| D1+ | Local → remote content sync | ✅ 4.0 | +| D2 | Tray, shortcuts, native notifications | Planned | +| D3 | `electron-updater` | Planned | + +**Why Electron over Tauri:** mature updater/tray/builder ecosystem; Chromium matches Admin stack; shell is swappable — **web + toolkit stay unchanged**. + +See [desktop/README.md](./desktop/README.md). + +--- + +## 22. Evolution & roadmap + +### 4.0 vs 3.x + +| 3.x | 4.0 | +|-----|-----| +| No official plugin runtime | Hook + manifest + Admin slots; built-in hello-world, seo, image-optimizer | +| Web Admin only | Optional **Electron desktop** (SQLite local mode) | +| hello-world–centric themes | npm catalog (`theme-starter` anchor) | +| CLI pure JS `lib/` | TypeScript `src/` → `out/` | + +### 3.0 vs 2.x + +| 2.x | 3.0 | +|-----|-----| +| Monolithic `client/` Next (incl. /admin) | `web/` Admin SPA + `themes/` visitor | +| Multiple HTTP layers | Unified toolkit client | +| Manual setup | CLI `init` + `dev` | + +### Implementation phases (historical) + +| Step | Deliverable | Status | +|------|-------------|--------| +| 1–2 | toolkit client + web Shell + auth | ✅ 3.x | +| 3–4 | article module template + core CRUD | ✅ 3.x | +| 5–6 | server extension/hook + appearance/plugins UI | ✅ 3.x–4.0 | +| 7 | theme.json + CLI theme commands | ✅ 3.x | +| 8–9 | responsive components + import/export | ✅ 3.x | +| 10 | Electron desktop shell | ✅ 4.0 MVP | +| 11 | Plugin Hook + Admin slots | ✅ 4.0 | + +### Known legacy / future work + +- Standalone `server` npm package deprecated — use CLI bundled API +- `dev:client` script name retained; starts active theme +- Plugin npm catalog, marketplace, Desktop auto-update → future 4.x iterations + +### Feature coverage + +| Domain | Status | +|--------|--------| +| Content, media, appearance, system settings | ✅ | +| Plugins (Hook + Registry + Admin slots) | ✅ 4.0 | +| Desktop (Electron + SQLite) | ✅ 4.0 MVP | +| Knowledge base | ✅ server module | + +--- + +## 23. Acceptance criteria + +| Dimension | Standard | +|-----------|----------| +| Maintainability | New CRUD module ≤ one directory + one `register()`; API changes = server + codegen only | +| Extensibility | Official SEO plugin mounts menu + Hook without core edits | +| Performance | Admin route switch < 100ms; theme Lighthouse SEO ≥ 90 | +| Multi-device | No horizontal scroll at 390px; core flows pass E2E on three viewports | +| Consistency | web / themes / plugins have no custom HTTP clients | +| Desktop | Packaged app shows same Admin as browser; no forked Admin source | + +--- + +## 24. References + +- [README.md](./README.md) — quick start (English) +- [README-zh_CN.md](./README-zh_CN.md) — quick start (Chinese) +- [docs/migration-3-to-4.md](./docs/migration-3-to-4.md) — 3.x → 4.0 migration +- [docs/](./docs/) — Docusaurus tutorials +- [themes/README.md](./themes/README.md) — theme development +- [plugins/README.md](./plugins/README.md) — plugin development +- [desktop/README.md](./desktop/README.md) — desktop client +- [toolkit/README.md](./toolkit/README.md) — SDK usage +- [cli/README.md](./cli/README.md) — CLI reference diff --git a/CHANGELOG.md b/CHANGELOG.md index 54c1080f..3369d327 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,50 @@ +# Changelog + +## [4.0.0](https://github.com/fecommunity/reactpress/compare/v4.0.0-beta.13...v4.0.0) (unreleased) + +> **Stable release** — promotes 4.x from `beta` to `latest` on npm. Install with `npm i -g @fecommunity/reactpress`. + +### Publishing & packaging + +* npm metadata: brand-aligned descriptions, keywords, homepage, and README for CLI, toolkit, web, and server +* toolkit: ship `README.md` and `LICENSE` in npm tarball; license unified to MIT +* web: ship `LICENSE` in npm tarball +* server: remove invalid `types` entry; add `publishConfig`; align `engines` to Node 18+ +* **Desktop**: GitHub Actions matrix build (macOS / Windows / Linux) uploads installers to Releases +* **Docs**: canonical URL `https://docs.gaoredu.com`; desktop download guide; removed legacy 2.x / split-package pages + +### Upgrade from 3.x + +* Run `npm i -g @fecommunity/reactpress@beta` (or `@latest` after stable publish) +* Use `reactpress init` in a new directory; see [migration guide](./docs/migration-3-to-4.md) + +--- + +# [4.0.0-beta.0](https://github.com/fecommunity/reactpress/compare/v3.7.0...v4.0.0-beta.0) (2026-06-27) + +> **Pre-release** — install with `npm i -g @fecommunity/reactpress@beta` for testing. Final 4.0.0 follows after validation. + +### Plugin System + +* **Hook + manifest**: `plugin.json` lifecycle (install / activate / config / hot reload); `HookService` with filters and actions +* **Admin slots**: `AdminSlot` + `PluginAdminProvider`; SEO plugin integrates article editor +* **Built-in plugins**: `hello-world` (auto summary), `seo` (slug, keywords, meta description), `image-optimizer` (legacy media WebP batch optimization) +* **CLI**: `reactpress plugin list` / `install`; `pnpm build:plugins` in full build pipeline +* **Security**: manifest JSON Schema validation, module path constraints, Ajv config validation + +### Desktop Client + +* **Electron shell**: loads same Admin SPA (`web/dist`); `pnpm dev:desktop` / `pnpm build:desktop` +* **Local mode**: embedded SQLite API (default port `13102`), no Docker/MySQL required +* **Remote mode**: connect to existing ReactPress API +* **Sync**: push articles, pages, and settings from local to remote site + +### Themes & Docs + +* **Theme catalog**: npm anchor `theme-starter`; enhanced theme management; removed deprecated bundled themes +* **hello-world**: updated README aligned with Pages Router + `createThemeApp` +* **Docs**: ARCHITECTURE / design sync; migration 3→4; ReactPress 4.0 guide + # [3.7.0](https://github.com/fecommunity/reactpress/compare/v3.6.0...v3.7.0) (2026-06-23) ### Security diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 039cf75a..1350364e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ agree to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md). | :--- | :-- | | **Bug reports** | [Bug report](https://github.com/fecommunity/reactpress/issues/new?template=bug_report.md) — steps, component, versions, logs | | **Feature ideas** | [Feature request](https://github.com/fecommunity/reactpress/issues/new?template=feature_request.md) — problem, solution, area | -| **Community tasks** | [Help wanted](https://github.com/fecommunity/reactpress/issues/new?template=help_wanted.md) — claim a scoped item from [proposed issues](./docs/proposed-reactpress-upstream-issues.md) | +| **Community tasks** | [Help wanted](https://github.com/fecommunity/reactpress/issues/new?template=help_wanted.md) — scoped tasks labeled `help wanted` on GitHub | | **Product feedback** | [Feedback & suggestions](https://github.com/fecommunity/reactpress/issues/new?template=feedback.md) — UX and operator ideas (not security) | | **Code & docs** | Fork, branch, submit a PR (see below) | | **Security issues** | Follow [SECURITY.md](./SECURITY.md) — do **not** use public issues | @@ -30,8 +30,8 @@ Security Advisories when applicable. ### Prerequisites -- Node.js >= 18.0.0 -- pnpm >= 8.0.0 +- Node.js >= 20.0.0 +- pnpm 9.x(与根目录 `packageManager` 一致,推荐 `corepack enable` 后使用) - MySQL 5.7+ (or Docker via `pnpm run init` / `pnpm docker:dev`) ### First run @@ -52,10 +52,11 @@ Run `pnpm test` and `pnpm test:smoke` before submitting changes that touch the C reactpress/ ├── cli/ # @fecommunity/reactpress — init, dev, build, doctor ├── server/ # NestJS API (primary backend) -├── client/ # Next.js admin & public frontend +├── web/ # Admin SPA (Vite) +├── desktop/ # Electron desktop client +├── themes/ # Visitor theme templates (Next.js) +├── plugins/ # Official plugins (SEO, summaries, image optimizer) ├── toolkit/ # OpenAPI-generated API SDK + theme utilities -├── themes/ # Classic theme manifests & reference themes -├── templates/ # Starter project templates ├── docs/ # Docusaurus documentation site ├── scripts/ # Dev, deploy, and lifecycle scripts └── .reactpress/ # Local CLI config (generated) @@ -71,7 +72,7 @@ reactpress/ | Docker MySQL + proxy | `pnpm docker:dev` | | Regenerate API types | `pnpm run build:toolkit` | | Swagger spec | `pnpm run generate:swagger` | -| API lifecycle | `pnpm run start:api` / `stop` / `restart` / `status` | +| API lifecycle | `pnpm run start` / `stop` / `restart` / `status` | `pnpm dev` builds toolkit first, waits for API health, then starts the client. @@ -80,8 +81,9 @@ After API changes: `pnpm run generate:swagger` → `pnpm run build:toolkit`. ## Building ```bash -pnpm run build # toolkit + server + client +pnpm run build # toolkit + server + web + active theme pnpm run build:server # Nest only +pnpm run build:web # Admin SPA only pnpm run build:client # Next.js only pnpm run build:docs # Docusaurus site ``` @@ -121,12 +123,22 @@ sh scripts/deploy.sh Maintainers only: ```bash -pnpm login +pnpm login --registry https://registry.npmjs.org + +# Interactive (choose beta/stable + version) pnpm run publish:packages + +# Beta prerelease (uses package.json versions, npm tag: beta) +NPM_OTP=123456 pnpm run publish:packages -- --tag beta --yes + +# Explicit version +NPM_OTP=123456 pnpm run publish:packages -- --tag beta --version 4.0.0-beta.0 --yes + +# Build artifacts only (no npm publish) +pnpm run publish:build ``` -Published packages: root meta, **server**, **client**, **toolkit**, **templates**. -`@fecommunity/reactpress` is the CLI entry (`init`, `dev`, Docker database helpers). +Published packages: **toolkit**, **web**, **server** (deprecated), **cli** (`@fecommunity/reactpress`). ## Architecture & Documentation @@ -141,6 +153,12 @@ Published packages: root meta, **server**, **client**, **toolkit**, **templates* Live docs: [blog.gaoredu.com](https://blog.gaoredu.com) +## Documentation language + +- **English**: `README.md`, package READMEs (`cli/`, `toolkit/`, `server/`, `web/`, etc.), `CHANGELOG.md`, API docs +- **Chinese**: `README-zh_CN.md`, `docs/i18n/zh/`, and other `*-zh*.md` locale files +- `cli/scripts/sync-bundled-core.mjs` must not copy `README.md` from the legacy CLI package (it would revert `cli/README.md` to Chinese on every `prepare` / `prepack`) + ## Questions? - [GitHub Discussions](https://github.com/fecommunity/reactpress/discussions) (if enabled) or Issues for questions diff --git a/README-zh_CN.md b/README-zh_CN.md index e82d4e6b..bd3c6751 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -1,319 +1,415 @@
- - ReactPress 标志 - -

ReactPress

- -

- 开源发布平台 — WordPress 式编辑体验,Next.js 级前台性能。
- 一个全局包 · 零配置 CMS · Headless API · 可生产部署的主题 -

- -

- GitHub Stars - NPM 版本 - NPM 下载量 - CI - 许可证 -

+ + ReactPress 标志 + -
- - ReactPress 官方主题 — 在线演示 - +ReactPress — 用 React 发布,像 WordPress 一样交付。 + +

ReactPress

+

用 React 发布,像 WordPress 一样交付。

+ +

+ CMS、后台、无头 API、Next.js 主题、插件与桌面端 — 一条 CLI,零拼装。 +

+ +
+ +

+ 快速开始 ↓ +  ·  + 全栈演示 +  ·  + 主题演示 +  ·  + 文档 +  ·  + English +

+ +[![GitHub stars](https://img.shields.io/github/stars/fecommunity/reactpress?style=social)](https://github.com/fecommunity/reactpress/stargazers) +[![npm downloads](https://img.shields.io/npm/dm/@fecommunity/reactpress?style=flat-square)](https://www.npmjs.com/package/@fecommunity/reactpress) +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/fecommunity/reactpress/blob/master/LICENSE) +[![NPM Version](https://img.shields.io/npm/v/@fecommunity/reactpress.svg?style=flat-square)](https://www.npmjs.com/package/@fecommunity/reactpress) +[![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org/) +[![Lighthouse 95](https://img.shields.io/badge/Lighthouse-95%20%2F%20100%20SEO-0cce6b?style=flat-square&logo=lighthouse&logoColor=white)](https://reactpress-theme-starter.vercel.app) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/fecommunity/reactpress/pulls) + +

+ React + Next.js + NestJS + Electron + MySQL / SQLite +

+ +
+ + + 在 GitHub 上 Star ReactPress + -

+

如果 ReactPress 帮你省下了 CMS + API + 前台拼装的功夫 — 点个 ⭐ 能让下一位开发者更容易发现它。

- - - - - - -
- ⚡ 约 60 秒
- 零配置冷启动 -
- 📊 95 / 100 / 100 / 100
- 官方主题 Lighthouse 实测 -
- 🔌 Headless
- REST · Swagger · TypeScript SDK -
- -
- -

- 主题演示 -  ·  - 全栈演示 -  ·  - 官方文档 -  ·  - 官方主题 -  ·  - English -

- -

如果 ReactPress 帮到了你,⭐ 点个 Star,让更多人发现它。

--- -## 快速开始 +## 效果预览 -**环境要求:** Node.js 18+ · 推荐 Docker(内置 MySQL) +
-```bash -npm i -g @fecommunity/reactpress@3 -mkdir my-blog && cd my-blog -reactpress init -reactpress dev -``` +![ReactPress CLI — 从安装到上线约 60 秒](./public/usage.gif) -就这么简单 — CLI 自动生成配置、启动 MySQL、拉起 CMS API,无需手写 `.env`。 + + + + + +
+ + 桌面客户端 — SQLite 离线写作 + + 桌面端 — 离线写作,同步到线上 + + + 官方主题 — 搜索、评论、知识库 + + 访客站 — 搜索 · 评论 · 知识库 · 深色模式 +
-| 服务 | 地址 | -| :--- | :--- | -| CMS API | `http://localhost:3002/api` | -| Swagger 文档 | `http://localhost:3002/api` | -| 健康检查 | `http://localhost:3002/api/health` | + + Lighthouse:性能 95、无障碍 100、最佳实践 100、SEO 100 + -运行 `reactpress` 打开交互菜单 · 启动失败请执行 `reactpress doctor`。 +评分来自官方主题演示;实际上线结果取决于托管与内容。 -> **下一步:** 接入[官方主题](#接入访客站),或参考[全栈指南](https://docs.gaoredu.com/)。 +
-
-目录 +--- -- [快速开始](#快速开始) -- [效果演示](#效果演示) -- [为什么选 ReactPress?](#为什么选-reactpress) -- [核心特性](#核心特性) -- [适用场景](#适用场景) -- [架构概览](#架构概览) -- [使用路径](#使用路径) -- [技术栈与生态](#技术栈与生态) -- [常见问题](#常见问题) -- [社区与贡献](#社区与贡献) +## 30 秒快速开始 -
+```bash +npm i -g @fecommunity/reactpress@beta +mkdir my-site && cd my-site +reactpress init +``` ---- +**环境要求:** [Node.js 20+](https://nodejs.org/) · 无需 Docker 或外部数据库 -## 效果演示 +> 4.x 发布在 npm **`@beta`** 标签(`@latest` 仍为 3.x)。 -
+| 入口 | 地址 | +| :------ | :-- | +| **访客站** | http://localhost:3001 | +| **管理后台** | http://localhost:3001/admin/(`admin` / `admin`) | +| **API** | http://localhost:3002/api/health | -![ReactPress CLI 演示 — 从安装到运行](./public/usage.gif) +启动异常时运行 `reactpress doctor` 诊断环境问题。 - - - - - - - - - - - - - +
CLI访客站(深色模式)
- - ReactPress CLI 交互菜单 - - - - ReactPress 官方主题 — 深色模式 - -
+ + + + + +
约 60 秒
init → 全栈就绪
95 / 100
Lighthouse 性能
MIT
可自托管
1 条 CLI
完整平台
- - Lighthouse 评分:性能 95、无障碍 100、最佳实践 100、SEO 100 - +
+ +**跑通了?** [Star 本仓库](https://github.com/fecommunity/reactpress/stargazers) · [提交 Issue](https://github.com/fecommunity/reactpress/issues) · [阅读文档](https://docs.gaoredu.com/)
--- -## 为什么选 ReactPress? +## 目录 + +- [效果预览](#效果预览) +- [30 秒快速开始](#30-秒快速开始) +- [目录](#目录) +- [痛点](#痛点) +- [ReactPress 是什么?](#reactpress-是什么) +- [能做什么](#能做什么) +- [架构](#架构) +- [主题](#主题) +- [插件](#插件) +- [桌面优先写作](#桌面优先写作) +- [为什么选 ReactPress?](#为什么选-reactpress) +- [4.0 新特性](#40-新特性) +- [开发者](#开发者) +- [部署](#部署) +- [路线图(4.x)](#路线图4x) +- [FAQ](#faq) +- [贡献](#贡献) -大多数发布工具都在逼你二选一:**CMS 好用但前台慢或绑定紧**,或**静态站极快但没有像样的编辑器**。ReactPress 减轻这种取舍 — WordPress 式编辑 + 现代化、可解耦的访客站。 +--- -| | **ReactPress** | WordPress | Ghost | 静态站点(Hugo、Hexo) | -| :--- | :--- | :--- | :--- | :--- | -| **首次跑通** | **`init` + `dev`,约 60 秒**¹ | 服务器、PHP、主题与插件 | 托管或自建,步骤较多 | 每站独立仓库与构建 | -| **内容编辑** | **Web 后台** | Web 后台 | Web 后台 | Git 中的 Markdown | -| **前台速度与 SEO** | **Lighthouse 95/100/100/100**² | 因主题与插件差异大 | 通常较好 | 优秀,无内置 CMS | -| **前端灵活性** | **Headless — 可替换主题** | 主题/插件生态强,耦合度高 | 与 Ghost 主题绑定 | 构建时固定 | -| **内置能力** | **搜索、评论、知识库** | 常靠插件 | 会员/通讯为主 | 需自行实现 | -| **更适合** | **博客、内容站、定制发布** | 通用网站 | 通讯与出版业务 | 文档站、开发者博客 | +## 痛点 -¹ 需 Node.js 与 Docker 就绪;首次 Docker 拉取可能更久。 -² 基于[官方主题在线演示](https://reactpress-theme-starter.vercel.app)实测。 +现代内容系统往往逼你在几难全之间做取舍: ---- +| 路径 | 代价 | +| :--- | :--- | +| **WordPress 式 CMS** | 编辑体验好 — 主题慢、PHP 栈耦合 | +| **静态站点生成器** | 极快 — 非开发者没有像样的 CMS | +| **Headless CMS**(Strapi、Payload) | API 灵活 — 后台、前台、部署仍要自行拼装 | -## 核心特性 +> **前端团队值得拥有一个发布平台 — 而不是五个仓库硬接在一起。** -| | 特性 | 你能得到什么 | -| :---: | :--- | :--- | -| ⚡ | **约 60 秒冷启动** | `init` + `dev`,零配置,内置 Docker MySQL | -| ✍️ | **熟悉的 CMS** | 文章、页面、媒体、分类、标签、定时发布 | -| 🎨 | **现代化前台** | 官方 Next.js 主题 — 搜索、评论、知识库、深色模式 | -| 🔌 | **Headless 就绪** | REST API、Swagger、API Key、Webhook — 可替换或自建前台 | -| 📊 | **生产级指标** | 官方主题演示 Lighthouse **95 / 100 / 100 / 100** | -| 🛠️ | **开发者体验** | 交互式 CLI、`doctor`、`status`、`db backup` | -| 🌐 | **国际化** | 中英文后台与文档 | -| 📦 | **一个包搞定** | `@fecommunity/reactpress@3` — CLI + API + 模板,无需拼装 | +``` +以前 用 ReactPress +──── ───────────── +选 CMS 后端 → reactpress init +写作与管理内容 → 后台 /admin/ +访客访问主题 → http://localhost:3001 +遇到问题 → reactpress doctor +``` --- -## 适用场景 +## ReactPress 是什么? + +ReactPress 是 **为 React 时代打造的全栈发布平台** — 不是又一个需要你自己接线的 Headless 后端。 + +一条 CLI,全部包含: + +| 层级 | 你得到什么 | +| :---- | :----------- | +| **CMS** | WordPress 式编辑 — 文章、页面、媒体、分类 | +| **API** | Headless REST — React 优先、Swagger 文档 | +| **Admin** | Web 写作界面 — 无需另建后台 | +| **Themes** | 可 npm 安装的 Next.js 前台 — 可替换 | +| **Plugins** | 基于 Hook 的扩展 — SEO、摘要、图片优化 | +| **Desktop** | 本地优先写作 — SQLite、离线、可同步线上 | -| | 场景 | 为什么选 ReactPress | -| :---: | :--- | :--- | -| 📝 | **个人博客** | 富文本编辑器 — 不必在 Git 里写 Markdown | -| 🏢 | **内容站与文档** | 知识库、搜索、评论开箱即用 | -| 🧑‍💻 | **开发团队** | Headless API + SDK,任意前端栈 | -| 🚀 | **独立开发者** | `npm i -g` → 约 60 秒跑通 CMS | -| 🔌 | **Headless CMS** | REST + Swagger + Webhook 作为内容中心 | +> 内容归系统管,前台归开发者管。**它不是 CMS — 它是发布平台。** --- -## 架构概览 +## 能做什么 + +| 场景 | 为什么适合 | +| :------- | :------------------ | +| 个人博客 | 后台写作 + Lighthouse 级 Next.js 主题 | +| 开发者文档与知识库 | 官方主题 + API 内置 | +| SaaS 营销站 | Headless API + 自定义 Next.js 前台 | +| 多编辑团队 | Web 后台给作者,主题仓库给工程师 | +| 离线优先工作流 | 桌面端 SQLite,就绪后同步 | + +--- -ReactPress 将**内容管理**与**前台展示**解耦 — 在后台创作,任意前端渲染。 +## 架构 ```mermaid flowchart LR - subgraph Author["创作端 · :3001/admin"] - A["管理后台
React · Ant Design"] + subgraph Authoring + Admin["Admin"] + Desktop["Desktop"] end - - subgraph Core["内容平台 · :3002"] - B["NestJS REST API
Swagger · Headless · Webhook"] - DB[(MySQL)] - B --- DB + subgraph Core + API["CMS API"] + Plugins["Plugins"] end - - subgraph Delivery["访客交付"] - C["官方主题
Next.js SSR"] - D["自定义前台
Toolkit · REST · API Key"] + subgraph Delivery + Theme["Theme"] end - - A -->|创作与发布| B - B -->|SSR 拉数| C - B -->|Headless API| D + Admin --> API + Desktop --> API + Plugins --> API + API --> Theme ``` -| 组件 | 作用 | -| :--- | :--- | -| **CLI(`reactpress`)** | 初始化、开发、构建、部署、Docker、诊断 | -| **CMS API** | 内容、媒体、设置、Headless 接口、Webhook | -| **管理后台** | 编辑者的 Web 界面(全栈部署中包含) | -| **[官方主题](https://github.com/fecommunity/reactpress-theme-starter)** | 推荐访客站 — 快速、SEO 友好、功能完整 | -| **[@fecommunity/reactpress-toolkit](https://www.npmjs.com/package/@fecommunity/reactpress-toolkit)** | 自建前台的 TypeScript SDK | +``` +CMS Core → 内容、媒体、设置 (NestJS) +Admin UI → 写作体验 (React + Vite) +API Layer → Headless 访问 (REST + Swagger) +Theme System → 访客前台 (Next.js, npm) +Plugin System→ 扩展能力 (hooks + Admin 插槽) +Desktop App → 离线写作 (Electron + SQLite) +``` --- -## 使用路径 +## 主题 + +主题是完全可替换的 Next.js 前台 — 不绑定核心。在 **管理后台 → 外观 → 主题** 中安装并启用。 -### 预览主题(无需后端) +无需后端即可预览: ```bash npx create-next-app@latest my-blog --example "https://github.com/fecommunity/reactpress-theme-starter" --use-pnpm cd my-blog && pnpm dev:mock ``` -打开 **http://localhost:3001** — 与[在线演示](https://reactpress-theme-starter.vercel.app)相同。 +**在线演示:** [reactpress-theme-starter.vercel.app](https://reactpress-theme-starter.vercel.app) · [![使用 Vercel 部署](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/fecommunity/reactpress-theme-starter) -[![使用 Vercel 部署主题](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/fecommunity/reactpress-theme-starter) +--- + +## 插件 + +扩展核心,无需改源码。在 **管理后台 → 插件** 中安装并启用。 + +| 插件 | 能力 | +| :----- | :--------- | +| `seo` | Slug、关键词、meta 描述 + Admin 编辑器插槽 | +| `hello-world` | 自动生成文章摘要 | +| `image-optimizer` | 媒体库批量 WebP 优化 | + +开发指南:[plugins/README.md](./plugins/README.md) + +--- + +## 桌面优先写作 + +离线写作,就绪后同步。无需 Docker。 + +**[下载桌面客户端](https://docs.gaoredu.com/docs/tutorial-extras/desktop-client)**(macOS / Windows / Linux)· 或从源码打包: + +```bash +pnpm dev:desktop # monorepo 根目录 +pnpm build:desktop # 打包安装程序 +``` + +SQLite 本地存储 · 离线编辑 · 远程 API 模式 · 同步到生产 · [desktop/README.md](./desktop/README.md) + +--- + +## 为什么选 ReactPress? + +| | ReactPress | WordPress | 静态站 | Headless CMS | +| :-: | :--- | :--- | :--- | :--- | +| **编辑体验** | 有 | 有 | 无 | 部分 | +| **前台自由度** | 有 | 无 | 仅构建时 | 有 | +| **开箱完整系统** | 有 | 靠插件 | 无 | 无 | +| **上手时间** | 约 1 分钟 | 数小时 | 单站快 | 搭建 + 拼装 | +| **本地 / 离线写作** | 桌面端 | 无 | 无 | 无 | +| **Lighthouse 性能** | 95² | 看主题 | 优秀 | 看前台 | + +**对比 WordPress** — 同样的后台工作流,现代化 Next.js 交付,无 PHP 主题臃肿。 + +**对比静态生成器** — 保留速度,补上真正的 CMS。 + +**对比 Strapi / Payload** — 它们只 ship 后端;ReactPress ship **完整发布平台**。 + +² [官方主题演示](https://reactpress-theme-starter.vercel.app) + +--- + +## 4.0 新特性 + +代号 **Extend** — 插件、桌面端、npm 主题。仍是 **一条 CLI、一套 Admin**。 + +```bash +npm i -g @fecommunity/reactpress@beta +``` -### 接入访客站 +[4.0 指南](./docs/tutorial/tutorial-extras/reactpress-4-0.md) · [从 3.x 迁移](./docs/tutorial/tutorial-extras/migration-3-to-4.md) -1. 保持 ReactPress API 运行(`reactpress dev`,或 `reactpress dev --api-only`)。 -2. 克隆 [reactpress-theme-starter](https://github.com/fecommunity/reactpress-theme-starter) → `pnpm install`。 -3. 复制 `.env.example` 为 `.env` → `pnpm dev`。 +--- -在 ReactPress 管理后台调整颜色、Logo 与导航。完整说明:[主题 README](https://github.com/fecommunity/reactpress-theme-starter/blob/master/README_zh.md)。 +## 开发者 -### 部署上线 +默认 Headless,任意前台通过 REST 接入。 ```bash -reactpress build && reactpress start +curl -H "X-API-Key: YOUR_KEY" \ + "http://localhost:3002/api/article/headless/list?status=publish&page=1&pageSize=10" ``` -Docker、PM2、备份等:[完整文档](https://docs.gaoredu.com/)。 +| 资源 | 链接 | +| :------- | :--- | +| Swagger | http://localhost:3002/api | +| 主题开发 | [themes/README.md](./themes/README.md) | +| 插件开发 | [plugins/README.md](./plugins/README.md) | +| 官方 Starter | [reactpress-theme-starter](https://github.com/fecommunity/reactpress-theme-starter) | -### 常用命令 +
+CLI 命令与端口(本地 init) | 命令 | 作用 | -| :--- | :--- | -| `reactpress` | 交互式菜单 | -| `reactpress init` | 初始化新站点 | -| `reactpress dev` | 本地运行(API;访客站需接入主题) | -| `reactpress dev --api-only` | 仅 API(Headless 模式) | -| `reactpress build` / `reactpress start` | 生产构建与启动 | -| `reactpress doctor` / `reactpress status` | 诊断与查看状态 | -| `reactpress db backup` | 备份数据库 | +| :------ | :----- | +| `reactpress` / `reactpress init` | 初始化并启动(SQLite + API + 主题) | +| `reactpress init --force` | 重新初始化已有项目 | +| `reactpress doctor` | 诊断环境与访问地址 | +| `reactpress logs` | 查看 API 日志 | +| `reactpress stop` | 停止 API 与访客站服务 | + +| 服务 | 地址 / 端口 | +| :------ | :--------- | +| 访客站 | http://localhost:3001 | +| 管理后台 | http://localhost:3001/admin/ | +| API | http://localhost:3002/api | + +Monorepo 贡献者请参阅 [CONTRIBUTING.md](./CONTRIBUTING.md) 及各子包 README(`server/`、`web/`、`themes/`)。 + +
+ +--- + +## 部署 + +`reactpress init` 会启动本地生产形态的全栈(SQLite API + 内嵌后台的主题)。VPS、Docker、PM2 与备份见[部署文档](https://docs.gaoredu.com/)。 + +仅部署访客站:部署 [reactpress-theme-starter](https://github.com/fecommunity/reactpress-theme-starter) 并指向你的 API。 --- -## 技术栈与生态 +## 路线图(4.x) -| | | -| :-- | :-- | -| **技术栈** | Node.js CLI · NestJS API · MySQL · React 后台 · Next.js 主题 · TypeScript SDK | -| **主仓库** | [fecommunity/reactpress](https://github.com/fecommunity/reactpress) | -| **官方主题** | [fecommunity/reactpress-theme-starter](https://github.com/fecommunity/reactpress-theme-starter) | -| **官方文档** | [docs.gaoredu.com](https://docs.gaoredu.com/) | -| **NPM** | [@fecommunity/reactpress](https://www.npmjs.com/package/@fecommunity/reactpress) · [@fecommunity/reactpress-toolkit](https://www.npmjs.com/package/@fecommunity/reactpress-toolkit) | -| **在线演示** | [全栈](https://blog.gaoredu.com) · [仅主题](https://reactpress-theme-starter.vercel.app) | +- 插件 npm 目录 +- 桌面端自动更新、托盘、快捷键 +- 主题与插件市场 --- -## 常见问题 +## FAQ
-必须用 Docker 吗? +需要 Docker 吗? + +默认 CLI 流程不需要 — `reactpress init` 使用内置 SQLite。仅在 `.reactpress/config.json` 配置 `embedded-docker`(MySQL)时才需要 Docker。桌面端同样使用 SQLite,无需 Docker。 -推荐使用。ReactPress 默认通过嵌入式 Docker MySQL 运行。也可在 `.reactpress/config.json` 中配置外部 MySQL。
-可以用自己的前台吗? +能用自己的前台吗? + +可以 — Headless REST API + API Key。Fork [官方 starter](https://github.com/fecommunity/reactpress-theme-starter) 或对接 `/api/article`、`/api/page` 等接口。 + +
+ +
+和 WordPress 有什么不同? + +同样是后台驱动的工作流,但默认主题更快、Headless 路径更干净,现代 React/Next.js 前台无需插件堆叠。 -可以。ReactPress 以 Headless 为优先 — REST API、API Key 和 [@fecommunity/reactpress-toolkit](https://www.npmjs.com/package/@fecommunity/reactpress-toolkit) 可接入任意技术栈。
-和 WordPress 有什么区别? +4.0 能用于生产吗? + +4.0 处于 beta 阶段(撰写时为 `4.0.0-beta.18`)。已发布的 CLI 支持 `init`、`doctor`、`logs`、`stop`。生产环境升级前请阅读[迁移指南](./docs/tutorial/tutorial-extras/migration-3-to-4.md)。 -同样是后台驱动的发布工作流,但通往快速现代化前台的路径更短 — 无需 PHP 栈,也无需靠插件堆叠来优化性能。API 与主题在设计上就是解耦的。
-「约 60 秒跑通」是什么意思? +WordPress 替代?Headless CMS?Next.js 博客? + +都可以 — ReactPress 同时覆盖:自托管 WordPress 式编辑、供自定义前台的 Headless REST,以及 Lighthouse 95 的官方 Next.js 主题。 -在 Node.js 与 Docker 已安装的前提下,二次冷启动(`reactpress init` + `reactpress dev`)通常在 60 秒内完成。首次拉取 Docker 镜像会更久。
--- -## 社区与贡献 +## 贡献 -| | | -| :-- | :-- | -| **Bug 与功能建议** | [GitHub Issues](https://github.com/fecommunity/reactpress/issues) | -| **问答与想法** | [GitHub Discussions](https://github.com/fecommunity/reactpress/discussions) | -| **参与贡献** | [贡献指南](./CONTRIBUTING.md) · [行为准则](./CODE_OF_CONDUCT.md) · [安全策略](./SECURITY.md) | +[贡献指南](./CONTRIBUTING.md) · [行为准则](./CODE_OF_CONDUCT.md) · [安全策略](./SECURITY.md) -**衷心感谢**每一位帮助 ReactPress 成长的朋友。 +[Issues](https://github.com/fecommunity/reactpress/issues) · [Pull requests](https://github.com/fecommunity/reactpress/pulls) @@ -342,7 +438,17 @@ Docker、PM2、备份等:[完整文档](https://docs.gaoredu.com/)。
-MIT License · © ReactPress / FECommunity +**MIT License** · © ReactPress / FECommunity + +
+ + + 在 GitHub 上 Star ReactPress + + +

用 React 发布,像 WordPress 一样交付。
帮助更多开发者发现它 — 欢迎在 GitHub 上 ⭐。

+ +
diff --git a/README.md b/README.md index 6d5d050b..b1da503f 100644 --- a/README.md +++ b/README.md @@ -1,279 +1,368 @@
- - ReactPress Logo - -

ReactPress

+ + ReactPress Logo + -

- The open-source publishing platform — WordPress familiarity, Next.js performance.
- One global package · Zero-config CMS · Headless API · Production-ready themes -

+ReactPress — Publish with React. Ship like WordPress. + +

ReactPress

+ +

Publish with React. Ship like WordPress.

+ +

+ CMS, Admin, Headless API, Next.js themes, plugins & desktop — one CLI, zero assembly. +

+ +
+ +

+ Quick start ↓ +  ·  + Live demo +  ·  + Theme demo +  ·  + Docs +  ·  + 中文 +

+ +[![GitHub stars](https://img.shields.io/github/stars/fecommunity/reactpress?style=social)](https://github.com/fecommunity/reactpress/stargazers) +[![npm downloads](https://img.shields.io/npm/dm/@fecommunity/reactpress?style=flat-square)](https://www.npmjs.com/package/@fecommunity/reactpress) +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/fecommunity/reactpress/blob/master/LICENSE) +[![NPM Version](https://img.shields.io/npm/v/@fecommunity/reactpress.svg?style=flat-square)](https://www.npmjs.com/package/@fecommunity/reactpress) +[![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org/) +[![Lighthouse 95](https://img.shields.io/badge/Lighthouse-95%20%2F%20100%20SEO-0cce6b?style=flat-square&logo=lighthouse&logoColor=white)](https://reactpress-theme-starter.vercel.app) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/fecommunity/reactpress/pulls) + +

+ React + Next.js + NestJS + Electron + MySQL / SQLite +

+ +
+ + + Star ReactPress on GitHub + -

- GitHub stars - NPM version - NPM downloads - CI - License -

+

If ReactPress saves you from stitching CMS + API + frontend — a ⭐ helps the next developer find it.

-
+
- - ReactPress official theme — live demo - +--- -

+## See it in action + +
+ +![ReactPress CLI — install to live site in ~60 seconds](./public/usage.gif) + +
+ + + + +
+ + Desktop — offline writing with SQLite + + Desktop — offline writing, sync to production + + + Official theme — search, comments, knowledge base + + Visitor site — search · comments · knowledge base · dark mode +
+ + + Lighthouse: Performance 95, Accessibility 100, Best Practices 100, SEO 100 + + +Scores on the official theme demo. Production results depend on hosting and content. - - - - - - -
- ⚡ ~60s
- Zero-config cold start -
- 📊 95 / 100 / 100 / 100
- Lighthouse on official theme -
- 🔌 Headless
- REST · Swagger · TypeScript SDK -
- -
- -

- Theme demo -  ·  - Full-stack demo -  ·  - Documentation -  ·  - Official theme -  ·  - 中文文档 -

- -

If ReactPress saves you time, ⭐ Star on GitHub — it helps others find it.

--- -## Quick start - -**Requirements:** Node.js 18+ · Docker recommended (bundled MySQL) +## 30-second start ```bash -npm i -g @fecommunity/reactpress@3 -mkdir my-blog && cd my-blog +npm i -g @fecommunity/reactpress@beta +mkdir my-site && cd my-site reactpress init -reactpress dev ``` -That's it — the CLI scaffolds config, starts MySQL, and launches the CMS API. No manual `.env` editing. +**Requirements:** [Node.js 20+](https://nodejs.org/) · No Docker or external database -| Service | URL | +> 4.x 发布在 npm **`@beta`** 标签(`@latest` 仍为 3.x)。 + +| Surface | URL | | :------ | :-- | -| CMS API | `http://localhost:3002/api` | -| Swagger docs | `http://localhost:3002/api` | -| Health check | `http://localhost:3002/api/health` | +| **Public site** | http://localhost:3001 | +| **Admin** | http://localhost:3001/admin/ (`admin` / `admin`) | +| **API** | http://localhost:3002/api/health | + +`reactpress doctor` diagnoses setup issues when something does not start correctly. + + + + + + + + +
~60 sec
init → live stack
95 / 100
Lighthouse performance
MIT
self-hosted
1 CLI
full platform
+ +
-Run `reactpress` for the interactive menu · `reactpress doctor` if something fails. +**Working?** [Star the repo](https://github.com/fecommunity/reactpress/stargazers) · [Open an issue](https://github.com/fecommunity/reactpress/issues) · [Read the docs](https://docs.gaoredu.com/) -> **Next:** connect the [official theme](#connect-the-public-site), or follow the [full-stack guide](https://docs.gaoredu.com/). +
-
-Table of contents +--- + +## Contents -- [Quick start](#quick-start) - [See it in action](#see-it-in-action) -- [Why ReactPress?](#why-reactpress) -- [Features](#features) -- [Perfect for](#perfect-for) +- [30-second start](#30-second-start) +- [Contents](#contents) +- [The problem](#the-problem) +- [What is ReactPress?](#what-is-reactpress) +- [What you can build](#what-you-can-build) - [Architecture](#architecture) -- [Usage paths](#usage-paths) -- [Tech stack & ecosystem](#tech-stack--ecosystem) +- [Themes](#themes) +- [Plugins](#plugins) +- [Desktop-first writing](#desktop-first-writing) +- [Why ReactPress?](#why-reactpress) +- [What's new in 4.0](#whats-new-in-40) +- [For developers](#for-developers) +- [Deploy](#deploy) +- [Roadmap (4.x)](#roadmap-4x) - [FAQ](#faq) -- [Community & contributing](#community--contributing) - -
+- [Contributing](#contributing) --- -## See it in action +## The problem -
+Modern content systems force a bad trade-off: -![ReactPress CLI — from install to running stack](./public/usage.gif) - - - - - - - - - - - - - - -
CLIPublic site (dark)
- - ReactPress CLI interactive menu - - - - ReactPress official theme — dark mode - -
+| Path | Trade-off | +| :--- | :-------- | +| **WordPress-style CMS** | Great editing — slow themes, coupled PHP stack | +| **Static site generators** | Blazing fast — no real CMS for non-developers | +| **Headless CMS** (Strapi, Payload) | Flexible API — you still assemble admin, frontend, deploy | - - Lighthouse scores: Performance 95, Accessibility 100, Best Practices 100, SEO 100 - +> **Frontend teams deserve one publishing platform — not five repos to wire together.** -
+``` +Before With ReactPress +────── ─────────────── +Pick a CMS backend → reactpress init +Write & manage content → Admin at /admin/ +Visitors see your theme → http://localhost:3001 +Something wrong? → reactpress doctor +``` --- -## Why ReactPress? - -Most publishing tools force a trade-off: **easy CMS with a slow or tightly coupled frontend**, or **blazing static sites without a proper editor**. ReactPress reduces that trade-off — WordPress-style editing with a modern, decoupled public site. - -| | **ReactPress** | WordPress | Ghost | Static (Hugo, Hexo) | -| :-- | :-- | :-- | :-- | :-- | -| **Time to first stack** | **`init` + `dev` — ~60s**¹ | Server, PHP, themes, plugins | Managed or self-hosted install | New repo + pipeline per site | -| **Content editing** | **Web admin** | Web admin | Web admin | Markdown in Git | -| **Public site speed & SEO** | **Lighthouse 95/100/100/100**² | Varies by theme/plugins | Generally strong | Excellent, no built-in CMS | -| **Frontend flexibility** | **Headless — swap the theme** | Theme/plugin ecosystem, often coupled | Tied to Ghost themes | Fixed at build time | -| **Built-in extras** | **Search, comments, knowledge base** | Often via plugins | Membership focus | Build yourself | -| **Best for** | **Blogs, content sites, custom publishing** | General websites | Newsletters & publishing | Docs & dev blogs | +## What is ReactPress? -¹ After Node.js and Docker are ready; first Docker image pull may take longer. -² Measured on the [official theme live demo](https://reactpress-theme-starter.vercel.app). +ReactPress is a **full publishing platform built for the React era** — not another headless backend to wire up. ---- +One CLI install. Everything included: -## Features +| Layer | What you get | +| :---- | :----------- | +| **CMS** | WordPress-style editing — posts, pages, media, categories | +| **API** | Headless REST — React-first, Swagger-documented | +| **Admin** | Web writing UI — no separate admin to build | +| **Themes** | npm-installable Next.js frontends — swappable | +| **Plugins** | Hook-based extensibility — SEO, summaries, image optimization | +| **Desktop** | Local-first writing — SQLite, offline, sync upstream | -| | Feature | What you get | -| :---: | :------ | :----------- | -| ⚡ | **~60s cold start** | `init` + `dev` — zero config, embedded Docker MySQL | -| ✍️ | **Familiar CMS** | Posts, pages, media, categories, tags, scheduled publishing | -| 🎨 | **Modern frontend** | Official Next.js theme — search, comments, knowledge base, dark mode | -| 🔌 | **Headless-ready** | REST API, Swagger, API keys, webhooks — swap or build your own frontend | -| 📊 | **Production scores** | Official theme demo: Lighthouse **95 / 100 / 100 / 100** | -| 🛠️ | **Developer UX** | Interactive CLI menu, `doctor`, `status`, `db backup` | -| 🌐 | **i18n** | Chinese & English admin and docs | -| 📦 | **One package** | `@fecommunity/reactpress@3` — CLI + API + templates, no assembly required | +> Content owned by the system. Frontend owned by developers. **It is not a CMS — it is a publishing platform.** --- -## Perfect for +## What you can build -| | Use case | Why ReactPress | -| :---: | :------- | :------------- | -| 📝 | **Personal blogs** | Rich editor — no Markdown-in-Git workflow | -| 🏢 | **Content sites & docs** | Knowledge base, search, comments out of the box | -| 🧑‍💻 | **Developer teams** | Headless API + SDK, any frontend stack | -| 🚀 | **Indie makers** | `npm i -g` → running CMS in ~60 seconds | -| 🔌 | **Headless CMS** | REST + Swagger + webhooks as your content hub | +| Use case | Why ReactPress fits | +| :------- | :------------------ | +| Personal blogs | Admin writing + Lighthouse-fast Next.js theme | +| Developer docs & knowledge bases | Built into official theme + API | +| SaaS marketing sites | Headless API + custom Next.js frontend | +| Multi-editor teams | Web admin for writers, theme repo for engineers | +| Offline-first workflows | Desktop app with SQLite, sync when ready | --- ## Architecture -ReactPress separates **content management** from **presentation** — write in the admin, render anywhere. - ```mermaid flowchart LR - subgraph Author["Authoring · :3001/admin"] - A["Admin console
React · Ant Design"] + subgraph Authoring + Admin["Admin"] + Desktop["Desktop"] end - - subgraph Core["Content platform · :3002"] - B["NestJS REST API
Swagger · Headless · Webhooks"] - DB[(MySQL)] - B --- DB + subgraph Core + API["CMS API"] + Plugins["Plugins"] end - - subgraph Delivery["Public delivery"] - C["Official theme
Next.js SSR"] - D["Custom frontend
Toolkit · REST · API Key"] + subgraph Delivery + Theme["Theme"] end - - A -->|write & publish| B - B -->|SSR fetch| C - B -->|headless API| D + Admin --> API + Desktop --> API + Plugins --> API + API --> Theme ``` -| Component | Role | -| :-------- | :--- | -| **CLI (`reactpress`)** | Init, dev, build, deploy, Docker, diagnostics | -| **CMS API** | Content, media, settings, headless endpoints, webhooks | -| **Admin console** | Web UI for editors (included in full-stack setups) | -| **[Official theme](https://github.com/fecommunity/reactpress-theme-starter)** | Recommended public site — fast, SEO-friendly, feature-rich | -| **[@fecommunity/reactpress-toolkit](https://www.npmjs.com/package/@fecommunity/reactpress-toolkit)** | TypeScript SDK for custom frontends | +``` +CMS Core → content, media, settings (NestJS) +Admin UI → writing experience (React + Vite) +API Layer → headless access (REST + Swagger) +Theme System → visitor-facing frontend (Next.js, npm) +Plugin System→ extensibility (hooks + Admin slots) +Desktop App → offline writing (Electron + SQLite) +``` --- -## Usage paths +## Themes -### Preview the theme (no backend) +Themes are fully replaceable Next.js frontends — not locked to core. Install and activate them in **Admin → Appearance → Themes**. + +Preview without a backend: ```bash npx create-next-app@latest my-blog --example "https://github.com/fecommunity/reactpress-theme-starter" --use-pnpm cd my-blog && pnpm dev:mock ``` -Open **http://localhost:3001** — same as the [live demo](https://reactpress-theme-starter.vercel.app). +**Live:** [reactpress-theme-starter.vercel.app](https://reactpress-theme-starter.vercel.app) · [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/fecommunity/reactpress-theme-starter) + +--- + +## Plugins + +Extend without touching core. Install and enable plugins in **Admin → Plugins**. + +| Plugin | Capability | +| :----- | :--------- | +| `seo` | Slug, keywords, meta description + Admin editor slot | +| `hello-world` | Auto-generate article summaries | +| `image-optimizer` | Batch WebP optimization for media | + +Dev guide: [plugins/README.md](./plugins/README.md) + +--- + +## Desktop-first writing + +Write offline. Sync when ready. No Docker required. + +**[Download desktop client](https://docs.gaoredu.com/docs/tutorial-extras/desktop-client)** (macOS / Windows / Linux) · or build from source: + +```bash +pnpm dev:desktop # monorepo root +pnpm build:desktop # build installer +``` + +SQLite local storage · offline editing · remote API mode · sync to production · [desktop/README.md](./desktop/README.md) + +--- + +## Why ReactPress? + +| | ReactPress | WordPress | Static sites | Headless CMS | +| :-: | :--- | :--- | :--- | :--- | +| **Editing experience** | Yes | Yes | No | Partial | +| **Frontend freedom** | Yes | No | Build-time only | Yes | +| **Full system out of box** | Yes | Via plugins | No | No | +| **Time to start** | ~1 min | Hours | Fast per site | Setup + assembly | +| **Local / offline writing** | Desktop app | No | No | No | +| **Lighthouse performance** | 95² | Theme-dependent | Excellent | Depends on frontend | + +**vs WordPress** — same editing workflow, modern Next.js delivery, no PHP theme bloat. + +**vs Static generators** — keep the speed, add a real CMS. + +**vs Strapi / Payload** — they ship a backend; ReactPress ships the **full publishing platform**. + +² [Official theme demo](https://reactpress-theme-starter.vercel.app) + +--- + +## What's new in 4.0 -[![Deploy theme with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/fecommunity/reactpress-theme-starter) +Codename **Extend** — plugins, desktop, npm themes. Still **one CLI, one Admin.** -### Connect the public site +```bash +npm i -g @fecommunity/reactpress@beta +``` -1. Keep the ReactPress API running (`reactpress dev`, or `reactpress dev --api-only`). -2. Clone [reactpress-theme-starter](https://github.com/fecommunity/reactpress-theme-starter) → `pnpm install`. -3. Copy `.env.example` to `.env` → `pnpm dev`. +[4.0 guide](./docs/tutorial/tutorial-extras/reactpress-4-0.md) · [Migrate from 3.x](./docs/tutorial/tutorial-extras/migration-3-to-4.md) + +--- -Customize colors, logo, and navigation in the ReactPress admin. Full guide: [theme starter README](https://github.com/fecommunity/reactpress-theme-starter#readme). +## For developers -### Deploy to production +Headless by default. Connect any frontend via REST. ```bash -reactpress build && reactpress start +curl -H "X-API-Key: YOUR_KEY" \ + "http://localhost:3002/api/article/headless/list?status=publish&page=1&pageSize=10" ``` -Docker, PM2, backups: [full documentation](https://docs.gaoredu.com/). +| Resource | Link | +| :------- | :--- | +| Swagger | http://localhost:3002/api | +| Theme dev | [themes/README.md](./themes/README.md) | +| Plugin dev | [plugins/README.md](./plugins/README.md) | +| Official starter | [reactpress-theme-starter](https://github.com/fecommunity/reactpress-theme-starter) | + +
+CLI commands & ports (local init) + +| Command | Action | +| :------ | :----- | +| `reactpress` / `reactpress init` | Initialize and start (SQLite + API + theme) | +| `reactpress init --force` | Re-initialize existing project | +| `reactpress doctor` | Diagnose environment and URLs | +| `reactpress logs` | Tail API logs | +| `reactpress stop` | Stop API and site services | + +| Service | URL / port | +| :------ | :--------- | +| Public site | http://localhost:3001 | +| Admin | http://localhost:3001/admin/ | +| API | http://localhost:3002/api | + +Monorepo contributors: see [CONTRIBUTING.md](./CONTRIBUTING.md) and package READMEs under `server/`, `web/`, `themes/`. + +
+ +--- + +## Deploy -### Everyday commands +`reactpress init` runs a local production-style stack (SQLite API + theme with embedded admin). For VPS, Docker, PM2, and backups, see the [deployment docs](https://docs.gaoredu.com/). -| Command | What it does | -| :------ | :----------- | -| `reactpress` | Interactive menu | -| `reactpress init` | Set up a new site | -| `reactpress dev` | Run locally (API; add theme for public site) | -| `reactpress dev --api-only` | API only (headless) | -| `reactpress build` / `reactpress start` | Production build & run | -| `reactpress doctor` / `reactpress status` | Diagnose & check status | -| `reactpress db backup` | Back up the database | +Theme-only hosting: deploy [reactpress-theme-starter](https://github.com/fecommunity/reactpress-theme-starter) and point it at your API. --- -## Tech stack & ecosystem +## Roadmap (4.x) -| | | -| :-- | :-- | -| **Stack** | Node.js CLI · NestJS API · MySQL · React admin · Next.js theme · TypeScript SDK | -| **Main repo** | [fecommunity/reactpress](https://github.com/fecommunity/reactpress) | -| **Official theme** | [fecommunity/reactpress-theme-starter](https://github.com/fecommunity/reactpress-theme-starter) | -| **Documentation** | [docs.gaoredu.com](https://docs.gaoredu.com/) | -| **NPM** | [@fecommunity/reactpress](https://www.npmjs.com/package/@fecommunity/reactpress) · [@fecommunity/reactpress-toolkit](https://www.npmjs.com/package/@fecommunity/reactpress-toolkit) | -| **Live demos** | [Full stack](https://blog.gaoredu.com) · [Theme only](https://reactpress-theme-starter.vercel.app) | +- Plugin npm catalog +- Desktop auto-update, tray, shortcuts +- Theme & plugin marketplace --- @@ -282,38 +371,45 @@ Docker, PM2, backups: [full documentation](https://docs.gaoredu.com/).
Do I need Docker? -Recommended. ReactPress uses embedded Docker MySQL by default. You can also point to an external MySQL instance via `.reactpress/config.json`. +No for the default CLI flow — `reactpress init` uses embedded SQLite. Docker is only needed if you configure MySQL via `embedded-docker` in `.reactpress/config.json`. The desktop app also runs on SQLite without Docker. +
Can I use my own frontend? -Yes. ReactPress is headless-first — REST API, API keys, and [@fecommunity/reactpress-toolkit](https://www.npmjs.com/package/@fecommunity/reactpress-toolkit) for any stack. +Yes — headless REST API with API keys. Fork the [official starter](https://github.com/fecommunity/reactpress-theme-starter) or build against `/api/article`, `/api/page`, etc. +
How is this different from WordPress? -Similar admin-driven workflow, but a shorter path to a fast modern frontend — no PHP stack, no plugin maze for performance. API and theme are decoupled by design. +Same admin-driven workflow, but a faster default theme, a cleaner headless path, and no plugin bloat for a modern React/Next.js frontend. + +
+ +
+Is 4.0 production-ready? + +4.0 is in active beta (`4.0.0-beta.18` at time of writing). The published CLI supports `init`, `doctor`, `logs`, and `stop`. See the [migration guide](./docs/tutorial/tutorial-extras/migration-3-to-4.md) before upgrading production. +
-What does "~60 seconds" mean? +WordPress alternative? Headless CMS? Next.js blog? + +Yes — ReactPress targets all three: self-hosted WordPress-style editing, headless REST for custom frontends, and an official Next.js theme with Lighthouse 95 performance out of the box. -After Node.js and Docker are installed, a second cold start (`reactpress init` + `reactpress dev`) typically completes within 60 seconds. First Docker image pull takes longer.
--- -## Community & contributing +## Contributing -| | | -| :-- | :-- | -| **Issues & features** | [GitHub Issues](https://github.com/fecommunity/reactpress/issues) | -| **Questions & ideas** | [GitHub Discussions](https://github.com/fecommunity/reactpress/discussions) | -| **Contributing** | [Guide](./CONTRIBUTING.md) · [Code of Conduct](./CODE_OF_CONDUCT.md) · [Security](./SECURITY.md) | +[Contributing](./CONTRIBUTING.md) · [Code of Conduct](./CODE_OF_CONDUCT.md) · [Security](./SECURITY.md) -**Thank you** to everyone who has helped shape ReactPress. +[Issues](https://github.com/fecommunity/reactpress/issues) · [Pull requests](https://github.com/fecommunity/reactpress/pulls) @@ -342,7 +438,17 @@ After Node.js and Docker are installed, a second cold start (`reactpress init` +
-MIT License · © ReactPress / FECommunity +**MIT License** · © ReactPress / FECommunity + +
+ + + Star ReactPress on GitHub + + +

Publish with React. Ship like WordPress.
Help us reach more builders — ⭐ on GitHub.

+ +
diff --git a/SECURITY.md b/SECURITY.md index 411af86b..dc1b7e23 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,20 +2,22 @@ ## Supported Versions -We release security fixes for the actively maintained ReactPress 3.x line. +We release security fixes for the actively maintained ReactPress **4.x** line. +ReactPress 3.x receives critical fixes on a best-effort basis during the 4.x rollout. Older major versions (2.x and below) no longer receive security updates unless noted in a release announcement. | Version | Supported | | :------ | :----------------- | -| 3.x | :white_check_mark: | +| 4.x | :white_check_mark: | +| 3.x | :warning: (transition) | | 2.x | :x: | | < 2.0 | :x: | -Install the latest stable release: +Install the latest release: ```bash -npm i -g @fecommunity/reactpress@latest +npm i -g @fecommunity/reactpress@beta ``` ## Reporting a Vulnerability diff --git a/cli/.npmignore b/cli/.npmignore new file mode 100644 index 00000000..3211a9be --- /dev/null +++ b/cli/.npmignore @@ -0,0 +1,13 @@ +# Runtime deps are installed via postinstall — never ship bundled node_modules. +server/node_modules +toolkit/node_modules + +# Dev / local artifacts +server/logs +server/.reactpress +server/public/uploads +server/dist/public +**/*.tsbuildinfo +**/*.map +**/*.d.ts +**/*.d.ts.map diff --git a/cli/README.md b/cli/README.md index fdaf2ace..db2ad9f6 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,47 +1,80 @@ -# @fecommunity/reactpress-cli +# @fecommunity/reactpress -零配置一键初始化与管理 ReactPress CMS & 博客服务器。内置 NestJS 服务端,无需单独克隆 [fecommunity/reactpress](https://github.com/fecommunity/reactpress)。 +> **Publish with React. Ship like WordPress.** +> +> CMS, Admin, Headless API, Next.js themes, plugins & desktop — one CLI, zero assembly. -完整文档与贡献指南见:[github.com/fecommunity/reactpress-cli](https://github.com/fecommunity/reactpress-cli) +[![npm version](https://img.shields.io/npm/v/@fecommunity/reactpress/beta.svg?label=beta)](https://www.npmjs.com/package/@fecommunity/reactpress/v/beta) +[![npm latest](https://img.shields.io/npm/v/@fecommunity/reactpress.svg?label=latest)](https://www.npmjs.com/package/@fecommunity/reactpress) +[![npm downloads](https://img.shields.io/npm/dm/@fecommunity/reactpress.svg)](https://www.npmjs.com/package/@fecommunity/reactpress) +[![License: MIT](https://img.shields.io/npm/l/@fecommunity/reactpress.svg)](https://github.com/fecommunity/reactpress/blob/master/LICENSE) +[![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org/) -## 安装 +The official **ReactPress CLI** — install a complete, self-hosted publishing stack in ~60 seconds. CMS API, Web Admin, and Next.js theme support ship together. No Docker, nginx, or external database required for local development. + +| Layer | What you get | +| :---- | :----------- | +| **CMS API** | Headless NestJS REST — SQLite locally, MySQL in production | +| **Admin** | Writing console at `/admin/` — posts, pages, media, plugins, themes | +| **Themes** | npm-installable Next.js frontends — swap without touching content | +| **CLI** | `init`, `doctor`, `logs` — operate and diagnose from the terminal | + +Built for frontend teams who want WordPress-grade editing without wiring five repos together. + +[Documentation](https://docs.gaoredu.com/) · [Live demo](https://blog.gaoredu.com) · [Theme demo](https://reactpress-theme-starter.vercel.app) · [GitHub](https://github.com/fecommunity/reactpress) · [Chinese overview](../README-zh_CN.md) + +## Install ```bash -npm install -g @fecommunity/reactpress-cli -``` +# 4.x pre-release (recommended until 4.0.0 stable) +npm install -g @fecommunity/reactpress@beta -全局命令为 `reactpress-cli`(与 npm 包名无关)。 +# stable (3.x — legacy) +# npm install -g @fecommunity/reactpress +``` -> npm 上的无作用域包名 `reactpress-cli` 已被占用,本包发布为 `@fecommunity/reactpress-cli`。 +Requires Node.js 20+. On first install, `postinstall` downloads bundled server runtime dependencies (~1–2 minutes). -## 快速开始 +## Quick start ```bash -mkdir my-blog && cd my-blog -reactpress-cli init -reactpress-cli start +mkdir my-site && cd my-site +reactpress init ``` -浏览器访问 `http://localhost:3002`(API 文档:`/api`)。 +| Surface | URL | +| :------ | :-- | +| **Public site** | http://localhost:3001 | +| **Admin** | http://localhost:3001/admin/ (`admin` / `admin`) | +| **API** | http://127.0.0.1:3002/api | + +Run `reactpress doctor` if something does not start correctly. -## 常用命令 +## Commands -| 命令 | 说明 | -|------|------| -| `reactpress-cli init [dir]` | 初始化项目 | -| `reactpress-cli start` | 启动服务(自动准备数据库) | -| `reactpress-cli stop` | 停止服务 | -| `reactpress-cli restart` | 重启服务 | -| `reactpress-cli status` | 查看状态 | -| `reactpress-cli config [key] [value]` | 查看/修改配置 | -| `reactpress-cli config server.port 3003 --apply` | 改端口并重启 | +| Command | Description | +| :------ | :---------- | +| `reactpress init [dir]` | Initialize a new publishing site | +| `reactpress doctor [dir]` | Diagnose Node.js, ports, database, and services | +| `reactpress logs [dir]` | Tail API logs (error / request / response) | +| `reactpress stop [dir]` | Stop API and site services | -## 要求 +## Requirements -- Node.js 18+ +- Node.js 20+ - macOS / Linux / Windows -- 默认使用 Docker 运行嵌入式 MySQL;也可在 `.reactpress/config.json` 中配置外部数据库 +- Embedded SQLite + bundled API (no Docker or MySQL for local dev) + +## Ecosystem + +| Package | Role | +| :------ | :--- | +| [`@fecommunity/reactpress-toolkit`](../toolkit/) | TypeScript SDK — API clients, theme SSR, plugin hooks | +| [`@fecommunity/reactpress-web`](../web/) | Admin SPA — static assets and Node mount helpers | +| [`@fecommunity/reactpress-server`](../server/) | Standalone API (deprecated — use CLI bundled API) | + +## License -## 许可证 +MIT © [FECommunity](https://github.com/fecommunity) -MIT © FECommunity +

Part of ReactPress — content owned by the system, frontend owned by developers.

diff --git a/cli/bin/reactpress-cli-shim.js b/cli/bin/reactpress-cli-shim.js index bf2dc2b0..35931a94 100755 --- a/cli/bin/reactpress-cli-shim.js +++ b/cli/bin/reactpress-cli-shim.js @@ -4,7 +4,7 @@ * @deprecated 3.0 起请使用 `reactpress`(@fecommunity/reactpress)。3.1 将移除此 bin。 */ const chalk = require('chalk'); -const { t } = require('../lib/i18n'); +const { t } = require('../out/lib/i18n'); function mapLegacyArgv(argv) { const [cmd, ...rest] = argv; @@ -16,11 +16,7 @@ function mapLegacyArgv(argv) { } if (!process.env.REACTPRESS_SUPPRESS_DEPRECATION) { - console.warn( - chalk.yellow( - t('shim.deprecated') - ) - ); + console.warn(chalk.yellow(t('shim.deprecated'))); } const mapped = mapLegacyArgv(process.argv.slice(2)); diff --git a/cli/bin/reactpress-theme-client.js b/cli/bin/reactpress-theme-client.js new file mode 100644 index 00000000..3118c0a1 --- /dev/null +++ b/cli/bin/reactpress-theme-client.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../out/bin/reactpress-theme-client.js'); diff --git a/cli/bin/reactpress.js b/cli/bin/reactpress.js index 67829e15..a89dc68c 100755 --- a/cli/bin/reactpress.js +++ b/cli/bin/reactpress.js @@ -1,381 +1,2 @@ #!/usr/bin/env node - -/** - * ReactPress unified CLI — init, dev, build, server, docker, publish. - * Run without arguments for an interactive menu (Claude Code–style). - */ - -const { Command } = require('commander'); -const path = require('path'); -const chalk = require('chalk'); -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'); -const { runBuild } = require('../lib/build'); -const { startApiWithPm2 } = require('../lib/pm2'); -const { runNodeScript, runReactpressCli } = require('../lib/spawn'); -const { getClientBin } = require('../lib/paths'); -const { runInteractiveLoop } = require('../ui/interactive'); -const { t } = require('../lib/i18n'); - -const rootPkg = require(path.join(__dirname, '..', 'package.json')); - -const program = new Command(); - -program - .name('reactpress') - .description(t('cli.description')) - .version(rootPkg.version); - -program - .command('init') - .description(t('cli.init.description')) - .argument('[directory]', t('cli.init.directory'), '.') - .option('-f, --force', t('cli.init.force')) - .action(async (directory, options) => { - const projectRoot = path.resolve(directory); - process.env.REACTPRESS_ORIGINAL_CWD = projectRoot; - const { isMonorepoCheckout } = require('../lib/bootstrap'); - if (isMonorepoCheckout(projectRoot)) { - const result = await initMonorepoProject(projectRoot, { force: !!options.force }); - console.log(`[reactpress] ${result.message}`); - process.exit(result.ok ? 0 : 1); - return; - } - const args = ['init', directory]; - if (options.force) args.push('--force'); - runReactpressCli(args, { cwd: projectRoot }); - }); - -program - .command('dev') - .description(t('cli.dev.description')) - .option('--api-only', t('cli.dev.apiOnly')) - .option('--client-only', t('cli.dev.clientOnly')) - .action(async (options) => { - const projectRoot = ensureOriginalCwd(); - try { - if (options.clientOnly) { - await runNodeScript(getClientBin(), [], { cwd: projectRoot }); - return; - } - if (options.apiOnly) { - await runApiDev(projectRoot); - return; - } - await runDev(projectRoot); - } catch (err) { - console.error(chalk.red('[reactpress]'), err.message || err); - process.exit(err.exitCode ?? 1); - } - }); - -const serverCmd = program.command('server').description(t('cli.server.description')); - -serverCmd - .command('start') - .description(t('cli.server.start.description')) - .option('--pm2', t('cli.server.start.pm2')) - .option('--bg', t('cli.server.start.bg')) - .action(async (options) => { - const projectRoot = ensureOriginalCwd(); - try { - if (options.pm2) { - await startApiWithPm2(projectRoot); - return; - } - const cmd = options.bg ? 'start:bg' : 'start'; - const code = await runLifecycleCommand(cmd, projectRoot); - process.exit(code ?? 0); - } catch (err) { - console.error(chalk.red('[reactpress]'), err.message || err); - process.exit(1); - } - }); - -serverCmd.command('stop').description(t('cli.server.stop')).action(async () => { - const code = await runLifecycleCommand('stop', ensureOriginalCwd()); - process.exit(code ?? 0); -}); - -serverCmd.command('restart').description(t('cli.server.restart')).action(async () => { - const code = await runLifecycleCommand('restart', ensureOriginalCwd()); - process.exit(code ?? 0); -}); - -serverCmd.command('status').description(t('cli.server.status')).action(async () => { - await runLifecycleCommand('status', ensureOriginalCwd()); -}); - -const clientCmd = program.command('client').description(t('cli.client.description')); - -clientCmd - .command('start') - .description(t('cli.client.start')) - .option('--pm2', t('cli.client.start.pm2')) - .action(async (options) => { - const args = options.pm2 ? ['--pm2'] : []; - await runNodeScript(getClientBin(), args, { cwd: ensureOriginalCwd() }); - }); - -program - .command('build') - .description(t('cli.build.description')) - .option('-t, --target ', t('cli.build.target'), 'all') - .action(async (options) => { - try { - await runBuild(options.target, ensureOriginalCwd()); - } catch (err) { - console.error(chalk.red('[reactpress]'), err.message || err); - process.exit(1); - } - }); - -const dockerCmd = program.command('docker').description(t('cli.docker.description')); - -dockerCmd - .command('up') - .description(t('cli.docker.up')) - .action(async () => { - await runDockerCommand('up', ensureOriginalCwd()); - }); - -dockerCmd - .command('down') - .alias('stop') - .description(t('cli.docker.down')) - .action(async () => { - await runDockerCommand('down', ensureOriginalCwd()); - }); - -dockerCmd - .command('start') - .description(t('cli.docker.start')) - .action(async () => { - await runDockerCommand('start', ensureOriginalCwd()); - }); - -dockerCmd.command('restart').description(t('cli.docker.restart')).action(async () => { - await runDockerCommand('restart', ensureOriginalCwd()); -}); - -dockerCmd.command('status').description(t('cli.docker.status')).action(async () => { - await runDockerCommand('status', ensureOriginalCwd()); -}); - -dockerCmd - .command('logs [service]') - .description(t('cli.docker.logs')) - .action(async (service) => { - 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')) - .action(async () => { - await printUnifiedStatus(ensureOriginalCwd()); - }); - -program - .command('doctor') - .description(t('cli.doctor.description')) - .action(async () => { - const code = await runDoctor(ensureOriginalCwd()); - process.exit(code); - }); - -const dbCmd = program.command('db').description(t('cli.db.description')); - -dbCmd - .command('backup') - .description(t('cli.db.backup')) - .option('-o, --output ', t('cli.db.backup.output')) - .action(async (options) => { - try { - await runDbBackup(ensureOriginalCwd(), options.output); - } catch (err) { - console.error(chalk.red('[reactpress]'), err.message || err); - process.exit(1); - } - }); - -program - .command('publish') - .description(t('cli.publish.description')) - .option('--build', t('cli.publish.build')) - .option('--publish', t('cli.publish.publish')) - .action(async (options) => { - try { - 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); - } - }); - -program - .command('start') - .description(t('cli.start.description')) - .action(async () => { - const projectRoot = ensureOriginalCwd(); - 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, - cwd: projectRoot, - }); - child.on('close', (c) => process.exit(c ?? 0)); - }); - -program.on('--help', () => { - console.log(''); - 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(''); -}); - -async function main() { - const argv = process.argv.slice(2); - if (argv.length === 0) { - await runInteractiveLoop(); - return; - } - program.parse(process.argv); -} - -main().catch((err) => { - console.error(chalk.red('[reactpress]'), err.message || err); - process.exit(1); -}); +require('../out/bin/reactpress.js'); diff --git a/cli/lib/api-dev-runner.js b/cli/lib/api-dev-runner.js deleted file mode 100644 index 4bbde612..00000000 --- a/cli/lib/api-dev-runner.js +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env node -const { runApiDev } = require('./api-dev'); -const { ensureOriginalCwd } = require('./root'); - -ensureOriginalCwd(); -runApiDev(); diff --git a/cli/lib/api-dev.js b/cli/lib/api-dev.js deleted file mode 100644 index cac3fc4c..00000000 --- a/cli/lib/api-dev.js +++ /dev/null @@ -1,89 +0,0 @@ -const { spawn } = require('child_process'); -const path = require('path'); -const { ensureProjectEnvironment } = require('./bootstrap'); -const { - getServerBin, - getServerDir, - isUsingMonorepoServer, - canStartLocalApi, -} = require('./paths'); -const { stopApi } = require('./lifecycle'); -const { ensureOriginalCwd } = require('./root'); -const { t } = require('./i18n'); - -let apiChild; - -function stopApiDev(projectRoot) { - if (apiChild && !apiChild.killed) { - apiChild.kill('SIGTERM'); - } - if (!isUsingMonorepoServer(projectRoot)) { - stopApi(projectRoot); - } -} - -function startApiDev(projectRoot) { - if (isUsingMonorepoServer(projectRoot)) { - console.log(t('apiDev.modeServer')); - apiChild = spawn('pnpm', ['run', '--dir', './server', 'dev'], { - cwd: projectRoot, - stdio: 'inherit', - shell: true, - env: { - ...process.env, - REACTPRESS_ORIGINAL_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, - }, - }); - } else { - console.error(t('lifecycle.noServerAvailable')); - process.exit(1); - } - - if (apiChild) { - apiChild.on('close', (code) => { - process.exit(code ?? 0); - }); - console.log(t('apiDev.ctrlCHint')); - console.log(t('apiDev.stopHint')); - } -} - -async function runApiDev(projectRoot = ensureOriginalCwd()) { - try { - await ensureProjectEnvironment(projectRoot); - } catch (err) { - console.error(t('dev.envFailed'), err.message || err); - process.exit(1); - } - - process.on('SIGINT', () => { - stopApiDev(projectRoot); - process.exit(0); - }); - process.on('SIGTERM', () => { - stopApiDev(projectRoot); - process.exit(0); - }); - - startApiDev(projectRoot); -} - -function getApiDevScriptPath() { - return path.join(__dirname, 'api-dev-runner.js'); -} - -module.exports = { - runApiDev, - stopApiDev, - getApiDevScriptPath, -}; diff --git a/cli/lib/bootstrap.js b/cli/lib/bootstrap.js deleted file mode 100644 index 9d5dd7ba..00000000 --- a/cli/lib/bootstrap.js +++ /dev/null @@ -1,114 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { pathToFileURL } = require('url'); -const { ensureOriginalCwd, isMonorepoCheckout } = require('./root'); -const { getCliPackageRoot } = require('./paths'); -const { t } = require('./i18n'); - -async function importCliModule(relativePath) { - const modulePath = path.join(getCliPackageRoot(), 'dist', relativePath); - return import(pathToFileURL(modulePath).href); -} - -async function copyTemplateFile(src, dest) { - await fs.promises.mkdir(path.dirname(dest), { recursive: true }); - await fs.promises.copyFile(src, dest); -} - -async function initMonorepoProject(projectRoot, { force = false } = {}) { - const { getProjectPaths, getTemplatesDir } = await importCliModule('utils/paths.js'); - const { saveConfig, syncEnvFromConfig } = await importCliModule('services/config.js'); - const { ensureDatabase, ensureDatabaseHostPort } = await importCliModule('services/database.js'); - - const paths = getProjectPaths(projectRoot); - const templatesDir = getTemplatesDir(); - - if (fs.existsSync(paths.configPath) && !force) { - const config = await (await importCliModule('services/config.js')).loadConfig(projectRoot); - await ensureDatabaseHostPort(projectRoot, undefined, config); - const dbResult = await ensureDatabase(projectRoot, config); - if (!dbResult.ok) { - return { ok: false, projectRoot, message: dbResult.message }; - } - return { ok: true, projectRoot, message: t('bootstrap.configReady') }; - } - - await fs.promises.mkdir(paths.reactpressDir, { recursive: true }); - await copyTemplateFile( - path.join(templatesDir, 'docker-compose.yml'), - paths.dockerComposePath - ); - - const config = JSON.parse( - await fs.promises.readFile(path.join(templatesDir, 'config.default.json'), 'utf8') - ); - await saveConfig(projectRoot, config); - await syncEnvFromConfig(projectRoot, config); - - if (!fs.existsSync(paths.envPath) || force) { - await copyTemplateFile(path.join(templatesDir, 'env.default'), paths.envPath); - await syncEnvFromConfig(projectRoot, config); - } - - await ensureDatabaseHostPort(projectRoot, undefined, config); - const dbResult = await ensureDatabase(projectRoot, config); - - if (!dbResult.ok) { - return { - ok: true, - projectRoot, - message: t('bootstrap.projectDbPending', { message: dbResult.message }), - }; - } - - return { - ok: true, - projectRoot, - message: t('bootstrap.ready'), - }; -} - -async function ensureProjectEnvironment(projectRoot = ensureOriginalCwd()) { - const root = path.resolve(projectRoot); - const { setProjectCwd } = await importCliModule('utils/cli-context.js'); - setProjectCwd(root); - - const { isReactPressProject, loadConfig } = await importCliModule('services/config.js'); - const { ensureDatabase, ensureDatabaseHostPort } = await importCliModule('services/database.js'); - - if (!(await isReactPressProject(root))) { - if (isMonorepoCheckout(root)) { - const result = await initMonorepoProject(root); - if (!result.ok) { - throw new Error(result.message || t('bootstrap.initFailed')); - } - return result; - } - - const { initProject } = await importCliModule('services/init.js'); - const result = await initProject({ directory: root, force: false }); - if (!result.ok) { - throw new Error(result.message || t('bootstrap.cliInitFailed')); - } - return result; - } - - const config = await loadConfig(root); - await ensureDatabaseHostPort(root, undefined, config); - const dbResult = await ensureDatabase(root, config); - if (!dbResult.ok) { - throw new Error( - t('bootstrap.dbNotReady', { - message: dbResult.message || t('bootstrap.dbPendingShort'), - }) - ); - } - - return { ok: true, projectRoot: root, message: t('bootstrap.dbReady') }; -} - -module.exports = { - ensureProjectEnvironment, - initMonorepoProject, - isMonorepoCheckout, -}; diff --git a/cli/lib/build.js b/cli/lib/build.js deleted file mode 100644 index ee70bc19..00000000 --- a/cli/lib/build.js +++ /dev/null @@ -1,162 +0,0 @@ -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'); - -const FORBIDDEN_SCRIPTS = new Set(['build']); - -/** @type {Record} */ -const BUILD_STEPS = { - toolkit: [{ script: 'build:toolkit', labelKey: 'build.label.toolkit' }], - server: [{ script: 'build:server', labelKey: 'build.label.server' }], - client: [{ script: 'build:client', labelKey: 'build.label.client' }], - docs: [{ script: 'build:docs', labelKey: 'build.label.docs' }], - all: [ - { script: 'build:toolkit', labelKey: 'build.label.toolkit' }, - { script: 'build:server', labelKey: 'build.label.server' }, - { script: 'build:client', labelKey: 'build.label.client' }, - ], -}; - -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')); - } - - const steps = BUILD_STEPS[target]; - if (!steps) { - throw new Error( - t('build.unknownTarget', { - target, - available: TARGETS.join(', '), - }) - ); - } - - const total = steps.length; - const buildStarted = Date.now(); - - console.log(''); - if (total > 1) { - console.log(label(t('build.plan', { total }))); - console.log(''); - } - - for (let i = 0; i < steps.length; i++) { - const { script, labelKey } = steps[i]; - if (FORBIDDEN_SCRIPTS.has(script)) { - throw new Error(t('build.forbiddenScript', { script })); - } - - const current = i + 1; - 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(); - - try { - runSync(invocation.command, invocation.args, { - cwd: invocation.cwd, - env: buildChildEnv, - }); - } catch (err) { - spinner.fail(`${badge} ${t('build.stepFailed', { current, total, label: stepLabel })}`); - throw err; - } - - const seconds = ((Date.now() - stepStarted) / 1000).toFixed(1); - 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(''); - console.log(` ${icon.spark} ${ok(t('build.done', { seconds: totalSeconds }))}`); - } - console.log(''); -} - -module.exports = { - runBuild, - TARGETS, - BUILD_STEPS, - resolveBuildInvocation, -}; diff --git a/cli/lib/db-backup.js b/cli/lib/db-backup.js deleted file mode 100644 index 729cb5f9..00000000 --- a/cli/lib/db-backup.js +++ /dev/null @@ -1,67 +0,0 @@ -const fs = require('fs'); -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'); - const out = {}; - try { - const content = fs.readFileSync(envPath, 'utf8'); - for (const line of content.split('\n')) { - const m = line.match(/^([A-Z_]+)=(.*)$/); - if (m) out[m[1]] = m[2].trim().replace(/^['"]|['"]$/g, ''); - } - } catch { - // ignore - } - return out; -} - -async function runDbBackup(projectRoot, outputPath) { - const env = parseEnv(projectRoot); - const host = env.DB_HOST || '127.0.0.1'; - const port = env.DB_PORT || '3306'; - const user = env.DB_USER || 'root'; - const password = env.DB_PASSWD || env.DB_PASSWORD || 'root'; - const database = env.DB_DATABASE || 'reactpress'; - const out = - outputPath || - path.join(projectRoot, `reactpress-backup-${new Date().toISOString().replace(/[:.]/g, '-')}.sql`); - - const cmd = `mysqldump -h ${host} -P ${port} -u ${user} -p${password} ${database}`; - console.log(chalk.cyan('[reactpress]'), t('db.backup.to', { path: out })); - try { - const dump = execSync(cmd, { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 }); - fs.writeFileSync(out, dump, 'utf8'); - 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; - } -} - -module.exports = { runDbBackup }; diff --git a/cli/lib/dev-banner.js b/cli/lib/dev-banner.js deleted file mode 100644 index 876f7206..00000000 --- a/cli/lib/dev-banner.js +++ /dev/null @@ -1,72 +0,0 @@ -const { - brand, - icon, - ok, - divider, - padRight, - terminalWidth, - gradientText, - palette, - pulseBar, - statusLights, -} = require('../ui/theme'); -const { - loadClientSiteUrl, - loadServerSiteUrl, - getApiPrefix, - getHealthUrl, -} = require('./http'); -const { t } = require('./i18n'); - -function getDevUrls(projectRoot) { - const client = loadClientSiteUrl(projectRoot).replace(/\/$/, ''); - const server = loadServerSiteUrl(projectRoot).replace(/\/$/, ''); - const prefix = getApiPrefix(projectRoot).replace(/\/$/, '') || '/api'; - return { - site: client, - admin: `${client}/admin`, - api: `${server}${prefix}`, - swagger: `${server}${prefix}`, - health: getHealthUrl(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( - ` ${icon.ok} ${gradientText(t('devBanner.ready'), [palette.green, palette.accent], { bold: true })} ${statusLights('online')}` - ); - console.log(` ${brand.primary('╔' + '═'.repeat(w) + '╗')}`); - - if (!apiOnly) { - 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(` ${brand.primary('╚' + '═'.repeat(w) + '╝')}`); - console.log( - ` ${brand.dim(t('devBanner.hint'))} ${brand.muted('·')} ${brand.dim(t('devBanner.shortcuts'))}` - ); - console.log(''); -} - -module.exports = { getDevUrls, printDevReadyBanner }; diff --git a/cli/lib/dev.js b/cli/lib/dev.js deleted file mode 100644 index 838585e2..00000000 --- a/cli/lib/dev.js +++ /dev/null @@ -1,141 +0,0 @@ -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'); -} - -let apiChild; -let webChild; -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); -} - -async function buildToolkit(projectRoot) { - if (!hasToolkit(projectRoot)) return; - await runBuild('toolkit', 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', - cwd: projectRoot, - env: { - ...process.env, - REACTPRESS_ORIGINAL_CWD: projectRoot, - }, - }); - - apiChild.on('close', (code) => { - if (shuttingDown) { - process.exit(code ?? 0); - return; - } - if (webChild && !webChild.killed) webChild.kill('SIGINT'); - process.exit(code ?? 1); - }); -} - -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) { - spinner.fail(t('dev.apiTimeout', { seconds: API_READY_TIMEOUT_MS / 1000 })); - shutdown('SIGINT'); - process.exit(1); - } - spinner.succeed(t('dev.apiReady')); -} - -function spawnClient(projectRoot) { - webChild = spawn('pnpm', ['run', '--dir', './client', 'dev'], { - stdio: 'inherit', - shell: true, - cwd: projectRoot, - }); - - const clientUrl = loadClientSiteUrl(projectRoot); - waitForHttp(clientUrl, CLIENT_READY_TIMEOUT_MS).then((clientReady) => { - if (clientReady) { - printDevReadyBanner(projectRoot); - } else { - console.warn( - t('dev.clientSlow', { - seconds: CLIENT_READY_TIMEOUT_MS / 1000, - url: clientUrl, - }) - ); - } - }); - - webChild.on('close', (code) => { - 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}`); - } catch (err) { - console.error(t('dev.envFailed'), err.message || err); - console.error(formatDevFailureHint()); - process.exit(1); - } - - await buildToolkit(projectRoot); - await startDevStack(projectRoot); -} - -module.exports = { - runDev, - buildToolkit, - startDevStack, - detectProjectType, -}; diff --git a/cli/lib/docker.js b/cli/lib/docker.js deleted file mode 100644 index cf4932b3..00000000 --- a/cli/lib/docker.js +++ /dev/null @@ -1,275 +0,0 @@ -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() { - try { - execSync('docker info', { stdio: 'ignore' }); - return true; - } catch { - return false; - } -} - -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')); - 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) { - console.log(t('docker.starting')); - if (!isDockerRunning()) { - throw new Error(t('docker.notRunning')); - } - 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')); -} - -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) { - 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; - } - attempts += 1; - spinner.text = t('docker.waitingMysqlProgress', { attempts, max: maxAttempts }); - await new Promise((r) => setTimeout(r, 1000)); - } - spinner.fail(t('docker.mysqlTimeout')); - return false; -} - -async function dockerStartWithDev(projectRoot) { - startDockerServices(projectRoot); - 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); - - const apiRunner = path.join(__dirname, 'api-dev-runner.js'); - console.log(t('docker.startDevStack')); - console.log(t('docker.visitUrls')); - - return new Promise((resolve, reject) => { - const child = spawn( - 'npx', - [ - 'concurrently', - '-n', - 'api,web', - '-c', - 'blue,green', - `node "${apiRunner}"`, - 'pnpm run --dir ./client dev', - ], - { - stdio: 'inherit', - shell: true, - cwd: projectRoot, - env: { - ...process.env, - REACTPRESS_ORIGINAL_CWD: projectRoot, - }, - } - ); - - child.on('error', reject); - child.on('close', (code) => { - if (code !== 0) { - reject(Object.assign(new Error(t('docker.devProcessExit', { code })), { exitCode: code })); - return; - } - resolve(); - }); - }); -} - -/** - * 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(projectRoot); - return; - case 'down': - case 'stop': - stopDockerServices(projectRoot); - return; - case 'start': - await dockerStartWithDev(projectRoot); - return; - case 'restart': - stopDockerServices(projectRoot); - await new Promise((r) => setTimeout(r, 2000)); - startDockerServices(projectRoot); - await waitForMysql(projectRoot); - return; - 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]; - const args = ['logs', '-f']; - if (service) args.push(service); - runCompose(args, ctx); - return; - } - default: - throw new Error(t('docker.unknownCommand', { command })); - } -} - -module.exports = { - runDockerCommand, - startDockerServices, - stopDockerServices, - waitForMysql, - isDockerRunning, - resolveComposeContext, - pickDockerComposeCommand, - mysqldumpFromDbContainer, -}; diff --git a/cli/lib/doctor.js b/cli/lib/doctor.js deleted file mode 100644 index 13d0e8ea..00000000 --- a/cli/lib/doctor.js +++ /dev/null @@ -1,266 +0,0 @@ -const fs = require('fs'); -const net = require('net'); -const path = require('path'); -const { execSync } = require('child_process'); -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'); - -function checkNodeVersion() { - const major = parseInt(process.versions.node.split('.')[0], 10); - if (major >= 18) { - return { ok: true, message: `Node.js ${process.version}` }; - } - return { - ok: false, - message: t('doctor.nodeBad', { version: process.version }), - fix: t('doctor.nodeFix'), - }; -} - -function checkDocker() { - if (isDockerRunning()) { - return { ok: true, message: t('doctor.dockerOk') }; - } - return { - ok: false, - message: t('doctor.dockerBad'), - fix: t('doctor.dockerFix'), - }; -} - -function parseEnv(projectRoot) { - const envPath = path.join(projectRoot, '.env'); - const out = {}; - try { - const content = fs.readFileSync(envPath, 'utf8'); - for (const line of content.split('\n')) { - const m = line.match(/^([A-Z_]+)=(.*)$/); - if (m) out[m[1]] = m[2].trim().replace(/^['"]|['"]$/g, ''); - } - } catch { - // ignore - } - return out; -} - -function checkPort(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(1000, () => { - socket.destroy(); - resolve(false); - }); - }); -} - -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 })); - if (clientBusy) issues.push(t('doctor.portClientBusy', { port: clientPort })); - if (issues.length) { - return { - ok: false, - message: issues.join('; '), - fix: t('doctor.portFix'), - }; - } - return { - ok: true, - message: t('doctor.portOk', { apiPort, clientPort }), - }; -} - -async function checkDatabase(projectRoot) { - const env = parseEnv(projectRoot); - const host = env.DB_HOST || '127.0.0.1'; - const port = parseInt(env.DB_PORT || '3306', 10); - const user = env.DB_USER || 'root'; - const password = env.DB_PASSWD || env.DB_PASSWORD || 'root'; - const database = env.DB_DATABASE || 'reactpress'; - - return new Promise((resolve) => { - let mysql; - try { - mysql = require('mysql2/promise'); - } catch { - try { - mysql = require(path.join(projectRoot, 'server/node_modules/mysql2/promise')); - } catch { - resolve({ - ok: false, - message: t('doctor.dbNoMysql2'), - fix: t('doctor.dbMysql2Fix'), - }); - return; - } - } - - mysql - .createConnection({ host, port, user, password, database, connectTimeout: 5000 }) - .then(async (conn) => { - await conn.ping(); - await conn.end(); - resolve({ - ok: true, - message: t('doctor.dbOk', { host, port, database }), - }); - }) - .catch((err) => { - resolve({ - ok: false, - message: t('doctor.dbBad', { error: err.message }), - fix: t('doctor.dbFix'), - }); - }); - }); -} - -async function checkApiHealth(projectRoot) { - const healthUrl = getHealthUrl(projectRoot); - const result = await checkHealth(healthUrl); - if (result.ok) { - return { ok: true, message: t('doctor.apiOk', { url: healthUrl }) }; - } - return { - ok: false, - message: t('doctor.apiBad', { url: healthUrl }), - fix: t('doctor.apiFix'), - }; -} - -function checkPnpm() { - try { - const v = execSync('pnpm -v', { encoding: 'utf8' }).trim(); - return { ok: true, message: `pnpm ${v}` }; - } catch { - return { - ok: false, - message: t('doctor.pnpmBad'), - fix: t('doctor.pnpmFix'), - }; - } -} - -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 = [ - { name: 'Node.js', run: () => checkNodeVersion() }, - { name: 'pnpm', run: () => checkPnpm() }, - { - name: t('doctor.check.config'), - run: () => ({ - ok: env.config, - message: env.config ? t('doctor.configOk') : t('doctor.configBad'), - fix: t('doctor.configFix'), - }), - }, - { - name: t('doctor.check.env'), - run: () => ({ - ok: env.env, - message: env.env ? t('doctor.envOk') : t('doctor.envBad'), - fix: t('doctor.envFix'), - }), - }, - { 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( - ` ${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)}`); - - for (const { name, run } of checks) { - 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) { - fixes.push({ name, fix: result.fix }); - } - } - - 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(` ${ok(t('doctor.allPass'))}`); - } else { - 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; -} - -module.exports = { runDoctor }; diff --git a/cli/lib/http.js b/cli/lib/http.js deleted file mode 100644 index 403c486a..00000000 --- a/cli/lib/http.js +++ /dev/null @@ -1,182 +0,0 @@ -const fs = require('fs'); -const http = require('http'); -const path = require('path'); - -function loadServerSiteUrl(projectRoot) { - const envPath = path.join(projectRoot, '.env'); - try { - const content = fs.readFileSync(envPath, 'utf8'); - const match = content.match(/^SERVER_SITE_URL=(.+)$/m); - if (match) { - return match[1].trim().replace(/^['"]|['"]$/g, ''); - } - } catch { - // ignore - } - return 'http://localhost:3002'; -} - -function loadClientSiteUrl(projectRoot) { - const envPath = path.join(projectRoot, '.env'); - try { - const content = fs.readFileSync(envPath, 'utf8'); - const match = content.match(/^CLIENT_SITE_URL=(.+)$/m); - if (match) { - return match[1].trim().replace(/^['"]|['"]$/g, ''); - } - } catch { - // ignore - } - return 'http://localhost:3001'; -} - -function getApiPrefix(projectRoot) { - const envPath = path.join(projectRoot, '.env'); - try { - const content = fs.readFileSync(envPath, 'utf8'); - const match = content.match(/^SERVER_API_PREFIX=(.+)$/m); - if (match) { - return match[1].trim().replace(/^['"]|['"]$/g, ''); - } - } catch { - // ignore - } - return '/api'; -} - -function getHealthUrl(projectRoot) { - const base = loadServerSiteUrl(projectRoot).replace(/\/$/, ''); - const prefix = getApiPrefix(projectRoot).replace(/\/$/, ''); - return `${base}${prefix}/health`; -} - -function probeHttp(url, timeoutMs = 3000) { - return new Promise((resolve) => { - let parsed; - try { - parsed = new URL(url); - } catch { - resolve({ ok: false, statusCode: 0, data: null }); - return; - } - const port = parsed.port || (parsed.protocol === 'https:' ? 443 : 80); - const req = http.request( - { - hostname: parsed.hostname, - port, - path: parsed.pathname + (parsed.search || ''), - method: 'GET', - timeout: timeoutMs, - }, - (res) => { - let body = ''; - res.on('data', (chunk) => { - body += chunk; - }); - res.on('end', () => { - const ok = res.statusCode === 200; - let data = null; - try { - data = JSON.parse(body); - } catch { - // ignore - } - resolve({ ok, statusCode: res.statusCode, data }); - }); - } - ); - req.on('timeout', () => { - req.destroy(); - resolve({ ok: false, statusCode: 0, data: null }); - }); - 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; - try { - parsed = new URL(url); - } catch { - resolve(false); - return; - } - - const port = parsed.port || (parsed.protocol === 'https:' ? 443 : 80); - const req = http.request( - { - hostname: parsed.hostname, - port, - path: parsed.pathname || '/', - method: 'GET', - timeout: timeoutMs, - }, - (res) => resolve(res.statusCode > 0) - ); - - req.on('timeout', () => { - req.destroy(); - resolve(false); - }); - req.on('error', () => resolve(false)); - req.end(); - }); -} - -async function waitForHttp(url, timeoutMs = 120_000, intervalMs = 500) { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (await isHttpResponding(url)) { - return true; - } - await new Promise((r) => setTimeout(r, intervalMs)); - } - return false; -} - -module.exports = { - loadServerSiteUrl, - loadClientSiteUrl, - getApiPrefix, - getHealthUrl, - checkHealth, - isHttpResponding, - waitForHttp, -}; diff --git a/cli/lib/i18n/index.js b/cli/lib/i18n/index.js deleted file mode 100644 index afeedd02..00000000 --- a/cli/lib/i18n/index.js +++ /dev/null @@ -1,32 +0,0 @@ -const { STRINGS } = require('./strings'); - -function resolveLocale() { - const raw = process.env.REACTPRESS_LANG || process.env.LANG || 'en'; - const code = String(raw).split(/[._-]/)[0].toLowerCase(); - return code === 'zh' ? 'zh' : 'en'; -} - -let locale = resolveLocale(); - -/** - * @param {string} key - * @param {Record} [vars] - */ -function t(key, vars = {}) { - const table = STRINGS[locale] || STRINGS.en; - let text = table[key] ?? STRINGS.en[key] ?? key; - for (const [name, value] of Object.entries(vars)) { - text = text.replace(new RegExp(`\\{${name}\\}`, 'g'), String(value)); - } - return text; -} - -function getLocale() { - return locale; -} - -function setLocale(next) { - locale = next === 'zh' ? 'zh' : 'en'; -} - -module.exports = { t, getLocale, setLocale, resolveLocale }; diff --git a/cli/lib/i18n/strings.js b/cli/lib/i18n/strings.js deleted file mode 100644 index 4469cc24..00000000 --- a/cli/lib/i18n/strings.js +++ /dev/null @@ -1,846 +0,0 @@ -/** CLI user-facing strings — English first, Chinese via REACTPRESS_LANG=zh or zh LANG */ -const STRINGS = { - en: { - 'cli.description': 'ReactPress CLI — init, dev, build, deploy, and publish', - 'cli.init.description': 'Initialize project (.reactpress/config.json + .env + Docker MySQL)', - 'cli.init.directory': 'Project directory', - 'cli.init.force': 'Overwrite existing config', - 'cli.dev.description': 'Zero-config dev: env check + toolkit build + API + frontend', - 'cli.dev.apiOnly': 'API only (watch)', - 'cli.dev.clientOnly': 'Frontend only', - 'cli.server.description': 'Manage API service', - 'cli.server.start.description': 'Start API (wait until HTTP ready)', - 'cli.server.start.pm2': 'Start with PM2 (production)', - 'cli.server.start.bg': 'Start in background without waiting for HTTP', - 'cli.server.stop': 'Stop API', - 'cli.server.restart': 'Restart API', - 'cli.server.status': 'API status', - 'cli.client.description': 'Manage frontend', - 'cli.client.start': 'Start Next.js client', - 'cli.client.start.pm2': 'Start with PM2', - 'cli.build.description': 'Build production artifacts', - 'cli.docker.description': 'Docker dev environment (MySQL + nginx)', - 'cli.docker.up': 'Start Docker services and wait for MySQL', - 'cli.docker.down': 'Stop Docker services', - 'cli.docker.start': 'Start Docker + full-stack dev (API + frontend)', - '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', - 'cli.db.backup': 'Backup current project database with mysqldump', - 'cli.db.backup.output': 'Output SQL file path', - 'cli.publish.description': 'Build and publish npm packages (interactive)', - 'cli.publish.build': 'Build all packages only', - 'cli.publish.publish': 'Interactive publish', - 'cli.start.description': 'Production: start API + frontend', - 'cli.help.examples': 'Examples:', - 'cli.help.interactive': ' reactpress Interactive menu', - 'cli.help.dev': ' reactpress dev Zero-config full-stack dev', - 'cli.help.init': ' reactpress init --force Re-initialize config', - 'cli.help.server': ' reactpress server start Start API', - 'cli.help.status': ' reactpress status Combined status', - 'cli.help.doctor': ' reactpress doctor Environment diagnostics', - 'cli.help.docker': ' reactpress docker start Docker + full stack', - 'cli.help.build': ' reactpress build -t client Build one target (toolkit|server|client|docs|all)', - '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', - 'menu.doctor': 'Environment diagnostics (doctor)', - 'menu.devApi': 'API only (dev watch)', - 'menu.devClient': 'Frontend only', - 'menu.serverStart': 'Start API (production background)', - 'menu.serverStop': 'Stop API', - 'menu.serverRestart': 'Restart API', - 'menu.build': 'Build (toolkit → server → client)', - 'menu.buildTarget': 'What do you want to build?', - 'menu.buildAll': 'All (toolkit → server → client)', - 'menu.dockerStart': 'Docker dev (DB + nginx + full stack)', - 'menu.dockerUp': 'Docker: database only', - 'menu.dockerStop': 'Stop Docker services', - 'menu.openAdmin': 'Open admin in browser', - 'menu.publish': 'Publish npm packages (interactive)', - 'menu.exit': 'Exit', - 'menu.prompt': 'Choose an action', - 'menu.back': 'Return to main menu?', - 'menu.retry': 'Return to main menu and retry?', - 'menu.startingDev': 'Starting full-stack dev…', - 'menu.initProject': 'Initializing project…', - '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', - 'dev.apiReady': '[reactpress] API ready, starting frontend…', - 'dev.clientSlow': '[reactpress] Frontend not responding within {seconds}s; it may still be compiling. Visit {url} later', - 'dev.envFailed': '[reactpress] Environment setup failed:', - 'dev.toolkitFailed': 'toolkit build failed with exit code: {code}', - 'dev.nextSteps': 'Suggested next steps:', - 'dev.nextDoctor': ' → reactpress doctor Diagnostics', - 'dev.nextDocker': ' → reactpress docker up Start embedded MySQL', - 'dev.nextEnv': ' → Check DB_* and SERVER_SITE_URL in .env', - '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.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', - 'doctor.dockerBad': 'Docker is not running or unavailable', - 'doctor.dockerFix': 'Install and start Docker: https://docs.docker.com/get-docker/ , then run reactpress docker up; or use external MySQL in config.json', - 'doctor.portApiBusy': 'API port {port} is in use', - 'doctor.portClientBusy': 'Frontend port {port} is in use', - 'doctor.portFix': 'Change SERVER_PORT / CLIENT_PORT in .env, or stop the blocking process', - 'doctor.portOk': 'Ports {apiPort} (API) and {clientPort} (frontend) are available', - 'doctor.dbNoMysql2': 'mysql2 not installed; cannot check database', - 'doctor.dbMysql2Fix': 'Run pnpm install at monorepo root', - 'doctor.dbOk': 'MySQL {host}:{port}/{database} connected', - 'doctor.dbBad': 'Database connection failed: {error}', - 'doctor.dbFix': 'Run reactpress docker up or check DB_* in .env', - 'doctor.apiOk': 'API health check passed ({url})', - 'doctor.apiBad': 'API health check failed ({url})', - 'doctor.apiFix': 'Run reactpress server start or reactpress dev', - 'doctor.pnpmBad': 'pnpm not found', - 'doctor.pnpmFix': 'npm i -g pnpm, or enable corepack at monorepo root', - 'doctor.check.config': 'Config file', - 'doctor.check.env': 'Environment', - 'doctor.check.ports': 'Ports', - 'doctor.check.database': 'Database', - 'doctor.check.api': 'API health', - 'doctor.configOk': '.reactpress/config.json exists', - 'doctor.configBad': 'Missing .reactpress/config.json', - 'doctor.configFix': 'Run reactpress init', - '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.', - '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.configBad': 'not initialized', - 'status.envOk': '.env', - 'status.envBad': 'missing .env', - 'status.apiOnline': 'online', - 'status.apiOffline': 'offline', - 'status.apiUnreachable': '{url} (offline or not started)', - 'status.dbUp': 'connected', - 'status.dbDown': 'unavailable', - 'status.pidRunning': '(running)', - '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).', - 'bootstrap.initFailed': 'Initialization failed', - 'bootstrap.cliInitFailed': 'reactpress-cli init failed', - 'bootstrap.dbPendingShort': 'Database not ready', - 'bootstrap.dbNotReady': '{message}. Tip: start Docker and run reactpress docker up, or run reactpress doctor', - 'bootstrap.dbReady': 'Database is ready', - 'db.backup.to': 'Backing up database to {path}', - 'db.backup.done': 'Backup complete', - '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', - 'common.none': '(none)', - 'common.unknownError': 'Unknown error', - '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.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': 'bundled API (@fecommunity/reactpress)', - 'lifecycle.pidFile': ' PID file: {path}', - 'lifecycle.recordedPid': ' Recorded PID: {pid}', - 'lifecycle.processAlive': ' Process alive: {alive}', - 'lifecycle.httpStatus': ' HTTP ({url}): {status}', - 'lifecycle.httpReachable': 'reachable', - 'lifecycle.httpUnreachable': 'unreachable', - 'lifecycle.unknownCommand': 'Unknown lifecycle command: {command}', - 'docker.stopping': '[reactpress] Stopping Docker services…', - 'docker.stopped': '[reactpress] Docker services stopped.', - 'docker.stopFailed': '[reactpress] Failed to stop Docker:', - 'docker.starting': '[reactpress] Starting Docker services…', - 'docker.notRunning': 'Docker is not running. Please start Docker Desktop first.', - 'docker.started': '[reactpress] Docker services started.', - 'docker.waitingMysql': '[reactpress] Waiting for MySQL…', - 'docker.mysqlReady': '[reactpress] MySQL is ready.', - 'docker.waitingMysqlProgress': '[reactpress] Waiting for MySQL… ({attempts}/{max})', - '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 (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.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': '[{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)', - 'build.label.docs': 'Documentation (docs)', - 'pm2.startFailed': '[reactpress] PM2 failed to start API:', - 'pm2.exitCode': 'PM2 exited with code {code}', - 'spawn.commandFailed': 'Command failed ({command}): exit code {code}', - 'spawn.exitCode': 'Exit code {code}', - 'shim.deprecated': '\n[deprecated] reactpress-cli will be removed in 3.1. Use instead:\n npm i -g @fecommunity/reactpress\n reactpress init · reactpress dev · reactpress doctor\n', - 'server.help.invokedBy': ' (usually invoked by reactpress server start)', - 'publish.pkg.main': 'ReactPress 3.0 main package — single entry (init / dev / doctor / publish)', - 'publish.pkg.server': 'NestJS backend API (deprecated — use bundled API in reactpress-cli)', - 'bundle.cli.description': 'Zero-config init and manage ReactPress CMS & blog server', - 'bundle.cli.cwd': 'ReactPress project directory (default: current working directory)', - 'bundle.cli.init.description': 'One-click init ReactPress CMS & blog (zero config)', - 'bundle.cli.init.directory': 'Project directory', - 'bundle.cli.init.force': 'Overwrite existing config', - 'bundle.cli.start.description': 'Start server (prepares database automatically)', - 'bundle.cli.stop.description': 'Stop server', - 'bundle.cli.stop.database': 'Also stop embedded database container', - 'bundle.cli.restart.description': 'Restart server', - 'bundle.cli.status.description': 'Server and database status', - 'bundle.cli.config.description': 'View or update config (--apply to restart)', - 'bundle.cli.config.key': 'Config key, e.g. server.port', - 'bundle.cli.config.value': 'New value', - 'bundle.cli.config.list': 'List all config keys', - 'bundle.cli.config.apply': 'Restart automatically after update', - 'bundle.cli.unknownCommand': 'Unknown command: {command}', - 'bundle.cmd.init.spinner': 'Initializing ReactPress project…', - 'bundle.cmd.init.succeed': 'Initialization complete', - 'bundle.cmd.init.fail': 'Initialization failed', - 'bundle.cmd.init.projectDir': 'Project directory: {path}', - 'bundle.cmd.init.nextStep': 'Next: reactpress-cli start', - 'bundle.cmd.notProject': 'This directory is not a ReactPress project.', - 'bundle.cmd.notProjectInit': 'This directory is not a ReactPress project. Run reactpress-cli init first.', - 'bundle.cmd.start.spinner': 'Preparing database and server…', - 'bundle.cmd.start.succeed': 'Server started', - 'bundle.cmd.start.fail': 'Start failed', - 'bundle.cmd.stop.spinner': 'Stopping server…', - 'bundle.cmd.stop.succeed': 'Stopped', - 'bundle.cmd.stop.fail': 'Stop failed', - 'bundle.cmd.restart.spinner': 'Restarting server…', - 'bundle.cmd.restart.succeed': 'Restart complete', - 'bundle.cmd.restart.fail': 'Restart failed', - 'bundle.cmd.status.title': 'Service status', - 'bundle.cmd.status.project': 'Project: {path}', - 'bundle.cmd.status.service': 'Service: {status}{pid}', - 'bundle.cmd.status.running': 'running', - 'bundle.cmd.status.stopped': 'stopped', - 'bundle.cmd.status.url': 'URL: {url}', - 'bundle.cmd.status.database': 'Database: {status} ({mode})', - 'bundle.cmd.status.dbReady': 'ready', - 'bundle.cmd.status.dbNotReady': 'not ready', - 'bundle.cmd.config.title': 'Configuration', - 'bundle.cmd.config.keyRequired': 'Specify a config key, e.g.: reactpress-cli config server.port 3003', - 'bundle.cmd.config.listHint': 'Use --list to show all keys', - 'bundle.cmd.config.updated': 'Updated {key} = {value}', - 'bundle.cmd.config.restartSpinner': 'Restarting to apply config…', - 'bundle.cmd.config.applied': 'Config applied', - 'bundle.cmd.config.restartFail': 'Restart failed', - 'bundle.cmd.config.restartManual': 'Run reactpress-cli restart manually', - 'bundle.cmd.config.applyHint': 'Run reactpress-cli restart or config --apply to apply changes', - 'bundle.service.init.alreadyProject': 'Directory is already a ReactPress project. Use --force to overwrite config.', - 'bundle.service.init.dbPending': 'Project created, but database is not ready: {message}. Run reactpress-cli start later.', - 'bundle.service.init.complete': 'ReactPress project initialized. Run reactpress-cli start to launch the server.', - 'bundle.service.init.templateMissing': 'Template file missing: {path}', - 'bundle.service.config.notFound': 'ReactPress project not found. Run reactpress-cli init first.', - 'bundle.service.config.keyMissing': 'Config key does not exist: {key}', - 'bundle.service.server.alreadyRunning': 'Server already running (PID {pid}), visit {url}', - 'bundle.service.server.portBusy': 'Port {port} is in use; ReactPress cannot bind. If you started via Docker Compose, run: docker stop reactpress_server. Or change server.port in .reactpress/config.json.', - 'bundle.service.server.cannotStart': 'Could not start ReactPress server process.', - 'bundle.service.server.noHttp': 'Server process started (PID {pid}) but no HTTP response at {url}. Check port conflicts or DB_* in .env.', - 'bundle.service.server.started': 'ReactPress server started at {url}', - 'bundle.service.server.stopped': 'ReactPress server stopped.', - 'bundle.service.server.cannotStopPid': 'Could not stop process PID {pid}', - 'bundle.service.database.dockerMissing': 'Docker not detected. Install and start Docker, or set database.mode to external with an existing MySQL.', - 'bundle.service.database.portSwitched': 'Host port {previous} was in use; switched to {port} (.env updated)', - 'bundle.service.database.portBindRetry': 'Port {port} bind failed, trying another port…', - 'bundle.service.database.containerStartFailed': 'Failed to start database container: {detail}', - 'bundle.service.database.credsMismatch': 'Database container is on port {port} but user "{user}" cannot connect (volume credentials differ from .env). Run: cd .reactpress && docker compose down -v && cd .. && reactpress-cli start', - 'bundle.service.database.connectionTimeout': 'Database container started but connection timed out. Run: docker logs {container}', - 'bundle.service.database.cannotConnect': 'Cannot connect to database {host}:{port}. Check DB_* in .env.', - 'bundle.serverBundle.missing': 'Bundled server is missing. Reinstall reactpress-cli.', - 'bundle.serverBundle.installFailed': 'Bundled server dependency install failed. In the reactpress-cli install dir run: cd server && npm install --omit=dev --no-bin-links', - 'bundle.serverBundle.notBuilt': 'Bundled server is not built or dependencies are incomplete. Run npm run build:server (dev) or reinstall reactpress-cli.', - 'bundle.port.notFound': 'No available port in range {start}-{end}', - }, - zh: { - 'cli.description': 'ReactPress 全栈 CLI — 初始化、开发、构建、部署、发布', - 'cli.init.description': '初始化项目 (.reactpress/config.json + .env + Docker MySQL)', - 'cli.init.directory': '项目目录', - 'cli.init.force': '覆盖已有配置', - 'cli.dev.description': '零配置开发: 环境检查 + toolkit 构建 + API + 前端', - 'cli.dev.apiOnly': '仅启动 API (watch)', - 'cli.dev.clientOnly': '仅启动前端', - 'cli.server.description': '管理 API 服务', - 'cli.server.start.description': '启动 API(等待 HTTP 就绪)', - 'cli.server.start.pm2': '使用 PM2 启动(生产)', - 'cli.server.start.bg': '后台启动,不等待 HTTP', - 'cli.server.stop': '停止 API', - 'cli.server.restart': '重启 API', - 'cli.server.status': '查看 API 状态', - 'cli.client.description': '管理前端', - 'cli.client.start': '启动 Next.js 客户端', - 'cli.client.start.pm2': '使用 PM2 启动', - 'cli.build.description': '构建生产产物', - 'cli.docker.description': 'Docker 开发环境 (MySQL + nginx)', - 'cli.docker.up': '仅启动 Docker 服务并等待 MySQL', - 'cli.docker.down': '停止 Docker 服务', - 'cli.docker.start': '启动 Docker + 全栈开发 (API + 前端)', - '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': '数据库运维', - 'cli.db.backup': '使用 mysqldump 备份当前项目数据库', - 'cli.db.backup.output': '输出 SQL 文件路径', - 'cli.publish.description': '构建并发布 npm 包 (交互式)', - 'cli.publish.build': '仅构建所有包', - 'cli.publish.publish': '交互式发布', - 'cli.start.description': '生产模式: 启动 API + 前端', - 'cli.help.examples': '示例:', - 'cli.help.interactive': ' reactpress 交互式菜单', - 'cli.help.dev': ' reactpress dev 零配置全栈开发', - 'cli.help.init': ' reactpress init --force 重新初始化配置', - 'cli.help.server': ' reactpress server start 启动 API', - 'cli.help.status': ' reactpress status 综合状态', - 'cli.help.doctor': ' reactpress doctor 环境诊断', - 'cli.help.docker': ' reactpress docker start Docker + 全栈', - 'cli.help.build': ' reactpress build -t client 构建指定目标 (toolkit|server|client|docs|all)', - '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': '查看项目状态', - 'menu.doctor': '环境诊断 (doctor)', - 'menu.devApi': '仅启动 API (开发 watch)', - 'menu.devClient': '仅启动前端', - 'menu.serverStart': '启动 API (后台生产)', - 'menu.serverStop': '停止 API', - 'menu.serverRestart': '重启 API', - 'menu.build': '构建 (toolkit → server → client)', - 'menu.buildTarget': '选择要构建的目标', - 'menu.buildAll': '全部 (toolkit → server → client)', - 'menu.dockerStart': 'Docker 开发环境 (DB + nginx + 全栈)', - 'menu.dockerUp': 'Docker 仅启动数据库', - 'menu.dockerStop': '停止 Docker 服务', - 'menu.openAdmin': '在浏览器打开管理后台', - 'menu.publish': '发布 npm 包 (交互式)', - 'menu.exit': '退出', - 'menu.prompt': '选择操作', - 'menu.back': '返回主菜单?', - 'menu.retry': '返回主菜单重试?', - 'menu.startingDev': '启动全栈开发…', - 'menu.initProject': '初始化项目…', - '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', - 'dev.apiReady': '[reactpress] API 已就绪,正在启动前端…', - 'dev.clientSlow': '[reactpress] 前端在 {seconds}s 内未响应,可能仍在编译。稍后访问 {url}', - 'dev.envFailed': '[reactpress] 环境准备失败:', - 'dev.toolkitFailed': 'toolkit 构建失败,退出码: {code}', - 'dev.nextSteps': '下一步建议:', - 'dev.nextDoctor': ' → reactpress doctor 环境诊断', - 'dev.nextDocker': ' → reactpress docker up 启动嵌入式 MySQL', - 'dev.nextEnv': ' → 检查 .env 中 DB_* 与 SERVER_SITE_URL', - '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.shortcuts': 'Ctrl+C 停止', - 'devBanner.allSystemsGo': '一切就绪', - 'doctor.nodeBad': 'Node.js {version}(需要 ≥ 18)', - 'doctor.nodeFix': '请安装 Node.js 18+:https://nodejs.org/', - 'doctor.dockerOk': 'Docker 引擎可用', - 'doctor.dockerBad': 'Docker 未运行或不可用', - 'doctor.dockerFix': '安装并启动 Docker:https://docs.docker.com/get-docker/ ,然后运行 reactpress docker up;或改 config.json 使用外部 MySQL', - 'doctor.portApiBusy': 'API 端口 {port} 已被占用', - 'doctor.portClientBusy': '前端端口 {port} 已被占用', - 'doctor.portFix': '修改 .env 中 SERVER_PORT / CLIENT_PORT,或停止占用进程', - 'doctor.portOk': '端口 {apiPort}(API)、{clientPort}(前端)可用', - 'doctor.dbNoMysql2': '未安装 mysql2,无法检测数据库', - 'doctor.dbMysql2Fix': '在 monorepo 根目录执行 pnpm install', - 'doctor.dbOk': 'MySQL {host}:{port}/{database} 连通', - 'doctor.dbBad': '数据库连接失败: {error}', - 'doctor.dbFix': '运行 reactpress docker up 或检查 .env 中 DB_* 配置', - 'doctor.apiOk': 'API 健康检查通过 ({url})', - 'doctor.apiBad': 'API 未响应健康检查 ({url})', - 'doctor.apiFix': '运行 reactpress server start 或 reactpress dev', - 'doctor.pnpmBad': '未检测到 pnpm', - 'doctor.pnpmFix': 'npm i -g pnpm,或在 monorepo 根目录使用 corepack enable', - 'doctor.check.config': '配置文件', - 'doctor.check.env': '环境变量', - 'doctor.check.ports': '端口', - 'doctor.check.database': '数据库', - 'doctor.check.api': 'API 健康', - 'doctor.configOk': '.reactpress/config.json 存在', - 'doctor.configBad': '缺少 .reactpress/config.json', - 'doctor.configFix': '运行 reactpress init', - 'doctor.envOk': '.env 存在', - 'doctor.envBad': '缺少 .env', - 'doctor.envFix': '运行 reactpress init 或 reactpress config --apply', - '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.configBad': '未初始化', - 'status.envOk': '.env', - 'status.envBad': '缺少 .env', - 'status.apiOnline': '在线', - 'status.apiOffline': '离线', - 'status.apiUnreachable': '{url} (离线或未启动)', - 'status.dbUp': '连通', - 'status.dbDown': '不可用', - 'status.pidRunning': '(运行中)', - '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 开发环境已就绪(配置 + 数据库)。', - 'bootstrap.initFailed': '初始化失败', - 'bootstrap.cliInitFailed': 'reactpress-cli init 失败', - 'bootstrap.dbPendingShort': '数据库未就绪', - 'bootstrap.dbNotReady': '{message}。建议:启动 Docker 后运行 reactpress docker up,或执行 reactpress doctor', - 'bootstrap.dbReady': '数据库已就绪', - 'db.backup.to': '备份数据库到 {path}', - 'db.backup.done': '备份完成', - 'db.backup.viaDocker': '本机未找到 mysqldump,改用 Docker 内 db 容器的 mysqldump…', - 'db.backup.fail': - 'mysqldump 失败:请安装 MySQL 客户端(如 brew install mysql-client),或确保 Docker 数据库已运行以便自动在容器内备份', - 'common.done': '完成', - 'common.yes': '是', - 'common.no': '否', - 'common.none': '(无)', - 'common.unknownError': '未知错误', - 'lifecycle.apiStopped': '[reactpress] 已停止 API 进程 (pid {pid})', - 'lifecycle.stopPidFailed': '[reactpress] 停止 pid {pid} 失败:', - 'lifecycle.apiAlreadyRunning': '[reactpress] API 已在运行 (pid {pid})', - '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': '内置 API (@fecommunity/reactpress)', - 'lifecycle.pidFile': ' PID 文件: {path}', - 'lifecycle.recordedPid': ' 记录 PID: {pid}', - 'lifecycle.processAlive': ' 进程存活: {alive}', - 'lifecycle.httpStatus': ' HTTP ({url}): {status}', - 'lifecycle.httpReachable': '可访问', - 'lifecycle.httpUnreachable': '不可访问', - 'lifecycle.unknownCommand': '未知 lifecycle 命令: {command}', - 'docker.stopping': '[reactpress] 正在停止 Docker 服务…', - 'docker.stopped': '[reactpress] Docker 服务已停止。', - 'docker.stopFailed': '[reactpress] 停止 Docker 失败:', - 'docker.starting': '[reactpress] 正在启动 Docker 服务…', - 'docker.notRunning': 'Docker 未运行,请先启动 Docker Desktop。', - 'docker.started': '[reactpress] Docker 服务已启动。', - 'docker.waitingMysql': '[reactpress] 等待 MySQL 就绪…', - 'docker.mysqlReady': '[reactpress] MySQL 已就绪。', - 'docker.waitingMysqlProgress': '[reactpress] 等待 MySQL… ({attempts}/{max})', - 'docker.mysqlTimeout': '[reactpress] MySQL 在超时时间内未就绪。', - 'docker.mysqlNotReady': 'MySQL 未就绪', - 'docker.startDevStack': '[reactpress] 启动 API + 前端 (Docker MySQL)…', - '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.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': '[{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)', - 'build.label.docs': '文档 (docs)', - 'pm2.startFailed': '[reactpress] PM2 启动 API 失败:', - 'pm2.exitCode': 'PM2 退出码 {code}', - 'spawn.commandFailed': '命令失败 ({command}): 退出码 {code}', - 'spawn.exitCode': '退出码 {code}', - 'shim.deprecated': '\n[deprecated] reactpress-cli 将在 3.1 移除。请改用:\n npm i -g @fecommunity/reactpress\n reactpress init · reactpress dev · reactpress doctor\n', - 'server.help.invokedBy': ' (通常由 reactpress server start 调用)', - 'publish.pkg.main': 'ReactPress 3.0 主包 — 唯一入口 (init / dev / doctor / publish)', - 'publish.pkg.server': 'NestJS 后端 API (deprecated — 使用 reactpress-cli 内置 API)', - 'bundle.cli.description': '零配置初始化与管理 ReactPress CMS & 博客服务器', - 'bundle.cli.cwd': 'ReactPress 项目目录(默认:当前工作目录)', - 'bundle.cli.init.description': '一键初始化 ReactPress CMS & 博客服务器(零配置)', - 'bundle.cli.init.directory': '项目目录', - 'bundle.cli.init.force': '覆盖已有配置', - 'bundle.cli.start.description': '启动服务器(自动准备数据库)', - 'bundle.cli.stop.description': '停止服务器', - 'bundle.cli.stop.database': '同时停止嵌入式数据库容器', - 'bundle.cli.restart.description': '重启服务器', - 'bundle.cli.status.description': '查看服务与数据库状态', - 'bundle.cli.config.description': '查看或更新配置(更新后可用 --apply 重启生效)', - 'bundle.cli.config.key': '配置键,如 server.port', - 'bundle.cli.config.value': '新值', - 'bundle.cli.config.list': '列出所有配置', - 'bundle.cli.config.apply': '更新后自动重启服务', - 'bundle.cli.unknownCommand': '未知命令: {command}', - 'bundle.cmd.init.spinner': '正在初始化 ReactPress 项目…', - 'bundle.cmd.init.succeed': '初始化完成', - 'bundle.cmd.init.fail': '初始化失败', - 'bundle.cmd.init.projectDir': '项目目录: {path}', - 'bundle.cmd.init.nextStep': '下一步: reactpress-cli start', - 'bundle.cmd.notProject': '当前目录不是 ReactPress 项目。', - 'bundle.cmd.notProjectInit': '当前目录不是 ReactPress 项目。请先运行 reactpress-cli init。', - 'bundle.cmd.start.spinner': '正在准备数据库与服务…', - 'bundle.cmd.start.succeed': '服务已启动', - 'bundle.cmd.start.fail': '启动失败', - 'bundle.cmd.stop.spinner': '正在停止服务…', - 'bundle.cmd.stop.succeed': '已停止', - 'bundle.cmd.stop.fail': '停止失败', - 'bundle.cmd.restart.spinner': '正在重启服务…', - 'bundle.cmd.restart.succeed': '重启完成', - 'bundle.cmd.restart.fail': '重启失败', - 'bundle.cmd.status.title': '服务状态', - 'bundle.cmd.status.project': '项目: {path}', - 'bundle.cmd.status.service': '服务: {status}{pid}', - 'bundle.cmd.status.running': '运行中', - 'bundle.cmd.status.stopped': '已停止', - 'bundle.cmd.status.url': '地址: {url}', - 'bundle.cmd.status.database': '数据库: {status} ({mode})', - 'bundle.cmd.status.dbReady': '就绪', - 'bundle.cmd.status.dbNotReady': '未就绪', - 'bundle.cmd.config.title': '配置项', - 'bundle.cmd.config.keyRequired': '请指定配置键,例如: reactpress-cli config server.port 3003', - 'bundle.cmd.config.listHint': '使用 --list 查看所有配置项', - 'bundle.cmd.config.updated': '已更新 {key} = {value}', - 'bundle.cmd.config.restartSpinner': '正在重启以使配置生效…', - 'bundle.cmd.config.applied': '配置已应用', - 'bundle.cmd.config.restartFail': '重启失败', - 'bundle.cmd.config.restartManual': '请手动运行 reactpress-cli restart', - 'bundle.cmd.config.applyHint': '运行 reactpress-cli restart 或 config --apply 使配置生效', - 'bundle.service.init.alreadyProject': '目录已是 ReactPress 项目。使用 --force 覆盖配置。', - 'bundle.service.init.dbPending': '项目已创建,但数据库未就绪: {message}。可稍后运行 reactpress-cli start。', - 'bundle.service.init.complete': 'ReactPress 项目初始化完成。运行 reactpress-cli start 启动服务。', - 'bundle.service.init.templateMissing': '模板文件缺失: {path}', - 'bundle.service.config.notFound': '未找到 ReactPress 项目。请先运行 reactpress-cli init 初始化。', - 'bundle.service.config.keyMissing': '配置项不存在: {key}', - 'bundle.service.server.alreadyRunning': '服务已在运行 (PID {pid}),访问 {url}', - 'bundle.service.server.portBusy': '端口 {port} 已被占用,ReactPress 无法绑定。若曾用 Docker Compose 启动过 ReactPress,请执行: docker stop reactpress_server。也可在 .reactpress/config.json 中修改 server.port。', - 'bundle.service.server.cannotStart': '无法启动 ReactPress 服务进程。', - 'bundle.service.server.noHttp': '服务进程已启动 (PID {pid}),但 {url} 无 HTTP 响应。请检查端口占用或 .env 数据库配置。', - 'bundle.service.server.started': 'ReactPress 服务已启动,访问 {url}', - 'bundle.service.server.stopped': 'ReactPress 服务已停止。', - 'bundle.service.server.cannotStopPid': '无法停止进程 PID {pid}', - 'bundle.service.database.dockerMissing': '未检测到 Docker。请安装并启动 Docker,或将 database.mode 设为 external 并使用已有 MySQL。', - 'bundle.service.database.portSwitched': '宿主机端口 {previous} 已被占用,已改用 {port}(已更新 .env)', - 'bundle.service.database.portBindRetry': '端口 {port} 绑定失败,正在尝试其他端口…', - 'bundle.service.database.containerStartFailed': '启动数据库容器失败: {detail}', - 'bundle.service.database.credsMismatch': '数据库容器已在端口 {port} 运行,但账号「{user}」无法连接(数据卷中的凭证与 .env 不一致)。请在项目目录执行: cd .reactpress && docker compose down -v && cd .. && reactpress-cli start', - 'bundle.service.database.connectionTimeout': '数据库容器已启动,但连接超时。请执行 docker logs {container} 查看详情。', - 'bundle.service.database.cannotConnect': '无法连接数据库 {host}:{port},请检查 .env 中的 DB_* 配置。', - 'bundle.serverBundle.missing': '内置服务端缺失,请重新安装 reactpress-cli。', - 'bundle.serverBundle.installFailed': '内置服务端依赖安装失败。请在 reactpress-cli 安装目录下手动执行: cd server && npm install --omit=dev --no-bin-links', - 'bundle.serverBundle.notBuilt': '内置服务端未构建或依赖不完整。请运行 npm run build:server(开发)或重新安装 reactpress-cli。', - 'bundle.port.notFound': '在 {start}-{end} 范围内未找到可用端口', - }, -}; - -module.exports = { STRINGS }; diff --git a/cli/lib/lifecycle.js b/cli/lib/lifecycle.js deleted file mode 100644 index e1c2ce7b..00000000 --- a/cli/lib/lifecycle.js +++ /dev/null @@ -1,190 +0,0 @@ -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 { 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); - return true; - } catch (err) { - console.error(t('dev.envFailed'), err.message || err); - return false; - } -} - -function stopApi(projectRoot) { - const pid = readPid(projectRoot); - if (pid && isProcessRunning(pid)) { - try { - process.kill(pid, 'SIGTERM'); - console.log(t('lifecycle.apiStopped', { pid })); - } catch (err) { - console.warn(t('lifecycle.stopPidFailed', { pid }), err.message); - } - } - clearPidFile(projectRoot); -} - -async function startApi(projectRoot, { wait = true } = {}) { - if (!(await ensureConfig(projectRoot))) { - return 1; - } - - const existing = readPid(projectRoot); - if (existing && isProcessRunning(existing)) { - console.log(t('lifecycle.apiAlreadyRunning', { pid: existing })); - return 0; - } - clearPidFile(projectRoot); - - if (!canStartLocalApi(projectRoot)) { - console.error(t('lifecycle.noServerAvailable')); - return 1; - } - - if (isUsingMonorepoServer(projectRoot)) { - console.log(t('lifecycle.startingLocalApi')); - } else { - console.log(t('lifecycle.startingBundledApi')); - } - - const child = spawn(process.execPath, [getServerBin(projectRoot)], { - cwd: getServerDir(projectRoot), - detached: true, - stdio: 'ignore', - env: { - ...process.env, - REACTPRESS_ORIGINAL_CWD: projectRoot, - }, - }); - - child.unref(); - writePid(projectRoot, child.pid); - console.log(t('lifecycle.apiStartedBg', { pid: child.pid })); - - if (!wait) { - return 0; - } - - 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) { - spinner.fail(t('lifecycle.apiTimeout120', { url: serverUrl })); - return 1; - } - spinner.succeed(t('lifecycle.apiReady', { url: serverUrl })); - return 0; -} - -async function statusApi(projectRoot) { - const pid = readPid(projectRoot); - const serverUrl = loadServerSiteUrl(projectRoot); - const { isHttpResponding } = require('./http'); - const httpOk = await isHttpResponding(serverUrl); - - const source = isUsingMonorepoServer(projectRoot) - ? t('lifecycle.source.monorepo') - : t('lifecycle.source.bundle'); - - console.log(t('lifecycle.apiStatusTitle')); - console.log(t('lifecycle.source', { source })); - console.log(t('lifecycle.pidFile', { path: getPidFile(projectRoot) })); - console.log( - t('lifecycle.recordedPid', { - pid: pid ?? t('common.none'), - }) - ); - console.log( - t('lifecycle.processAlive', { - alive: pid - ? isProcessRunning(pid) - ? t('common.yes') - : t('common.no') - : '—', - }) - ); - console.log( - t('lifecycle.httpStatus', { - url: serverUrl, - status: httpOk ? t('lifecycle.httpReachable') : t('lifecycle.httpUnreachable'), - }) - ); -} - -async function runLifecycleCommand(command, projectRoot = ensureOriginalCwd()) { - switch (command) { - case 'start': - return startApi(projectRoot, { wait: true }); - case 'start:bg': - return startApi(projectRoot, { wait: false }); - case 'stop': - stopApi(projectRoot); - return 0; - case 'restart': - stopApi(projectRoot); - await waitForPortFree(parseServerPort(projectRoot)); - await new Promise((r) => setTimeout(r, 400)); - return startApi(projectRoot, { wait: true }); - case 'status': - await statusApi(projectRoot); - return 0; - default: - throw new Error(t('lifecycle.unknownCommand', { command })); - } -} - -module.exports = { - startApi, - stopApi, - statusApi, - runLifecycleCommand, -}; diff --git a/cli/lib/nginx.js b/cli/lib/nginx.js deleted file mode 100644 index e056cab8..00000000 --- a/cli/lib/nginx.js +++ /dev/null @@ -1,342 +0,0 @@ -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 deleted file mode 100644 index 42351d46..00000000 --- a/cli/lib/paths.js +++ /dev/null @@ -1,108 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { ensureOriginalCwd, getMonorepoRoot } = require('./root'); - -function resolveProjectRoot(projectRoot) { - return path.resolve(projectRoot || ensureOriginalCwd()); -} - -function getMonorepoServerDir(projectRoot) { - return path.join(resolveProjectRoot(projectRoot), 'server'); -} - -function hasMonorepoServerSource(projectRoot) { - return fs.existsSync( - path.join(getMonorepoServerDir(projectRoot), 'src', 'main.ts') - ); -} - -function getCliPackageRoot() { - const ownRoot = path.join(__dirname, '..'); - if (fs.existsSync(path.join(ownRoot, 'dist', 'index.js'))) { - return ownRoot; - } - try { - return path.dirname( - require.resolve('@fecommunity/reactpress-cli-core/package.json') - ); - } catch { - return path.dirname(require.resolve('@fecommunity/reactpress-cli/package.json')); - } -} - -function getBundledServerDir() { - return path.join(getCliPackageRoot(), 'server'); -} - -function hasBundledServerBuild() { - return fs.existsSync(path.join(getBundledServerDir(), 'dist', 'main.js')); -} - -function getServerDir(projectRoot) { - if (hasMonorepoServerSource(projectRoot)) { - return getMonorepoServerDir(projectRoot); - } - return getBundledServerDir(); -} - -function getServerBin(projectRoot) { - return path.join(getServerDir(projectRoot), 'bin', 'reactpress-server.js'); -} - -function getSwaggerPath(projectRoot) { - return path.join(getServerDir(projectRoot), 'public', 'swagger.json'); -} - -function getServerMain(projectRoot) { - return path.join(getServerDir(projectRoot), 'dist', 'main.js'); -} - -function isUsingMonorepoServer(projectRoot) { - return hasMonorepoServerSource(projectRoot); -} - -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(resolveProjectRoot(projectRoot), '.reactpress', 'server.pid'); -} - -module.exports = { - getMonorepoRoot, - resolveProjectRoot, - getMonorepoServerDir, - hasMonorepoServerSource, - hasBundledServerBuild, - isUsingMonorepoServer, - canStartLocalApi, - getCliPackageRoot, - getBundledServerDir, - getServerDir, - getServerBin, - getSwaggerPath, - getServerMain, - getClientBin, - getPidFile, -}; diff --git a/cli/lib/pm2.js b/cli/lib/pm2.js deleted file mode 100644 index 1473b061..00000000 --- a/cli/lib/pm2.js +++ /dev/null @@ -1,32 +0,0 @@ -const { spawn } = require('child_process'); -const { getServerBin, getServerDir } = require('./paths'); -const { ensureOriginalCwd } = require('./root'); -const { t } = require('./i18n'); - -function startApiWithPm2(projectRoot = ensureOriginalCwd()) { - return new Promise((resolve, reject) => { - const child = spawn(process.execPath, [getServerBin(projectRoot), '--pm2'], { - stdio: 'inherit', - cwd: getServerDir(projectRoot), - env: { - ...process.env, - REACTPRESS_ORIGINAL_CWD: projectRoot, - }, - }); - - child.on('error', (error) => { - console.error(t('pm2.startFailed'), error); - reject(error); - }); - - child.on('close', (code) => { - if (code !== 0) { - reject(Object.assign(new Error(t('pm2.exitCode', { code })), { exitCode: code })); - return; - } - resolve(); - }); - }); -} - -module.exports = { startApiWithPm2 }; diff --git a/cli/lib/process.js b/cli/lib/process.js deleted file mode 100644 index d0f3ced1..00000000 --- a/cli/lib/process.js +++ /dev/null @@ -1,45 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { getPidFile } = require('./paths'); - -function readPid(projectRoot) { - const pidFile = getPidFile(projectRoot); - try { - const raw = fs.readFileSync(pidFile, 'utf8').trim(); - const pid = Number.parseInt(raw, 10); - return Number.isFinite(pid) ? pid : null; - } catch { - return null; - } -} - -function isProcessRunning(pid) { - if (!pid) return false; - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} - -function clearPidFile(projectRoot) { - const pidFile = getPidFile(projectRoot); - if (fs.existsSync(pidFile)) { - fs.unlinkSync(pidFile); - } -} - -function writePid(projectRoot, pid) { - const pidFile = getPidFile(projectRoot); - fs.mkdirSync(path.dirname(pidFile), { recursive: true }); - fs.writeFileSync(pidFile, String(pid)); -} - -module.exports = { - readPid, - isProcessRunning, - clearPidFile, - writePid, - getPidFile, -}; diff --git a/cli/lib/project-type.js b/cli/lib/project-type.js deleted file mode 100644 index 32e53012..00000000 --- a/cli/lib/project-type.js +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index 976fcb5b..00000000 --- a/cli/lib/publish.js +++ /dev/null @@ -1,968 +0,0 @@ -#!/usr/bin/env node - -const { execSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); -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 [ - { - name: '@fecommunity/reactpress', - path: 'cli', - description: t('publish.pkg.main') - }, - { - name: '@fecommunity/reactpress-toolkit', - path: 'toolkit', - description: 'API client and utilities toolkit' - }, - { - name: '@fecommunity/reactpress-client', - path: 'client', - description: 'Frontend application package' - }, - { - name: '@fecommunity/reactpress-server', - path: 'server', - description: t('publish.pkg.server'), - deprecated: true - }, - { - name: '@fecommunity/reactpress-template-hello-world', - path: 'templates/hello-world', - description: 'Hello World template for ReactPress' - }, - { - name: '@fecommunity/reactpress-template-twentytwentyfive', - path: 'templates/twentytwentyfive', - description: 'Twenty Twenty Five blog template for ReactPress' - } -]; -} - -const packages = getPackages(); - -// Generate a hash for a file or directory -function generateHash(filePath) { - try { - if (fs.statSync(filePath).isDirectory()) { - const files = fs.readdirSync(filePath); - const hashes = files - .filter(file => !file.startsWith('.') && file !== 'node_modules' && file !== 'dist' && file !== '.next') // Ignore hidden files and build directories - .map(file => generateHash(path.join(filePath, file))) - .sort(); - return crypto.createHash('md5').update(hashes.join('')).digest('hex'); - } else { - const content = fs.readFileSync(filePath); - return crypto.createHash('md5').update(content).digest('hex'); - } - } catch (error) { - return ''; - } -} - -// Get package content hash -function getPackageHash(packagePath) { - const fullPath = path.join(getWorkspaceRoot(), packagePath); - return generateHash(fullPath); -} - -// Check if package has meaningful changes (for build) -function hasMeaningfulChangesForBuild(packagePath, packageName) { - try { - // Create a hash file path to store previous hash in node_modules - const hashFilePath = path.join(getWorkspaceRoot(), 'node_modules', '.build-cache', `${packageName.replace('/', '_')}.hash`); - - // Generate current hash - const currentHash = getPackageHash(packagePath); - - // Check if we have a previous hash - if (fs.existsSync(hashFilePath)) { - const previousHash = fs.readFileSync(hashFilePath, 'utf8').trim(); - return currentHash !== previousHash; - } - - // If no previous hash, consider it as having changes - return true; - } catch (error) { - console.log(chalk.yellow(`⚠️ Could not check changes for ${packageName}, assuming changes exist`)); - return true; - } -} - -// Check if package has meaningful changes (for publish) -function hasMeaningfulChangesForPublish(packagePath, packageName) { - try { - // Create a hash file path to store previous hash in node_modules - const hashFilePath = path.join(getWorkspaceRoot(), 'node_modules', '.publish-cache', `${packageName.replace('/', '_')}.hash`); - - // Generate current hash - const currentHash = getPackageHash(packagePath); - - // Check if we have a previous hash - if (fs.existsSync(hashFilePath)) { - const previousHash = fs.readFileSync(hashFilePath, 'utf8').trim(); - return currentHash !== previousHash; - } - - // If no previous hash, consider it as having changes - return true; - } catch (error) { - console.log(chalk.yellow(`⚠️ Could not check changes for ${packageName}, assuming changes exist`)); - return true; - } -} - -// Save package hash (for build) -function savePackageHashForBuild(packagePath, packageName) { - try { - const hashFilePath = path.join(getWorkspaceRoot(), 'node_modules', '.build-cache', `${packageName.replace('/', '_')}.hash`); - - // Ensure cache directory exists - const cacheDir = path.dirname(hashFilePath); - if (!fs.existsSync(cacheDir)) { - fs.mkdirSync(cacheDir, { recursive: true }); - } - - // Generate and save current hash - const currentHash = getPackageHash(packagePath); - fs.writeFileSync(hashFilePath, currentHash); - } catch (error) { - console.log(chalk.yellow(`⚠️ Could not save hash for ${packageName}`)); - } -} - -// Save package hash (for publish) -function savePackageHashForPublish(packagePath, packageName) { - try { - const hashFilePath = path.join(getWorkspaceRoot(), 'node_modules', '.publish-cache', `${packageName.replace('/', '_')}.hash`); - - // Ensure cache directory exists - const cacheDir = path.dirname(hashFilePath); - if (!fs.existsSync(cacheDir)) { - fs.mkdirSync(cacheDir, { recursive: true }); - } - - // Generate and save current hash - const currentHash = getPackageHash(packagePath); - fs.writeFileSync(hashFilePath, currentHash); - } catch (error) { - console.log(chalk.yellow(`⚠️ Could not save hash for ${packageName}`)); - } -} - -// Get current versions -function getCurrentVersion(packagePath) { - try { - const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - return pkg.version; - } catch (error) { - return 'unknown'; - } -} - -// Increment version based on type -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': - // For beta, we increment the beta number or add beta.1 if not present - const match = version.match(/^(.*)-beta\.(\d+)$/); - if (match) { - const baseVersion = match[1]; - const betaNumber = parseInt(match[2]); - return `${baseVersion}-beta.${betaNumber + 1}`; - } else { - // If no beta version exists, add beta.1 - return `${version}-beta.1`; - } - case 'alpha': - // For alpha, we increment the alpha number or add alpha.1 if not present - const alphaMatch = version.match(/^(.*)-alpha\.(\d+)$/); - if (alphaMatch) { - const baseVersion = alphaMatch[1]; - const alphaNumber = parseInt(alphaMatch[2]); - return `${baseVersion}-alpha.${alphaNumber + 1}`; - } else { - // If no alpha version exists, add alpha.1 - return `${version}-alpha.1`; - } - default: - return version; - } -} - -// Get next available version from npm registry -function getNextAvailableVersion(packageName, currentVersion, versionType) { - try { - // First, increment the version locally - let nextVersion = incrementVersion(currentVersion, versionType); - - // Check if this version already exists on npm - let versionExists = true; - let attempts = 0; - const maxAttempts = 100; // Prevent infinite loop - - while (versionExists && attempts < maxAttempts) { - try { - execSync(`npm view ${packageName}@${nextVersion} version`, { stdio: 'ignore' }); - // If we get here, the version exists, so we need to increment again - nextVersion = incrementVersion(nextVersion, versionType); - attempts++; - } catch (error) { - // If we get an error, the version doesn't exist, which is what we want - versionExists = false; - } - } - - if (attempts >= maxAttempts) { - throw new Error('Too many attempts to find available version'); - } - - return nextVersion; - } catch (error) { - // Fallback to simple increment if npm view fails - console.log(chalk.yellow(`⚠️ Could not check npm registry, using local increment for ${packageName}`)); - return incrementVersion(currentVersion, versionType); - } -} - -// Update package version -function updateVersion(packagePath, newVersion) { - console.log(chalk.blue(`\n✏️ Updating version to ${newVersion}...`)); - - const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - - const oldVersion = pkg.version; - pkg.version = newVersion; - - fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); - console.log(chalk.green(`✅ Version updated from ${oldVersion} to ${newVersion}`)); -} - -// Fix workspace dependencies for build -function fixWorkspaceDependenciesForBuild(packagePath) { - console.log(chalk.blue(`🔧 Fixing workspace dependencies for build: ${packagePath}...`)); - - const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - - // Fix dependencies - const depTypes = ['dependencies', 'devDependencies', 'peerDependencies']; - - depTypes.forEach(depType => { - if (pkg[depType]) { - Object.keys(pkg[depType]).forEach(depName => { - // Check if it's a workspace dependency - if (pkg[depType][depName] === 'workspace:*' || pkg[depType][depName].startsWith('workspace:')) { - // For build purposes, we'll use the file: protocol to reference local packages - const depPackage = packages.find(p => p.name === depName); - if (depPackage) { - console.log(chalk.gray(` Replacing ${depName} workspace dependency with file reference`)); - pkg[depType][depName] = `file:../${depPackage.path}`; - } - } - }); - } - }); - - // Write the updated package.json - fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); - console.log(chalk.green(`✅ Workspace dependencies fixed for build: ${packagePath}`)); -} - -// Restore workspace dependencies after build -function restoreWorkspaceDependenciesAfterBuild(packagePath) { - console.log(chalk.blue(`🔄 Restoring workspace dependencies after build: ${packagePath}...`)); - - const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - - // Restore dependencies - const depTypes = ['dependencies', 'devDependencies', 'peerDependencies']; - - depTypes.forEach(depType => { - if (pkg[depType]) { - Object.keys(pkg[depType]).forEach(depName => { - // Check if this is one of our internal packages referenced with file: - const depPackage = packages.find(p => p.name === depName); - if (depPackage && pkg[depType][depName].startsWith('file:')) { - console.log(chalk.gray(` Restoring ${depName} to workspace dependency`)); - pkg[depType][depName] = 'workspace:*'; - } - }); - } - }); - - // Write the updated package.json - fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); - console.log(chalk.green(`✅ Workspace dependencies restored after build: ${packagePath}`)); -} - -// Fix workspace dependencies for publish -function fixWorkspaceDependenciesForPublish(packagePath, packageVersions) { - console.log(chalk.blue(`🔧 Fixing workspace dependencies for publish: ${packagePath}...`)); - - const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - - // Fix dependencies - const depTypes = ['dependencies', 'devDependencies', 'peerDependencies']; - - depTypes.forEach(depType => { - if (pkg[depType]) { - Object.keys(pkg[depType]).forEach(depName => { - // Check if it's a workspace dependency - if (pkg[depType][depName] === 'workspace:*' || pkg[depType][depName].startsWith('workspace:')) { - // Replace with actual version - const depPackage = packages.find(p => p.name === depName); - if (depPackage && packageVersions[depName]) { - console.log(chalk.gray(` Replacing ${depName} workspace dependency with version ${packageVersions[depName]}`)); - pkg[depType][depName] = packageVersions[depName]; - } - } - }); - } - }); - - // Write the updated package.json - fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); - console.log(chalk.green(`✅ Workspace dependencies fixed for publish: ${packagePath}`)); -} - -// Restore workspace dependencies after publish -function restoreWorkspaceDependenciesAfterPublish(packagePath) { - console.log(chalk.blue(`🔄 Restoring workspace dependencies for ${packagePath}...`)); - - const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - - // Restore dependencies - const depTypes = ['dependencies', 'devDependencies', 'peerDependencies']; - - depTypes.forEach(depType => { - if (pkg[depType]) { - Object.keys(pkg[depType]).forEach(depName => { - // Check if this is one of our internal packages - const depPackage = packages.find(p => p.name === depName); - if (depPackage) { - console.log(chalk.gray(` Restoring ${depName} to workspace dependency`)); - pkg[depType][depName] = 'workspace:*'; - } - }); - } - }); - - // Write the updated package.json - fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); - console.log(chalk.green(`✅ Workspace dependencies restored for ${packagePath}`)); -} - -// Build package -function buildPackage(pkg) { - console.log(chalk.blue(`\n🔨 Building ${pkg.name} (${pkg.description})...`)); - - try { - // Fix workspace dependencies for build - fixWorkspaceDependenciesForBuild(pkg.path); - - try { - 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: pkgDir, stdio: 'inherit' }); - } else if (pkg.path === 'toolkit') { - execSync('pnpm run build', { cwd: pkgDir, stdio: 'inherit' }); - } else if (pkg.path === 'templates/hello-world' || pkg.path === 'templates/twentytwentyfive') { - 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 { - // Always restore workspace dependencies - restoreWorkspaceDependenciesAfterBuild(pkg.path); - } - } catch (error) { - console.log(chalk.red(`❌ Failed to build ${pkg.name}`)); - throw error; - } -} - -// Publish package -function publishPackage(packagePath, packageName, tag = 'latest') { - console.log(chalk.blue(`\n🚀 Publishing ${packageName} with tag ${tag}...`)); - - try { - const command = `pnpm publish --access public --tag ${tag} --registry https://registry.npmjs.org --no-git-checks`; - 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}`)); - throw error; - } -} - -// Create GitHub release -function createGitHubRelease(tagName, releaseNotes) { - console.log(chalk.blue(`\n📝 Creating GitHub release ${tagName}...`)); - - try { - // Create release using GitHub CLI if available - const command = `gh release create ${tagName} --title "${tagName}" --notes "${releaseNotes}"`; - execSync(command, { stdio: 'inherit' }); - console.log(chalk.green(`✅ GitHub release ${tagName} created successfully!`)); - } catch (error) { - console.log(chalk.yellow(`⚠️ Failed to create GitHub release (GitHub CLI may not be installed or configured)`)); - console.log(chalk.gray('You can manually create the release at: https://github.com/fecommunity/reactpress/releases/new')); - } -} - -// Check environment -function checkEnvironment() { - // Check if pnpm is installed - try { - execSync('pnpm --version', { stdio: 'ignore' }); - } catch (error) { - console.log(chalk.red('❌ pnpm is not installed. Please install pnpm first.')); - return false; - } - - // Check if logged in to npm - try { - execSync('pnpm whoami --registry https://registry.npmjs.org', { stdio: 'ignore' }); - } catch (error) { - console.log(chalk.red('❌ Not logged in to npm. Please run "pnpm login --registry https://registry.npmjs.org" first.')); - return false; - } - - return true; -} - -// Build packages function -async function buildPackages() { - console.log(chalk.blue('🏗️ ReactPress Package Builder\n')); - - // Show current versions - console.log(chalk.cyan('📋 Current package versions:')); - packages.forEach(pkg => { - const version = getCurrentVersion(pkg.path); - console.log(chalk.gray(` ${pkg.name}: ${version}`)); - }); - console.log(); - - try { - // Track which packages actually need to be built - const packagesToBuild = []; - - // Check for meaningful changes in each package - for (const pkg of packages) { - 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`)); - } else { - console.log(chalk.gray(`\n⏭️ ${pkg.name} has no meaningful changes, skipping...`)); - } - } else { - console.log(chalk.yellow(`⚠️ Package ${pkg.name} directory not found, skipping...`)); - } - } - - if (packagesToBuild.length === 0) { - console.log(chalk.green('\n✅ No packages have meaningful changes. Nothing to build!')); - return; - } - - // Build packages that have changes - for (const pkg of packagesToBuild) { - await buildPackage(pkg); - // Save the hash after successful build - savePackageHashForBuild(pkg.path, pkg.name); - } - - console.log(chalk.green(`\n🎉 ${packagesToBuild.length} package(s) built successfully!`)); - } catch (error) { - console.error(chalk.red('❌ Build failed:'), error); - process.exit(1); - } -} - -// Publish packages function -async function publishPackages() { - // Check if called with --no-build flag - const noBuild = process.argv.includes('--no-build'); - - console.log(chalk.blue('📦 ReactPress Package Publisher\n')); - - // Run environment checks - if (!checkEnvironment()) { - process.exit(1); - } - - // Show current versions - console.log(chalk.cyan('📋 Current package versions:')); - packages.forEach(pkg => { - const version = getCurrentVersion(pkg.path); - console.log(chalk.gray(` ${pkg.name}: ${version}`)); - }); - console.log(); - - // Ask for publishing options - const { action } = await inquirer.prompt([ - { - type: 'list', - name: 'action', - message: 'What would you like to do?', - choices: [ - { name: '🚀 Publish all packages with version bump', value: 'publish-all' }, - { name: '📦 Publish specific package', value: 'publish-one' }, - { name: '🔨 Build all packages only', value: 'build-all' }, - { name: '🏷️ Publish as beta/alpha', value: 'publish-prerelease' }, - { name: '❌ Cancel', value: 'cancel' } - ] - } - ]); - - if (action === 'cancel') { - console.log(chalk.yellow('Operation cancelled.')); - return; - } - - if (action === 'build-all') { - console.log(chalk.blue('🔨 Building all packages...\n')); - - // Track which packages actually need to be built - const packagesToBuild = []; - - // Check for meaningful changes in each package - for (const pkg of packages) { - 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`)); - } else { - console.log(chalk.gray(`\n⏭️ ${pkg.name} has no meaningful changes, skipping...`)); - } - } else { - console.log(chalk.yellow(`⚠️ Package ${pkg.name} directory not found, skipping...`)); - } - } - - if (packagesToBuild.length === 0) { - console.log(chalk.green('\n✅ No packages have meaningful changes. Nothing to build!')); - return; - } - - for (const pkg of packagesToBuild) { - 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([ - { - type: 'list', - name: 'selectedPackage', - message: 'Which package would you like to publish?', - choices: packages.map(pkg => ({ - name: `${pkg.name} (${pkg.description})`, - value: pkg - })) - } - ]); - - // Check if the selected package has meaningful changes - if (!hasMeaningfulChangesForPublish(selectedPackage.path, selectedPackage.name)) { - console.log(chalk.gray(`\n⏭️ ${selectedPackage.name} has no meaningful changes, skipping...`)); - console.log(chalk.green('✅ Nothing to publish!')); - return; - } - - const { versionType } = await inquirer.prompt([ - { - type: 'list', - name: 'versionType', - message: 'Version bump type:', - choices: [ - { name: 'Beta (1.0.0-beta.1 -> 1.0.0-beta.2)', value: 'beta' }, - { name: 'Patch (1.0.0 -> 1.0.1)', value: 'patch' }, - { name: 'Minor (1.0.0 -> 1.1.0)', value: 'minor' }, - { name: 'Major (1.0.0 -> 2.0.0)', value: 'major' }, - { name: 'Custom version', value: 'custom' } - ] - } - ]); - - let newVersion; - const currentVersion = getCurrentVersion(selectedPackage.path); - - if (versionType === 'custom') { - const { customVersion } = await inquirer.prompt([ - { - type: 'input', - name: 'customVersion', - message: `Enter new version for ${selectedPackage.name} (current: ${currentVersion}):`, - validate: (input) => { - const semverRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?$/; - return semverRegex.test(input) || 'Please enter a valid semver version (e.g., 1.0.0)'; - } - } - ]); - newVersion = customVersion; - } else { - newVersion = getNextAvailableVersion(selectedPackage.name, currentVersion, versionType); - } - - // Get all package versions for dependency resolution - const packageVersions = {}; - packages.forEach(pkg => { - packageVersions[pkg.name] = getCurrentVersion(pkg.path); - }); - // Update the selected package version - packageVersions[selectedPackage.name] = newVersion; - - // Fix workspace dependencies before publishing - fixWorkspaceDependenciesForPublish(selectedPackage.path, packageVersions); - - try { - updateVersion(selectedPackage.path, newVersion); - // Only build if not disabled - if (!noBuild) { - buildPackage(selectedPackage); - } - - // Determine tag based on version type - const tag = versionType === 'beta' ? 'beta' : 'latest'; - publishPackage(selectedPackage.path, selectedPackage.name, tag); - - // Save the hash after successful publish - savePackageHashForPublish(selectedPackage.path, selectedPackage.name); - - console.log(chalk.green(`\n🎉 ${selectedPackage.name} v${newVersion} published successfully!`)); - } finally { - // Always restore workspace dependencies - restoreWorkspaceDependenciesAfterPublish(selectedPackage.path); - } - - return; - } - - if (action === 'publish-prerelease') { - const { tag } = await inquirer.prompt([ - { - type: 'list', - name: 'tag', - message: 'Select prerelease tag:', - choices: ['beta', 'alpha', 'rc', 'next'] - } - ]); - - const { versionType } = await inquirer.prompt([ - { - type: 'list', - name: 'versionType', - message: 'Version bump type:', - choices: [ - { name: `Prerelease (${tag})`, value: tag }, - { name: 'Patch (1.0.0 -> 1.0.1)', value: 'patch' }, - { name: 'Minor (1.0.0 -> 1.1.0)', value: 'minor' }, - { name: 'Major (1.0.0 -> 2.0.0)', value: 'major' }, - { name: 'Custom version', value: 'custom' } - ] - } - ]); - - // Get all package versions for dependency resolution - const packageVersions = {}; - packages.forEach(pkg => { - const currentVersion = getCurrentVersion(pkg.path); - packageVersions[pkg.name] = currentVersion; - }); - - // Track which packages actually need to be published - const packagesToPublish = []; - - // Check for meaningful changes in each package - for (const pkg of packages) { - if (!fs.existsSync(path.join(getWorkspaceRoot(), pkg.path))) { - console.log(chalk.yellow(`⚠️ Package ${pkg.name} directory not found, skipping...`)); - continue; - } - - if (hasMeaningfulChangesForPublish(pkg.path, pkg.name)) { - packagesToPublish.push(pkg); - console.log(chalk.blue(`\n📦 ${pkg.name} has changes, will be published`)); - } else { - console.log(chalk.gray(`\n⏭️ ${pkg.name} has no meaningful changes, skipping...`)); - } - } - - if (packagesToPublish.length === 0) { - console.log(chalk.green('\n✅ No packages have meaningful changes. Nothing to publish!')); - return; - } - - // Process each package that has changes - for (const pkg of packagesToPublish) { - let newVersion; - const currentVersion = getCurrentVersion(pkg.path); - - if (versionType === 'custom') { - const { customVersion } = await inquirer.prompt([ - { - type: 'input', - name: 'customVersion', - message: `Enter version for ${pkg.name} (current: ${currentVersion}):`, - default: currentVersion - } - ]); - newVersion = customVersion; - } else { - newVersion = getNextAvailableVersion(pkg.name, currentVersion, versionType); - } - - // Update package version in our tracking - packageVersions[pkg.name] = newVersion; - - // Fix workspace dependencies before publishing - fixWorkspaceDependenciesForPublish(pkg.path, packageVersions); - - try { - updateVersion(pkg.path, newVersion); - // Only build if not disabled - if (!noBuild) { - buildPackage(pkg); - } - publishPackage(pkg.path, pkg.name, tag); - - // Save the hash after successful publish - savePackageHashForPublish(pkg.path, pkg.name); - } finally { - // Always restore workspace dependencies - restoreWorkspaceDependenciesAfterPublish(pkg.path); - } - } - - console.log(chalk.green(`\n🎉 ${packagesToPublish.length} package(s) published with ${tag} tag!`)); - return; - } - - if (action === 'publish-all') { - // Check if we're on master branch for final release - let isMasterBranch = false; - try { - const branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim(); - isMasterBranch = branch === 'master' || branch === 'main'; - } catch (error) { - console.log(chalk.yellow('⚠️ Unable to determine current branch')); - } - - const { versionType } = await inquirer.prompt([ - { - type: 'list', - name: 'versionType', - message: 'Version bump type:', - choices: [ - { name: `Beta ${isMasterBranch ? '(will publish as final)' : '(will publish as beta)'}`, value: 'beta' }, - { name: 'Patch (1.0.0 -> 1.0.1)', value: 'patch' }, - { name: 'Minor (1.0.0 -> 1.1.0)', value: 'minor' }, - { name: 'Major (1.0.0 -> 2.0.0)', value: 'major' }, - { name: 'Custom version', value: 'custom' } - ] - } - ]); - - // Get all package versions for dependency resolution - const packageVersions = {}; - const originalVersions = {}; - - packages.forEach(pkg => { - const currentVersion = getCurrentVersion(pkg.path); - originalVersions[pkg.name] = currentVersion; - packageVersions[pkg.name] = currentVersion; - }); - - let baseVersion; - if (versionType === 'custom') { - const { customVersion } = await inquirer.prompt([ - { - type: 'input', - name: 'customVersion', - message: 'Enter new version for all packages:', - validate: (input) => { - const semverRegex = /^\d+\.\d+\.\d+$/; - return semverRegex.test(input) || 'Please enter a valid semver version (e.g., 1.0.0)'; - } - } - ]); - baseVersion = customVersion; - } else { - // Use the highest current version as base and increment - const nextVersion = getNextAvailableVersion(packages[0].name, originalVersions[packages[0].name], versionType); - baseVersion = nextVersion; - } - - console.log(chalk.cyan(`\n📋 Will publish all packages with version: ${baseVersion}\n`)); - - const { confirm } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirm', - message: 'Are you sure you want to proceed?', - default: false - } - ]); - - if (!confirm) { - console.log(chalk.yellow('Operation cancelled.')); - return; - } - - // Track which packages actually need to be published - const packagesToPublish = []; - - // Check for meaningful changes in each package - for (const pkg of packages) { - if (!fs.existsSync(path.join(getWorkspaceRoot(), pkg.path))) { - console.log(chalk.yellow(`⚠️ Package ${pkg.name} directory not found, skipping...`)); - continue; - } - - if (hasMeaningfulChangesForPublish(pkg.path, pkg.name)) { - packagesToPublish.push(pkg); - console.log(chalk.blue(`\n📦 ${pkg.name} has changes, will be published`)); - } else { - console.log(chalk.gray(`\n⏭️ ${pkg.name} has no meaningful changes, skipping...`)); - } - } - - if (packagesToPublish.length === 0) { - console.log(chalk.green('\n✅ No packages have meaningful changes. Nothing to publish!')); - return; - } - - // Update versions, build and publish only packages with changes - for (const pkg of packagesToPublish) { - console.log(chalk.blue(`\n📦 Processing ${pkg.name}...`)); - - // For publish-all, we use the same version for all packages - const pkgVersion = baseVersion; - packageVersions[pkg.name] = pkgVersion; - - // Fix workspace dependencies before publishing - fixWorkspaceDependenciesForPublish(pkg.path, packageVersions); - - try { - updateVersion(pkg.path, pkgVersion); - // Only build if not disabled - if (!noBuild) { - buildPackage(pkg); - } - - // Determine tag based on version type and branch - const tag = (versionType === 'beta' && !isMasterBranch) ? 'beta' : 'latest'; - publishPackage(pkg.path, pkg.name, tag); - - // Save the hash after successful publish - savePackageHashForPublish(pkg.path, pkg.name); - } finally { - // Always restore workspace dependencies - restoreWorkspaceDependenciesAfterPublish(pkg.path); - } - } - - // Create GitHub release if on master and we actually published something - if (isMasterBranch && packagesToPublish.length > 0) { - const tagName = `v${baseVersion}`; - const releaseNotes = `Release ${baseVersion} - -Packages released: -${Object.entries(packageVersions).map(([name, version]) => `- ${name}@${version}`).join('\n')}`; - createGitHubRelease(tagName, releaseNotes); - } - - console.log(chalk.green(`\n🎉 ${packagesToPublish.length} package(s) published successfully with version ${baseVersion}!`)); - console.log(chalk.cyan('\n📋 Next steps:')); - console.log(chalk.gray('1. Create a git tag: git tag v' + baseVersion)); - console.log(chalk.gray('2. Push changes: git push && git push --tags')); - } -} - -// Main function -async function main() { - // Check command line arguments - const args = process.argv.slice(2); - - if (args.includes('--publish')) { - // When called with --publish, start the publish process - console.log(chalk.blue('🏗️ ReactPress Package Builder\n')); - - // Show current versions - console.log(chalk.cyan('📋 Current package versions:')); - packages.forEach(pkg => { - const version = getCurrentVersion(pkg.path); - console.log(chalk.gray(` ${pkg.name}: ${version}`)); - }); - console.log(); - - // Start the publish process - await publishPackages(); - } else if (args.includes('--build')) { - // When called with --build, just build packages - await buildPackages(); - } else { - // Default behavior - show help - console.log(chalk.blue('🏗️ ReactPress CLI\n')); - console.log('Usage:'); - console.log(' reactpress publish --build Build all packages'); - console.log(' reactpress publish --publish Publish packages (interactive)'); - console.log(''); - } -} - -module.exports = { main, buildPackages, publishPackages }; - -if (require.main === module) { - main().catch((error) => { - console.error(chalk.red('❌ Operation failed:'), error); - process.exit(1); - }); -} \ No newline at end of file diff --git a/cli/lib/root.js b/cli/lib/root.js deleted file mode 100644 index cfb53a79..00000000 --- a/cli/lib/root.js +++ /dev/null @@ -1,88 +0,0 @@ -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. - */ -function getMonorepoRoot() { - const packageRoot = path.resolve(__dirname, '..'); - const parentOfPackage = path.resolve(__dirname, '../..'); - if (fs.existsSync(path.join(parentOfPackage, 'pnpm-workspace.yaml'))) { - return parentOfPackage; - } - 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() { - 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() { - const root = getProjectRoot(); - process.env.REACTPRESS_ORIGINAL_CWD = root; - return root; -} - -function isMonorepoCheckout(cwd) { - const resolved = path.resolve(cwd || process.cwd()); - return ( - fs.existsSync(path.join(resolved, 'pnpm-workspace.yaml')) || - fs.existsSync(path.join(resolved, 'server', 'src', 'main.ts')) - ); -} - -module.exports = { - getMonorepoRoot, - getProjectRoot, - ensureOriginalCwd, - isMonorepoCheckout, - isProjectRoot, - findProjectRoot, - isPublishedCliRoot, -}; diff --git a/cli/lib/spawn.js b/cli/lib/spawn.js deleted file mode 100644 index 4753e978..00000000 --- a/cli/lib/spawn.js +++ /dev/null @@ -1,92 +0,0 @@ -const { spawn, spawnSync } = require('child_process'); -const path = require('path'); -const chalk = require('chalk'); -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 || ensureOriginalCwd(), - stdio: 'inherit', - env: { - ...process.env, - REACTPRESS_LANG: process.env.REACTPRESS_LANG || resolveLocale(), - REACTPRESS_ORIGINAL_CWD: - options.cwd || process.env.REACTPRESS_ORIGINAL_CWD || process.cwd(), - ...options.env, - }, - shell: options.shell ?? false, - }); - if (result.status !== 0) { - const err = new Error( - t('spawn.commandFailed', { command, code: result.status ?? 1 }) - ); - err.exitCode = result.status ?? 1; - throw err; - } - return result; -} - -function runNodeScript(scriptPath, args = [], options = {}) { - return new Promise((resolve, reject) => { - const child = spawn(process.execPath, [scriptPath, ...args], { - stdio: 'inherit', - cwd: options.cwd || ensureOriginalCwd(), - env: { - ...process.env, - REACTPRESS_LANG: process.env.REACTPRESS_LANG || resolveLocale(), - REACTPRESS_ORIGINAL_CWD: - options.cwd || process.env.REACTPRESS_ORIGINAL_CWD || process.cwd(), - ...options.env, - }, - }); - - child.on('error', (error) => { - console.error(chalk.red('[ReactPress]'), error.message || error); - reject(error); - }); - - child.on('close', (code) => { - if (code !== 0) { - reject(Object.assign(new Error(t('spawn.exitCode', { code })), { exitCode: code })); - return; - } - resolve(code); - }); - }); -} - -function spawnDetached(scriptPath, args = [], options = {}) { - const child = spawn(process.execPath, [scriptPath, ...args], { - stdio: options.stdio ?? 'ignore', - detached: true, - cwd: options.cwd, - env: { - ...process.env, - REACTPRESS_LANG: process.env.REACTPRESS_LANG || resolveLocale(), - REACTPRESS_ORIGINAL_CWD: - options.cwd || process.env.REACTPRESS_ORIGINAL_CWD || process.cwd(), - ...options.env, - }, - }); - child.unref(); - return child; -} - -function runReactpressCli(args, options = {}) { - const cliBin = path.join(getCliPackageRoot(), 'dist', 'index.js'); - return runSync(process.execPath, [cliBin, ...args], options); -} - -function resolveCliScript(relativePath) { - return path.join(__dirname, '..', relativePath); -} - -module.exports = { - runSync, - runNodeScript, - spawnDetached, - runReactpressCli, - resolveCliScript, -}; diff --git a/cli/lib/status.js b/cli/lib/status.js deleted file mode 100644 index a14ce81d..00000000 --- a/cli/lib/status.js +++ /dev/null @@ -1,132 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { - brand, - icon, - divider, - padRight, - statusPill, - sectionHeader, - terminalWidth, - gradientText, - palette, -} = require('../ui/theme'); -const { - loadServerSiteUrl, - loadClientSiteUrl, - getHealthUrl, - checkHealth, - isHttpResponding, -} = require('./http'); -const { isUsingMonorepoServer } = require('./paths'); -const { readPid, isProcessRunning } = require('./process'); -const { isDockerRunning } = require('./docker'); -const { ensureOriginalCwd } = require('./root'); -const { t } = require('./i18n'); - -function envFileStatus(projectRoot) { - const envPath = path.join(projectRoot, '.env'); - const configPath = path.join(projectRoot, '.reactpress', 'config.json'); - return { - env: fs.existsSync(envPath), - config: fs.existsSync(configPath), - envPath, - configPath, - }; -} - -function fieldRow(label, value) { - return ` ${brand.muted(padRight(label, 10))} ${value}`; -} - -async function printUnifiedStatus(projectRoot = ensureOriginalCwd()) { - const env = envFileStatus(projectRoot); - const apiUrl = loadServerSiteUrl(projectRoot); - const clientUrl = loadClientSiteUrl(projectRoot); - const pid = readPid(projectRoot); - const healthUrl = getHealthUrl(projectRoot); - const [apiHttp, clientHttp, health] = await Promise.all([ - isHttpResponding(apiUrl), - isHttpResponding(clientUrl), - checkHealth(healthUrl), - ]); - - 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(` ${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( - fieldRow( - t('status.field.config'), - env.config ? brand.success(t('status.configOk')) : brand.warn(t('status.configBad')) - ) - ); - console.log( - 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( - fieldRow( - t('status.field.health'), - health.ok - ? `${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( - fieldRow( - t('status.field.database'), - statusPill(dbOk, { on: t('status.dbUp'), off: t('status.dbDown') }) - ) - ); - } - const pidAlive = pid && isProcessRunning(pid); - console.log( - fieldRow( - t('status.field.pid'), - `${brand.dim(pid ?? '—')}${pidAlive ? ` ${brand.success(t('status.pidRunning'))}` : ''}` - ) - ); - - 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( - fieldRow( - t('status.field.engine'), - statusPill(isDockerRunning(), { - on: t('status.dockerUp'), - off: t('status.dockerDown'), - }) - ) - ); - - console.log(` ${divider(w)}`); - console.log(''); -} - -module.exports = { printUnifiedStatus, envFileStatus }; diff --git a/cli/package.json b/cli/package.json index 106be0e5..59f55663 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,9 +1,13 @@ { "name": "@fecommunity/reactpress", - "version": "3.7.0", - "description": "ReactPress 3.0 — zero-config CMS: one package, one reactpress command", + "version": "4.0.0-beta.18", + "description": "ReactPress CLI — Publish with React. Ship like WordPress. CMS, Admin, Headless API, and Next.js themes in one command. Self-hosted, MIT. `npx @fecommunity/reactpress init`", "author": "fecommunity", "license": "MIT", + "homepage": "https://docs.gaoredu.com/", + "bugs": { + "url": "https://github.com/fecommunity/reactpress/issues" + }, "repository": { "type": "git", "url": "git+https://github.com/fecommunity/reactpress.git", @@ -13,7 +17,18 @@ "reactpress", "cms", "blog", - "cli" + "blog-platform", + "cli", + "headless-cms", + "publishing-platform", + "wordpress-alternative", + "self-hosted", + "sqlite", + "nestjs", + "nextjs", + "content-management", + "typescript", + "open-source" ], "engines": { "node": ">=18" @@ -22,22 +37,27 @@ "reactpress": "./bin/reactpress.js", "reactpress-cli": "./bin/reactpress-cli-shim.js" }, - "main": "./bin/reactpress.js", + "main": "./out/bin/reactpress.js", "files": [ "bin", - "lib", - "ui", + "out", "dist", "server", + "toolkit", + "admin-dist", + "plugins", "templates", "scripts/install-bundled-runtime.mjs", "README.md", "LICENSE" ], "scripts": { - "test": "node --test tests", - "prepare": "node scripts/sync-bundled-core.mjs", - "prepack": "node scripts/sync-bundled-core.mjs && node scripts/sync-monorepo-server.mjs" + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "pnpm run build && node --test tests", + "prepare": "node scripts/prepare.mjs", + "prepack": "node scripts/sync-bundled-core.mjs && node scripts/sync-monorepo-server.mjs && node scripts/sync-bundled-toolkit.mjs && node scripts/sync-bundled-admin.mjs && node scripts/sync-bundled-plugins.mjs && node scripts/patch-bundled-dist.mjs && pnpm run build && node scripts/prune-bundled-pack.mjs", + "postinstall": "node scripts/install-bundled-runtime.mjs" }, "publishConfig": { "access": "public", @@ -57,6 +77,11 @@ "ora": "^5.4.1" }, "devDependencies": { - "@fecommunity/reactpress-cli-core": "npm:@fecommunity/reactpress-cli@0.1.0" + "@fecommunity/reactpress-cli-core": "npm:@fecommunity/reactpress-cli@0.1.0", + "@types/cross-spawn": "^6.0.6", + "@types/fs-extra": "^11.0.4", + "@types/inquirer": "^8.2.10", + "@types/node": "^24.5.2", + "typescript": "~5.9.3" } } diff --git a/cli/scripts/patch-bundled-dist.mjs b/cli/scripts/patch-bundled-dist.mjs new file mode 100644 index 00000000..a508e717 --- /dev/null +++ b/cli/scripts/patch-bundled-dist.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node +/** + * Patch synced ESM server-bundle to require bundled toolkit/dist at package root. + */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const target = path.join( + path.dirname(fileURLToPath(import.meta.url)), + '..', + 'dist', + 'utils', + 'server-bundle.js', +); + +if (!fs.existsSync(target)) { + process.exit(0); +} + +const src = fs.readFileSync(target, 'utf8'); +if (src.includes('getBundledToolkitMain')) { + process.exit(0); +} + +const patched = src + .replace( + "const SERVER_ENTRY = join('bin', 'reactpress-server.js');", + `const SERVER_ENTRY = join('bin', 'reactpress-server.js'); +const TOOLKIT_MAIN = join('toolkit', 'dist', 'index.js');`, + ) + .replace( + 'export function getBundledServerMain() {', + `export function getBundledToolkitMain() { + return join(getPackageRoot(), ...TOOLKIT_MAIN.split('/')); +} +export function getBundledServerMain() {`, + ) + .replace( + ' if (!(await fs.pathExists(join(serverDir, SERVER_MAIN)))) {', + ` if (!(await fs.pathExists(join(serverDir, SERVER_MAIN)))) { + return false; + } + if (!(await fs.pathExists(getBundledToolkitMain()))) {`, + ); + +fs.writeFileSync(target, patched); +console.log('[patch-bundled-dist] dist/utils/server-bundle.js toolkit check'); + +const bootstrapPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'out', 'lib', 'bootstrap.js'); +if (fs.existsSync(bootstrapPath)) { + let bootstrap = fs.readFileSync(bootstrapPath, 'utf8'); + const marker = 'ensureBundledPlugins(root)'; + if (!bootstrap.includes(marker)) { + bootstrap = bootstrap.replace( + '(0, cli_context_1.setProjectCwd)(root);', + `(0, cli_context_1.setProjectCwd)(root); + try { + const { ensureBundledPlugins } = require('../core/services/local-site'); + ensureBundledPlugins(root); + } + catch { + // ignore + }`, + ); + fs.writeFileSync(bootstrapPath, bootstrap); + console.log('[patch-bundled-dist] bootstrap.js ensureBundledPlugins hook'); + } +} diff --git a/cli/scripts/prepare.mjs b/cli/scripts/prepare.mjs new file mode 100644 index 00000000..502af736 --- /dev/null +++ b/cli/scripts/prepare.mjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node +/** + * prepare lifecycle: + * - npm install (local): sync bundled core + build TypeScript + * - npm pack / publish: no-op (prepack already synced and pruned) + */ +import { spawnSync } from 'node:child_process'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const cliRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); +const npmCommand = process.env.npm_command || ''; + +if (npmCommand === 'pack' || npmCommand === 'publish') { + process.exit(0); +} + +function run(label, cmd, args) { + const result = spawnSync(cmd, args, { cwd: cliRoot, stdio: 'inherit', shell: process.platform === 'win32' }); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +run('sync-bundled-core', process.execPath, [join(cliRoot, 'scripts', 'sync-bundled-core.mjs')]); +run('build', 'pnpm', ['run', 'build']); diff --git a/cli/scripts/prune-bundled-pack.mjs b/cli/scripts/prune-bundled-pack.mjs new file mode 100644 index 00000000..ae5f2138 --- /dev/null +++ b/cli/scripts/prune-bundled-pack.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node +/** + * Strip heavyweight artifacts before `npm pack` so the published tarball stays <10MB. + * Runtime deps are installed via postinstall (install-bundled-runtime.mjs). + */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const cliRoot = path.join(__dirname, '..'); + +const REMOVE_DIRS = [ + 'server/node_modules', + 'toolkit/node_modules', + 'server/logs', + 'server/.reactpress', + 'server/public/uploads', + 'server/dist/public', +]; + +const PRUNE_DIRS = ['out', 'dist', 'server/dist', 'toolkit/dist']; + +function shouldPruneFile(name) { + return ( + name.endsWith('.map') || + name.endsWith('.d.ts') || + name.endsWith('.d.ts.map') || + name.endsWith('.tsbuildinfo') + ); +} + +function rmDir(rel) { + const abs = path.join(cliRoot, rel); + if (!fs.existsSync(abs)) return; + fs.rmSync(abs, { recursive: true, force: true }); + console.log(`[prune-bundled-pack] removed ${rel}`); +} + +function walkPrune(dir) { + if (!fs.existsSync(dir)) return; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const abs = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === 'node_modules') { + fs.rmSync(abs, { recursive: true, force: true }); + console.log(`[prune-bundled-pack] removed ${path.relative(cliRoot, abs)}`); + continue; + } + walkPrune(abs); + continue; + } + if (shouldPruneFile(entry.name)) { + fs.unlinkSync(abs); + } + } +} + +function main() { + for (const rel of REMOVE_DIRS) { + rmDir(rel); + } + for (const rel of PRUNE_DIRS) { + walkPrune(path.join(cliRoot, rel)); + } + console.log('[prune-bundled-pack] done'); +} + +main(); diff --git a/cli/scripts/sync-bundled-admin.mjs b/cli/scripts/sync-bundled-admin.mjs new file mode 100644 index 00000000..24760327 --- /dev/null +++ b/cli/scripts/sync-bundled-admin.mjs @@ -0,0 +1,29 @@ +#!/usr/bin/env node +/** + * Copy built Admin SPA (web/dist) into cli/admin-dist/ for npm publish. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const cliRoot = path.join(__dirname, '..'); +const repoRoot = path.join(cliRoot, '..'); +const webDist = path.join(repoRoot, 'web', 'dist'); +const bundledAdmin = path.join(cliRoot, 'admin-dist'); + +function main() { + const indexHtml = path.join(webDist, 'index.html'); + if (!fs.existsSync(indexHtml)) { + console.warn('[sync-bundled-admin] Skip: build web first (pnpm run build:web)'); + process.exit(0); + } + + if (fs.existsSync(bundledAdmin)) { + fs.rmSync(bundledAdmin, { recursive: true, force: true }); + } + fs.cpSync(webDist, bundledAdmin, { recursive: true }); + console.log('[sync-bundled-admin] web/dist -> cli/admin-dist/'); +} + +main(); diff --git a/cli/scripts/sync-bundled-core.mjs b/cli/scripts/sync-bundled-core.mjs index 68021882..91c8df6c 100644 --- a/cli/scripts/sync-bundled-core.mjs +++ b/cli/scripts/sync-bundled-core.mjs @@ -14,7 +14,7 @@ const require = createRequire(import.meta.url); const LEGACY_PKG = '@fecommunity/reactpress-cli-core'; -const SKIP_DIRS = new Set(['node_modules', '.git', 'logs']); +const SKIP_DIRS = new Set(['node_modules', '.git', 'logs', '.reactpress']); const SKIP_FILES = new Set(['package-lock.json']); function copyDir(src, dest) { @@ -52,7 +52,9 @@ function main() { console.log(`[sync-bundled-core] ${name}/ -> cli/${name}/`); } - for (const file of ['LICENSE', 'README.md']) { + // Copy LICENSE only. Do not sync README.md — legacy @fecommunity/reactpress-cli-core + // ships a Chinese README that would overwrite the v4 English package docs on every prepare/prepack. + for (const file of ['LICENSE']) { const src = path.join(legacyRoot, file); if (fs.existsSync(src)) { fs.copyFileSync(src, path.join(cliRoot, file)); @@ -72,7 +74,7 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const require = createRequire(import.meta.url); -const { t, getLocale, setLocale } = require(join(dirname(fileURLToPath(import.meta.url)), '..', 'lib', 'i18n', 'index.js')); +const { t, getLocale, setLocale } = require(join(dirname(fileURLToPath(import.meta.url)), '..', 'out', 'lib', 'i18n', 'index.js')); export { t, getLocale, setLocale }; ` diff --git a/cli/scripts/sync-bundled-plugins.mjs b/cli/scripts/sync-bundled-plugins.mjs new file mode 100644 index 00000000..f06757fb --- /dev/null +++ b/cli/scripts/sync-bundled-plugins.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node +/** + * Copy built plugins (plugins/) into cli/plugins/ for npm publish. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const cliRoot = path.join(__dirname, '..'); +const repoRoot = path.join(cliRoot, '..'); +const pluginsRoot = path.join(repoRoot, 'plugins'); +const bundledPlugins = path.join(cliRoot, 'plugins'); + +const PLUGIN_SKIP_DIRS = new Set(['node_modules', '.git', '.turbo', 'src', 'coverage']); + +function copyPluginTree(srcDir, destDir) { + fs.mkdirSync(destDir, { recursive: true }); + for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) { + const src = path.join(srcDir, entry.name); + const dest = path.join(destDir, entry.name); + + if (entry.isDirectory()) { + if (PLUGIN_SKIP_DIRS.has(entry.name)) continue; + copyPluginTree(src, dest); + continue; + } + + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.copyFileSync(src, dest); + } +} + +function ensurePluginsBuilt() { + const registry = path.join(pluginsRoot, 'package.json'); + if (!fs.existsSync(registry)) { + console.warn('[sync-bundled-plugins] Skip: plugins/package.json missing'); + process.exit(0); + } + + let local = []; + try { + const pkg = JSON.parse(fs.readFileSync(registry, 'utf8')); + local = Array.isArray(pkg.reactpress?.local) ? pkg.reactpress.local : []; + } catch { + local = []; + } + + const needsBuild = local.some((id) => { + if (typeof id !== 'string' || !id.trim()) return false; + const pluginDir = path.join(pluginsRoot, id.trim()); + if (!fs.existsSync(path.join(pluginDir, 'plugin.json'))) return false; + return !fs.existsSync(path.join(pluginDir, 'dist', 'index.js')); + }); + + if (!needsBuild) return; + + console.log('[sync-bundled-plugins] Building plugins before sync…'); + const build = spawnSync('pnpm', ['run', 'build'], { + cwd: pluginsRoot, + stdio: 'inherit', + shell: process.platform === 'win32', + }); + if (build.status !== 0) { + process.exit(build.status ?? 1); + } +} + +function main() { + ensurePluginsBuilt(); + + const registry = path.join(pluginsRoot, 'package.json'); + if (!fs.existsSync(registry)) { + console.warn('[sync-bundled-plugins] Skip: plugins/package.json missing'); + process.exit(0); + } + + if (fs.existsSync(bundledPlugins)) { + fs.rmSync(bundledPlugins, { recursive: true, force: true }); + } + fs.mkdirSync(bundledPlugins, { recursive: true }); + fs.copyFileSync(registry, path.join(bundledPlugins, 'package.json')); + + for (const entry of fs.readdirSync(pluginsRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + if (PLUGIN_SKIP_DIRS.has(entry.name)) continue; + const pluginDir = path.join(pluginsRoot, entry.name); + if (!fs.existsSync(path.join(pluginDir, 'plugin.json'))) continue; + copyPluginTree(pluginDir, path.join(bundledPlugins, entry.name)); + } + + console.log('[sync-bundled-plugins] plugins/ -> cli/plugins/'); +} + +main(); diff --git a/cli/scripts/sync-bundled-toolkit.mjs b/cli/scripts/sync-bundled-toolkit.mjs new file mode 100644 index 00000000..acbb01a7 --- /dev/null +++ b/cli/scripts/sync-bundled-toolkit.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +/** + * Copy built toolkit into cli/toolkit/ for bundled server runtime (require ../../toolkit/dist). + */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const cliRoot = path.join(__dirname, '..'); +const repoRoot = path.join(cliRoot, '..'); +const monorepoToolkit = path.join(repoRoot, 'toolkit'); +const bundledToolkit = path.join(cliRoot, 'toolkit'); + +function copyDir(src, dest) { + fs.mkdirSync(dest, { recursive: true }); + for (const entry of fs.readdirSync(src, { withFileTypes: true })) { + if (entry.name === 'node_modules') continue; + const from = path.join(src, entry.name); + const to = path.join(dest, entry.name); + if (entry.isDirectory()) { + copyDir(from, to); + } else { + fs.copyFileSync(from, to); + } + } +} + +function replaceDir(src, dest) { + if (fs.existsSync(dest)) { + fs.rmSync(dest, { recursive: true, force: true }); + } + copyDir(src, dest); +} + +function main() { + const distIndex = path.join(monorepoToolkit, 'dist', 'index.js'); + if (!fs.existsSync(distIndex)) { + console.warn('[sync-bundled-toolkit] Skip: build toolkit first (pnpm run build --dir toolkit)'); + process.exit(0); + } + + replaceDir(path.join(monorepoToolkit, 'dist'), path.join(bundledToolkit, 'dist')); + + const srcPkg = JSON.parse( + fs.readFileSync(path.join(monorepoToolkit, 'package.json'), 'utf8'), + ); + const minimal = { + name: srcPkg.name, + version: srcPkg.version, + main: srcPkg.main ?? 'dist/index.js', + types: srcPkg.types ?? 'dist/index.d.ts', + ...(srcPkg.exports ? { exports: srcPkg.exports } : {}), + ...(srcPkg.dependencies ? { dependencies: srcPkg.dependencies } : {}), + }; + fs.writeFileSync( + path.join(bundledToolkit, 'package.json'), + `${JSON.stringify(minimal, null, 2)}\n`, + ); + + console.log('[sync-bundled-toolkit] toolkit/dist -> cli/toolkit/'); +} + +main(); diff --git a/cli/scripts/sync-monorepo-server.mjs b/cli/scripts/sync-monorepo-server.mjs index 0f97c18a..c993965b 100644 --- a/cli/scripts/sync-monorepo-server.mjs +++ b/cli/scripts/sync-monorepo-server.mjs @@ -13,25 +13,30 @@ const repoRoot = path.join(cliRoot, '..'); const monorepoServer = path.join(repoRoot, 'server'); const bundledServer = path.join(cliRoot, 'server'); -function copyDir(src, dest) { +const SKIP_DIRS = new Set(['node_modules', 'logs', 'uploads', '.reactpress']); +const SKIP_FILES = new Set(['tsconfig.build.tsbuildinfo', 'tsconfig.tsbuildinfo']); + +function copyDir(src, dest, options = {}) { + const skipDirs = options.skipDirs ?? SKIP_DIRS; fs.mkdirSync(dest, { recursive: true }); for (const entry of fs.readdirSync(src, { withFileTypes: true })) { - if (entry.name === 'node_modules' || entry.name === 'logs') continue; + if (skipDirs.has(entry.name)) continue; + if (!entry.isDirectory() && SKIP_FILES.has(entry.name)) continue; const from = path.join(src, entry.name); const to = path.join(dest, entry.name); if (entry.isDirectory()) { - copyDir(from, to); + copyDir(from, to, options); } else { fs.copyFileSync(from, to); } } } -function replaceDir(src, dest) { +function replaceDir(src, dest, options = {}) { if (fs.existsSync(dest)) { fs.rmSync(dest, { recursive: true, force: true }); } - copyDir(src, dest); + copyDir(src, dest, options); } function main() { @@ -41,11 +46,25 @@ function main() { process.exit(0); } - replaceDir(path.join(monorepoServer, 'dist'), path.join(bundledServer, 'dist')); + replaceDir(path.join(monorepoServer, 'dist'), path.join(bundledServer, 'dist'), { + skipDirs: new Set([...SKIP_DIRS, 'public']), + }); replaceDir(path.join(monorepoServer, 'public'), path.join(bundledServer, 'public')); replaceDir(path.join(monorepoServer, 'bin'), path.join(bundledServer, 'bin')); - console.log('[sync-monorepo-server] server/dist|public|bin -> cli/server/'); + const serverPkg = path.join(monorepoServer, 'package.json'); + if (fs.existsSync(serverPkg)) { + const pkg = JSON.parse(fs.readFileSync(serverPkg, 'utf8')); + if (pkg.dependencies?.['@fecommunity/reactpress-toolkit']?.startsWith('workspace:')) { + pkg.dependencies['@fecommunity/reactpress-toolkit'] = 'file:../toolkit'; + } + fs.writeFileSync( + path.join(bundledServer, 'package.json'), + `${JSON.stringify(pkg, null, 2)}\n`, + ); + } + + console.log('[sync-monorepo-server] server/dist|public|bin|package.json -> cli/server/'); } main(); diff --git a/cli/server/package-lock.json b/cli/server/package-lock.json new file mode 100644 index 00000000..c8e4a4f6 --- /dev/null +++ b/cli/server/package-lock.json @@ -0,0 +1,21085 @@ +{ + "name": "@fecommunity/reactpress-server", + "version": "4.0.0-beta.18", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@fecommunity/reactpress-server", + "version": "4.0.0-beta.18", + "deprecated": "Bundled in @fecommunity/reactpress CLI from v3.1. Run `npx @fecommunity/reactpress init` for new projects.", + "license": "MIT", + "dependencies": { + "@fecommunity/reactpress-toolkit": "file:../toolkit", + "@nestjs/cli": "^6.9.0", + "@nestjs/common": "^6.7.2", + "@nestjs/config": "^0.6.3", + "@nestjs/core": "^6.7.2", + "@nestjs/jwt": "^6.1.1", + "@nestjs/passport": "^6.1.1", + "@nestjs/platform-express": "^6.11.5", + "@nestjs/swagger": "^4.8.2", + "@nestjs/typeorm": "^6.3.4", + "@types/express-serve-static-core": "^4.19.5", + "ali-oss": "^6.5.1", + "axios": "^0.23.0", + "bcryptjs": "^2.4.3", + "body-parser": "^1.19.0", + "class-transformer": "^0.2.3", + "compression": "^1.7.4", + "cross-env": "^7.0.3", + "date-fns": "^2.17.0", + "deepmerge": "^4.2.2", + "dotenv": "^8.2.0", + "express": "^4.18.2", + "express-rate-limit": "^5.0.0", + "fs-extra": "^10.0.0", + "helmet": "^3.21.2", + "highlight.js": "^9.18.0", + "lodash": "^4.17.21", + "log4js": "^6.1.0", + "marked": "^0.8.0", + "mysql2": "^3.12.0", + "node-ip2region": "^1.0.2", + "nodemailer": "^6.4.2", + "nuid": "^1.1.0", + "open": "^8.2.1", + "passport": "^0.4.1", + "passport-jwt": "^4.0.0", + "pm2": "^5.2.0", + "reflect-metadata": "^0.1.13", + "rimraf": "^3.0.0", + "rxjs": "^6.5.3", + "segment": "^0.1.3", + "sharp": "^0.34.5", + "sqlite3": "^5.1.7", + "swagger-themes": "^1.4.3", + "swagger-ui-express": "^4.1.6", + "typeorm": "^0.2.45", + "ua-parser-js": "^0.7.28" + }, + "bin": { + "reactpress-server": "bin/reactpress-server.js" + }, + "devDependencies": { + "@nestjs/schematics": "^6.7.0", + "@nestjs/testing": "^6.7.1", + "@types/express": "4.17.18", + "@types/jest": "^24.0.18", + "@types/node": "^12.7.5", + "@types/supertest": "^2.0.8", + "@typescript-eslint/eslint-plugin": "^5.21.0", + "@typescript-eslint/parser": "^5.21.0", + "eslint": "^8.15.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-simple-import-sort": "^7.0.0", + "jest": "^24.9.0", + "prettier": "^1.18.2", + "supertest": "^4.0.2", + "ts-jest": "^24.1.0", + "ts-loader": "^6.1.1", + "ts-node": "^8.4.1", + "tsconfig-paths": "^3.9.0", + "tslint": "^5.20.0", + "typescript": "~4.9.5" + }, + "engines": { + "node": ">=18" + } + }, + "../toolkit": { + "name": "@fecommunity/reactpress-toolkit", + "version": "4.0.0-beta.18", + "dependencies": { + "ajv": "^8.17.1", + "axios": "^1.12.2", + "dotenv": "^8.6.0", + "fs-extra": "^10.0.0", + "lodash": "^4.17.21", + "swagger-typescript-api": "^12.0.4" + } + }, + "node_modules/@angular-devkit/core": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-7.3.8.tgz", + "integrity": "sha512-3X9uzaZXFpm5o2TSzhD6wEOtVU32CgeytKjD1Scxj+uMMVo48SWLlKiFh312T+smI9ko7tOT8VqxglwYkWosgg==", + "license": "MIT", + "dependencies": { + "ajv": "6.9.1", + "chokidar": "2.0.4", + "fast-json-stable-stringify": "2.0.0", + "rxjs": "6.3.3", + "source-map": "0.7.3" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", + "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-7.3.8.tgz", + "integrity": "sha512-mvaKoORZIaW/h0VNZ3IQWP0qThRCZRX6869FNlzV0jlW0mhn07XbiIGHCGGSCDRxS7qJ0VbuIVnKXntF+iDeWw==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "7.3.8", + "rxjs": "6.3.3" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-0.13.8.tgz", + "integrity": "sha512-PnVetGOLqONNhKcUfJoCRUJU8BSpcTZpWwQS6YywyNrvyVXtnUi/dgMTQkS8fva3XC/5Ij3Mj7yrfTHaG7M7bw==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "7.3.8", + "@angular-devkit/schematics": "7.3.8", + "@schematics/schematics": "0.13.8", + "inquirer": "6.2.1", + "minimist": "1.2.0", + "rxjs": "6.3.3", + "symbol-observable": "1.2.0" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/inquirer": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz", + "integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.0", + "figures": "^2.0.0", + "lodash": "^4.17.10", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.1.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", + "license": "ISC" + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "license": "MIT", + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/rxjs": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", + "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", + "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cnakazawa/watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + }, + "bin": { + "watch": "cli.js" + }, + "engines": { + "node": ">=0.1.95" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fecommunity/reactpress-toolkit": { + "resolved": "../toolkit", + "link": true + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jest/console": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", + "integrity": "sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/source-map": "^24.9.0", + "chalk": "^2.0.1", + "slash": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@jest/console/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@jest/console/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@jest/core": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-24.9.0.tgz", + "integrity": "sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^24.7.1", + "@jest/reporters": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-changed-files": "^24.9.0", + "jest-config": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-resolve-dependencies": "^24.9.0", + "jest-runner": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "jest-watcher": "^24.9.0", + "micromatch": "^3.1.10", + "p-each-series": "^1.0.0", + "realpath-native": "^1.1.0", + "rimraf": "^2.5.4", + "slash": "^2.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/core/node_modules/ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@jest/core/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@jest/core/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/@jest/core/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@jest/core/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@jest/environment": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.9.0.tgz", + "integrity": "sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/fake-timers": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-24.9.0.tgz", + "integrity": "sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-mock": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/reporters": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-24.9.0.tgz", + "integrity": "sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.2", + "istanbul-lib-coverage": "^2.0.2", + "istanbul-lib-instrument": "^3.0.1", + "istanbul-lib-report": "^2.0.4", + "istanbul-lib-source-maps": "^3.0.1", + "istanbul-reports": "^2.2.6", + "jest-haste-map": "^24.9.0", + "jest-resolve": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.6.0", + "node-notifier": "^5.4.2", + "slash": "^2.0.0", + "source-map": "^0.6.0", + "string-length": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@jest/reporters/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@jest/reporters/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@jest/source-map": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.9.0.tgz", + "integrity": "sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0", + "graceful-fs": "^4.1.15", + "source-map": "^0.6.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/source-map/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/test-result": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.9.0.tgz", + "integrity": "sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/istanbul-lib-coverage": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz", + "integrity": "sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-runner": "^24.9.0", + "jest-runtime": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/transform": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.9.0.tgz", + "integrity": "sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/types": "^24.9.0", + "babel-plugin-istanbul": "^5.1.0", + "chalk": "^2.0.1", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.1.15", + "jest-haste-map": "^24.9.0", + "jest-regex-util": "^24.9.0", + "jest-util": "^24.9.0", + "micromatch": "^3.1.10", + "pirates": "^4.0.1", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "2.4.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@jest/transform/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@jest/transform/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nestjs/cli": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-6.14.2.tgz", + "integrity": "sha512-Bv6ZoziyuXrCmeWMf4rcEjN84O4RmZtQugscFm0k2nOWVfW79eltqMJlpj6h9IutLLC52Je/SOw0XTiDGOukyg==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "7.3.8", + "@angular-devkit/schematics": "7.3.8", + "@angular-devkit/schematics-cli": "0.13.8", + "@nestjs/schematics": "^6.8.2", + "@types/webpack": "4.41.5", + "chalk": "2.4.2", + "cli-table3": "0.5.1", + "commander": "4.1.1", + "copyfiles": "2.2.0", + "fork-ts-checker-webpack-plugin": "4.0.3", + "inquirer": "7.0.4", + "node-emoji": "1.10.0", + "ora": "4.0.3", + "os-name": "3.1.0", + "rimraf": "3.0.1", + "tree-kill": "1.2.2", + "tsconfig-paths": "3.9.0", + "tsconfig-paths-webpack-plugin": "3.2.0", + "typescript": "^3.6.4", + "webpack": "4.41.5", + "webpack-node-externals": "1.7.2" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@nestjs/cli/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@nestjs/cli/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@nestjs/cli/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@nestjs/cli/node_modules/inquirer": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", + "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^2.4.2", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.2.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/inquirer/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@nestjs/cli/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/cli/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/@nestjs/cli/node_modules/rimraf": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.1.tgz", + "integrity": "sha512-IQ4ikL8SjBiEDZfk+DFVwqRK8md24RWMEJkdSlgNLkyyAImcjf8SWvU1qFMDOb4igBClbTQ/ugPqXcRwdFTxZw==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nestjs/cli/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/cli/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@nestjs/cli/node_modules/tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@nestjs/common": { + "version": "6.11.11", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-6.11.11.tgz", + "integrity": "sha512-K4wuK/V2M82AsoudtY0UYV+M1nYDSSb10t8AkMwFiP+AWMuxCJNtE8qLc9jUe2aTKMbhBiQUfsbZFmg/MRinPg==", + "license": "MIT", + "dependencies": { + "axios": "0.19.2", + "cli-color": "2.0.0", + "tslib": "1.11.1", + "uuid": "7.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "reflect-metadata": "^0.1.12", + "rxjs": "^6.0.0" + } + }, + "node_modules/@nestjs/common/node_modules/axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410", + "license": "MIT", + "dependencies": { + "follow-redirects": "1.5.10" + } + }, + "node_modules/@nestjs/common/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@nestjs/common/node_modules/follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "license": "MIT", + "dependencies": { + "debug": "=3.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nestjs/common/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@nestjs/config": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-0.6.3.tgz", + "integrity": "sha512-JxvvUpmH0/WOrTB+zh8dEkxSUQXhB7V3d/qeQXyCnMiEFjaq89+fNFztpWjz4DlOfdS4/eYTzIEy9PH2uGnfzA==", + "license": "MIT", + "dependencies": { + "dotenv": "8.2.0", + "dotenv-expand": "5.1.0", + "lodash.get": "4.4.2", + "lodash.has": "4.5.2", + "lodash.set": "4.3.2", + "uuid": "8.3.2" + }, + "peerDependencies": { + "@nestjs/common": "^6.10.0 || ^7.0.0", + "reflect-metadata": "^0.1.12", + "rxjs": "^6.0.0" + } + }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/config/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@nestjs/core": { + "version": "6.11.11", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-6.11.11.tgz", + "integrity": "sha512-ewUy2rjiRWi6SziI5gXZnlat7PfnVklL3tusnU1qqtUm74cPY1Zre+zDCJ27P/+B7sFJHbkFfpi0qQP2pQv9jQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nuxtjs/opencollective": "0.2.2", + "fast-safe-stringify": "2.0.7", + "iterare": "1.2.0", + "object-hash": "2.0.3", + "path-to-regexp": "3.2.0", + "tslib": "1.11.1", + "uuid": "7.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^6.0.0", + "reflect-metadata": "^0.1.12", + "rxjs": "^6.0.0" + } + }, + "node_modules/@nestjs/jwt": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-6.1.2.tgz", + "integrity": "sha512-+qfcAeAuZiwGRj5WqmDCtzhlY109l2e6QJR4K2ANu1UJskxxaD9O7QS8SGegzpfTaIL01NAF8BWNxwy5ps6Lzg==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "8.3.7", + "jsonwebtoken": "8.5.1" + }, + "peerDependencies": { + "@nestjs/common": "^6.0.0" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-0.4.1.tgz", + "integrity": "sha512-JXrw2LMangSU3vnaXWXVX47GRG1FbbNh4aVBbidDjxT3zlghsoNQY6qyWtT001MCl8lJGo8I6i6+DurBRRxl/Q==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.8", + "class-transformer": "^0.2.0 || ^0.3.0 || ^0.4.0", + "class-validator": "^0.11.1 || ^0.12.0 || ^0.13.0", + "reflect-metadata": "^0.1.12" + } + }, + "node_modules/@nestjs/passport": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-6.2.0.tgz", + "integrity": "sha512-Wy8FeSCNxbfTKRe/mJk0w5hSLsqoNMehg6DUQOoDug+6Uaq+QLEy6wvGPUJbVqGKpJwFHGgUoWp2ag05zh4zpA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^6.0.0", + "passport": "^0.4.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "6.11.11", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-6.11.11.tgz", + "integrity": "sha512-4h3F3hDhNlO5+Ruy6eS+lSL2yIz5r4hF/BB3QkZVOuRdfji9n0gZAIR7tuSLTizqYxaHYRZ7dBv+PscQS/7ztQ==", + "license": "MIT", + "dependencies": { + "body-parser": "1.19.0", + "cors": "2.8.5", + "express": "4.17.1", + "multer": "1.4.2", + "tslib": "1.11.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^6.0.0", + "@nestjs/core": "^6.0.0" + } + }, + "node_modules/@nestjs/platform-express/node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@nestjs/platform-express/node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@nestjs/platform-express/node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/platform-express/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/platform-express/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/@nestjs/platform-express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@nestjs/platform-express/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/platform-express/node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", + "license": "MIT" + }, + "node_modules/@nestjs/platform-express/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@nestjs/platform-express/node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/@nestjs/platform-express/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@nestjs/platform-express/node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/platform-express/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/@nestjs/platform-express/node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "license": "MIT" + }, + "node_modules/@nestjs/platform-express/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@nestjs/platform-express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@nestjs/platform-express/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@nestjs/platform-express/node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "license": "MIT" + }, + "node_modules/@nestjs/platform-express/node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/@nestjs/platform-express/node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@nestjs/platform-express/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/@nestjs/platform-express/node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@nestjs/platform-express/node_modules/send/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "license": "MIT" + }, + "node_modules/@nestjs/platform-express/node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@nestjs/platform-express/node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "license": "ISC" + }, + "node_modules/@nestjs/platform-express/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/platform-express/node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/@nestjs/schematics": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-6.9.4.tgz", + "integrity": "sha512-tZoLDboqXQT+DcWUFTJyDsOIg9Qt6ilXYWWbrbbEPCP2+UKjfsqZPsEnG31DOj0IbovFS5g+Mbn3IanVsEcIsQ==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "7.3.8", + "@angular-devkit/schematics": "7.3.8", + "fs-extra": "8.1.0" + }, + "peerDependencies": { + "typescript": "^3.4.5" + } + }, + "node_modules/@nestjs/schematics/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@nestjs/schematics/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@nestjs/schematics/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@nestjs/swagger": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-4.8.2.tgz", + "integrity": "sha512-RSUwcVxrzXF7/b/IZ5lXnYHJ6jIGS9wWRTJKIt1kIaCNWT+0wRfTlAyhQkzs2g35/PTXJEcdIwwY7mBO/bwHzw==", + "license": "MIT", + "dependencies": { + "@nestjs/mapped-types": "0.4.1", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0" + }, + "peerDependencies": { + "@nestjs/common": "^6.8.0 || ^7.0.0", + "@nestjs/core": "^6.8.0 || ^7.0.0", + "fastify-swagger": "*", + "reflect-metadata": "^0.1.12", + "swagger-ui-express": "*" + }, + "peerDependenciesMeta": { + "fastify-swagger": { + "optional": true + }, + "swagger-ui-express": { + "optional": true + } + } + }, + "node_modules/@nestjs/swagger/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/@nestjs/testing": { + "version": "6.11.11", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-6.11.11.tgz", + "integrity": "sha512-Mqu4IWZhnLdIFfVfueAFFCm3jPfQoWg+YmDLBsFS2kpab3az5gsRCZQv9R9CCHGa1hHKYWqM1lMcz1IQc70kww==", + "dev": true, + "license": "MIT", + "dependencies": { + "optional": "0.1.4", + "tslib": "1.11.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^6.0.0", + "@nestjs/core": "^6.0.0" + } + }, + "node_modules/@nestjs/typeorm": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-6.3.4.tgz", + "integrity": "sha512-qMUHaTMo+U5WOlMfL0ogNm8C2T/Kej/v+NnjSixx/UmtluLvTbNYuZUfbHI9ePCmtCXDV0lMRvIAi+U5LCjsbA==", + "license": "MIT", + "dependencies": { + "uuid": "^7.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^6.7.0", + "@nestjs/core": "^6.7.0", + "reflect-metadata": "^0.1.12", + "rxjs": "^6.0.0", + "typeorm": "^0.2.7" + } + }, + "node_modules/@nestjs/typeorm/node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.2.2.tgz", + "integrity": "sha512-69gFVDs7mJfNjv9Zs5DFVD+pvBW+k1TaHSOqUWqAyTTfLcKI/EMYQgvEvziRd+zAFtUOoye6MfWh0qvinGISPw==", + "license": "MIT", + "dependencies": { + "chalk": "^2.4.1", + "consola": "^2.3.0", + "node-fetch": "^2.3.0" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@nuxtjs/opencollective/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@nuxtjs/opencollective/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@pm2/agent": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.0.4.tgz", + "integrity": "sha512-n7WYvvTJhHLS2oBb1PjOtgLpMhgImOq8sXkPBw6smeg9LJBWZjiEgPKOpR8mn9UJZsB5P3W4V/MyvNnp31LKeA==", + "license": "AGPL-3.0", + "dependencies": { + "async": "~3.2.0", + "chalk": "~3.0.0", + "dayjs": "~1.8.24", + "debug": "~4.3.1", + "eventemitter2": "~5.0.1", + "fast-json-patch": "^3.0.0-1", + "fclone": "~1.0.11", + "nssocket": "0.6.0", + "pm2-axon": "~4.0.1", + "pm2-axon-rpc": "~0.7.0", + "proxy-agent": "~6.3.0", + "semver": "~7.5.0", + "ws": "~7.5.10" + } + }, + "node_modules/@pm2/agent/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@pm2/agent/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@pm2/agent/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@pm2/agent/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/@pm2/agent/node_modules/dayjs": { + "version": "1.8.36", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.36.tgz", + "integrity": "sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==", + "license": "MIT" + }, + "node_modules/@pm2/agent/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@pm2/agent/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/agent/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/agent/node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@pm2/agent/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/@pm2/io": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.0.1.tgz", + "integrity": "sha512-KiA+shC6sULQAr9mGZ1pg+6KVW9MF8NpG99x26Lf/082/Qy8qsTCtnJy+HQReW1A9Rdf0C/404cz0RZGZro+IA==", + "license": "Apache-2", + "dependencies": { + "async": "~2.6.1", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "require-in-the-middle": "^5.0.0", + "semver": "~7.5.4", + "shimmer": "^1.2.0", + "signal-exit": "^3.0.3", + "tslib": "1.9.3" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@pm2/io/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/@pm2/io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@pm2/io/node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "license": "MIT" + }, + "node_modules/@pm2/io/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/io/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/io/node_modules/tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "license": "Apache-2.0" + }, + "node_modules/@pm2/io/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/@pm2/js-api": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@pm2/js-api/-/js-api-0.8.1.tgz", + "integrity": "sha512-n9tDOz1ojyDOs05XthEXrLFVQYbbh2oAN19UakLPyEZDrUyEq05h8wIZU8+dNXBQY/KeFlWMLVA76nnX52ofRg==", + "license": "Apache-2", + "dependencies": { + "async": "^2.6.3", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "extrareqp2": "^1.0.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@pm2/js-api/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/@pm2/js-api/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@pm2/js-api/node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "license": "MIT" + }, + "node_modules/@pm2/js-api/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@pm2/pm2-version-check": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", + "integrity": "sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@schematics/schematics": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/@schematics/schematics/-/schematics-0.13.8.tgz", + "integrity": "sha512-7Bqw2DzCbt7EkR0IYDEUXJ6WQjE90NqSMFqK0Lylps0WswJfjUq5axJduz5LwbmrIDhWdDhXMtI4QimUBm4Qsw==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "7.3.8", + "@angular-devkit/schematics": "7.3.8" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/anymatch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", + "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.18", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", + "integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.9.1.tgz", + "integrity": "sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-diff": "^24.3.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "8.3.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.3.7.tgz", + "integrity": "sha512-B5SSifLkjB0ns7VXpOOtOUlynE78/hKcY8G8pOAhkLJZinwofIBYqz555nRj2W9iDWZqFhK5R+7NZDaRmKWAoQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/source-list-map": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz", + "integrity": "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==", + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.10", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.10.tgz", + "integrity": "sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.16.tgz", + "integrity": "sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/superagent": "*" + } + }, + "node_modules/@types/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-oMnbAXeVo+KUnje3hzdORXUbfnzTfqD0H92mLl19NE5hFqH9Q4ktq+xehNSxcNeeLm1COopYwa0zeP6Iz+oIXg==", + "license": "MIT", + "dependencies": { + "tapable": "^2.3.0" + } + }, + "node_modules/@types/uglify-js": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz", + "integrity": "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==", + "license": "MIT", + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/@types/uglify-js/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@types/webpack": { + "version": "4.41.5", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.5.tgz", + "integrity": "sha512-693JfV/83UZxpQY8vutDSwkDjNujy2327UrFqQciJWXh761B/aUIZIM5N05IRIZ17WwsG8VfUSE3edwXvkehiQ==", + "license": "MIT", + "dependencies": { + "@types/anymatch": "*", + "@types/node": "*", + "@types/tapable": "*", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "source-map": "^0.6.0" + } + }, + "node_modules/@types/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + } + }, + "node_modules/@types/webpack/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@types/yargs": { + "version": "13.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.12.tgz", + "integrity": "sha512-qCxJE1qgz2y0hA4pIxjBR+PelCH0U5CK1XJXFwCNqfmliatKp47UCXXE9Dyk1OXBDLvsCF57TqQEJaeLfDYEOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/zen-observable": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz", + "integrity": "sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", + "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", + "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", + "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", + "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-code-frame": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", + "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "node_modules/@webassemblyjs/helper-fsm": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", + "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", + "license": "ISC" + }, + "node_modules/@webassemblyjs/helper-module-context": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", + "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "mamacro": "^0.0.3" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", + "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", + "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", + "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", + "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", + "license": "MIT", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", + "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", + "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/helper-wasm-section": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-opt": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", + "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", + "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", + "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "node_modules/@webassemblyjs/wast-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", + "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/floating-point-hex-parser": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-code-frame": "1.8.5", + "@webassemblyjs/helper-fsm": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", + "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + } + }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.3.tgz", + "integrity": "sha512-yqXL+k5rr8+ZRpOAntkaaRgWgE5o8ESAj5DyRmVTCSoZxXmqemb9Dd7T4i5UzwuERdLAJUy6XzR9zFVuf0kzkw==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", + "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "node_modules/ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "license": "MIT", + "peerDependencies": { + "ajv": ">=5.0.0" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ali-oss": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/ali-oss/-/ali-oss-6.23.0.tgz", + "integrity": "sha512-FipRmyd16Pr/tEey/YaaQ/24Pc3HEpLM9S1DRakEuXlSLXNIJnu1oJtHM53eVYpvW3dXapSjrip3xylZUTIZVQ==", + "license": "MIT", + "dependencies": { + "address": "^1.2.2", + "agentkeepalive": "^3.4.1", + "bowser": "^1.6.0", + "copy-to": "^2.0.1", + "dateformat": "^2.0.0", + "debug": "^4.3.4", + "destroy": "^1.0.4", + "end-or-error": "^1.0.1", + "get-ready": "^1.0.0", + "humanize-ms": "^1.2.0", + "is-type-of": "^1.4.0", + "js-base64": "^2.5.2", + "jstoxml": "^2.0.0", + "lodash": "^4.17.21", + "merge-descriptors": "^1.0.1", + "mime": "^2.4.5", + "platform": "^1.3.1", + "pump": "^3.0.0", + "qs": "^6.4.0", + "sdk-base": "^2.0.1", + "stream-http": "2.8.2", + "stream-wormhole": "^1.0.4", + "urllib": "^2.44.0", + "utility": "^1.18.0", + "xml2js": "^0.6.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/amp": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", + "integrity": "sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw==", + "license": "MIT" + }, + "node_modules/amp-message": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/amp-message/-/amp-message-0.1.2.tgz", + "integrity": "sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg==", + "license": "MIT", + "dependencies": { + "amp": "0.3.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "license": "ISC", + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/are-we-there-yet/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-equal": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.2.tgz", + "integrity": "sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.reduce": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.8.tgz", + "integrity": "sha512-DwuEqgXFBwbmZSRqt3BpQigWNUoqw9Ml2dTWdF3B2zQlQX4OeUE0zyuzX0fX0IbTvjdkZbcBTU3idgpO78qkTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-array-method-boxes-properly": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "is-string": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, + "node_modules/assert": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz", + "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==", + "license": "MIT", + "dependencies": { + "object.assign": "^4.1.4", + "util": "^0.10.4" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assert/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/assert/node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-types/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/async-each": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", + "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz", + "integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.4" + } + }, + "node_modules/babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", + "license": "MIT", + "dependencies": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "node_modules/babel-code-frame/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/babel-jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz", + "integrity": "sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/babel__core": "^7.1.0", + "babel-plugin-istanbul": "^5.1.0", + "babel-preset-jest": "^24.9.0", + "chalk": "^2.4.2", + "slash": "^2.0.0" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-jest/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", + "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "find-up": "^3.0.0", + "istanbul-lib-instrument": "^3.3.0", + "test-exclude": "^5.2.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz", + "integrity": "sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-preset-jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz", + "integrity": "sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-object-rest-spread": "^7.0.0", + "babel-plugin-jest-hoist": "^24.9.0" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "license": "MIT", + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bl/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/blessed": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz", + "integrity": "sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==", + "license": "MIT", + "bin": { + "blessed": "bin/tput.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", + "license": "MIT" + }, + "node_modules/bodec": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bodec/-/bodec-0.1.0.tgz", + "integrity": "sha512-Ylo+MAo5BDUq1KA3f3R/MFhh+g8cnHmo8bz3YPGhI1znrMaf77ol1sfvYJzsw3nTE+Y2GryfDxBaR+AqpAkEHQ==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "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.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/bowser": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz", + "integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, + "node_modules/browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "1.1.7" + } + }, + "node_modules/browser-resolve/node_modules/resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "license": "MIT", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "license": "MIT", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.6.tgz", + "integrity": "sha512-sd+Q65fjlWCYWtZKXiKfrUc8d+4jtp/8f0W2NkwzLtoW4bI6UDnWusLWIurHnmurW0XShIRxpwiOX4EoPtXUAg==", + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.3", + "browserify-rsa": "^4.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.6.1", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.9", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/browserify-zlib/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/browserslist": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==", + "dependencies": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "license": "MIT", + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", + "integrity": "sha512-W2lPwkBkMZwFlPCXhIlYgxu+7gC/NUlCtdK652DAJ1JdgV0sTrvuPFshNPrFa1TY2JOkLhgdeEBplB4ezEa+xg==", + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "license": "ISC", + "dependencies": { + "rsvp": "^4.8.4" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/chalk/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "license": "MIT" + }, + "node_modules/charm": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/charm/-/charm-0.1.2.tgz", + "integrity": "sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ==", + "license": "MIT/X11" + }, + "node_modules/chokidar": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "license": "MIT", + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.5" + }, + "optionalDependencies": { + "fsevents": "^1.2.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/class-transformer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.2.3.tgz", + "integrity": "sha512-qsP+0xoavpOlJHuYsQJsN58HXSl8Jvveo+T37rEvCEeRfMWoytAyR0Ua/YsFgpM6AZYZ/og2PJwArwzJl1aXtQ==", + "license": "MIT" + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.8.tgz", + "integrity": "sha512-SceYGWXvdqlWa/OnQ5FQuV+NxvNmMRhMw/w9AHkH71hTzveND4BTYgvp16g+oITK47qbOl/3D0bl0iygehWAWQ==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.0.tgz", + "integrity": "sha512-a0VZ8LeraW0jTuCkuAGMNufareGHhyZU9z8OGsW0gXd1hZGi1SRuNRXdbGkraBBKnhyUhyebFWnRbp+dIn0f0A==", + "license": "ISC", + "dependencies": { + "ansi-regex": "^2.1.1", + "d": "^1.0.1", + "es5-ext": "^0.10.51", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.14", + "timers-ext": "^0.1.7" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-highlight/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cli-highlight/node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/cli-highlight/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight/node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "license": "MIT" + }, + "node_modules/cli-highlight/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cli-highlight/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "colors": "^1.1.2" + } + }, + "node_modules/cli-tableau": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cli-tableau/-/cli-tableau-2.0.1.tgz", + "integrity": "sha512-he+WTicka9cl0Fg/y+YyxcN6/bfQ/1O3QmgxRXDhABKqLzvoOSM4fMzp39uMyLBulAFuywD2N7UaoQE7WaADxQ==", + "dependencies": { + "chalk": "3.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/cli-tableau/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-tableau/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-tableau/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cli-tableau/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "license": "ISC" + }, + "node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "license": "ISC", + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "license": "MIT", + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-security-policy-builder": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz", + "integrity": "sha512-/MtLWhJVvJNkA9dVLAp6fg9LxD2gfI6R2Fi1hPmfjYXSahJJzcfvoeDOxSyp4NvxMuwWv3WMssE9o31DoULHrQ==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "node_modules/copy-concurrently/node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "license": "ISC" + }, + "node_modules/copy-concurrently/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==", + "license": "MIT" + }, + "node_modules/copyfiles": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.2.0.tgz", + "integrity": "sha512-iJbHJI+8OKqsq+4JF0rqgRkZzo++jqO6Wf4FUU1JM41cJF6JcY5968XyF4tm3Kkm7ZOMrqlljdm8N9oyY5raGw==", + "license": "MIT", + "dependencies": { + "glob": "^7.0.5", + "minimatch": "^3.0.3", + "mkdirp": "^0.5.1", + "noms": "0.0.0", + "through2": "^2.0.1", + "yargs": "^13.2.4" + }, + "bin": { + "copyfiles": "copyfiles", + "copyup": "copyfiles" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/croner": { + "version": "4.1.97", + "resolved": "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz", + "integrity": "sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==", + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", + "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "0.3.x" + } + }, + "node_modules/culvert": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/culvert/-/culvert-0.1.2.tgz", + "integrity": "sha512-yi1x3EAWKjQTreYWeSd98431AV+IEE0qoDyOoaHJ7KJ21gv6HtBXHVLX74opVSGqcR8/AbjJBHAHpcOy2bj5Gg==", + "license": "MIT" + }, + "node_modules/cyclist": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.2.tgz", + "integrity": "sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==", + "license": "MIT" + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dasherize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", + "integrity": "sha512-APql/TZ6FdLEpf2z7/X2a2zyqK8juYtqaSVqxw9mYoQ64CXkfU15AeLh8pUszT8+fnYjgm6t0aIYpWKJbnLkuA==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dateformat": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", + "integrity": "sha512-GODcnWq3YGoTnygPfi02ygEiRxqUxpJwuRHjdhJYuxpcZmDq4rjBiXYmbCCzStxo176ixfLT6i4NPwQooRySnw==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-user-agent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-user-agent/-/default-user-agent-1.0.0.tgz", + "integrity": "sha512-bDF7bg6OSNcSwFWPu4zYKpVkJZQYVrAANMYB8bc9Szem1D0yKdm4sa/rOCs2aC9+2GMqQ7KnwtZRvDhmLF0dXw==", + "license": "MIT", + "dependencies": { + "os-name": "~1.0.3" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/default-user-agent/node_modules/os-name": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-1.0.3.tgz", + "integrity": "sha512-f5estLO2KN8vgtTRaILIgEGBoBrMnZ3JQ7W9TMZCnOIGwHe8TRGSpcagnWDo+Dfhd/z08k9Xe75hvciJJ8Qaew==", + "license": "MIT", + "dependencies": { + "osx-release": "^1.0.0", + "win-release": "^1.0.0" + }, + "bin": { + "os-name": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/degenerator/node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/degenerator/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/degenerator/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==", + "dependencies": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, + "node_modules/digest-header": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/digest-header/-/digest-header-1.1.0.tgz", + "integrity": "sha512-glXVh42vz40yZb9Cq2oMOt70FIoWiv+vxNvdKdU8CwjLad25qHM3trLxhl9bVjdr6WaslIXhWpn0NO8T/67Qjg==", + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "license": "MIT", + "engines": { + "node": ">=0.4", + "npm": ">=1.2" + } + }, + "node_modules/domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/dont-sniff-mimetype": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz", + "integrity": "sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "license": "BSD-2-Clause" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/duplexify/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "dev": true, + "license": "ISC" + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/end-or-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/end-or-error/-/end-or-error-1.0.1.tgz", + "integrity": "sha512-OclLMSug+k2A0JKuf494im25ANRBVW8qsjmwbgX7lQ8P82H21PQ1PWkoYwb9y5yMBS69BPlwtzdIFClo3+7kOQ==", + "license": "MIT", + "engines": { + "node": ">= 0.11.14" + } + }, + "node_modules/enhanced-resolve": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "dependencies": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/enhanced-resolve/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "license": "MIT", + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/escodegen/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", + "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.5.tgz", + "integrity": "sha512-9Ni+xgemM2IWLq6aXEpP2+V/V30GeA/46Ar629vcMqVPodFFWC9skHu/D1phvuqtS8bJCFnNf01/qcmqYEwNfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-simple-import-sort": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-7.0.0.tgz", + "integrity": "sha512-U3vEDB5zhYPNfxT5TYR7u01dboFZp+HNpnGhkDB2g/2E4wZ/g1Q9Ton8UwCLfRV9yAKyYqDh62oHOamvkFxsvw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/eventemitter2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", + "integrity": "sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "license": "MIT", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/exec-sh": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", + "integrity": "sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "license": "MIT", + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.8.tgz", + "integrity": "sha512-SceYGWXvdqlWa/OnQ5FQuV+NxvNmMRhMw/w9AHkH71hTzveND4BTYgvp16g+oITK47qbOl/3D0bl0iygehWAWQ==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/expand-brackets/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-24.9.0.tgz", + "integrity": "sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^24.9.0", + "ansi-styles": "^3.2.0", + "jest-get-type": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-regex-util": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "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.0", + "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.1", + "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.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.1.tgz", + "integrity": "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg==", + "license": "MIT" + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "license": "MIT", + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extrareqp2": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/extrareqp2/-/extrareqp2-1.0.0.tgz", + "integrity": "sha512-Gum0g1QYb6wpPJCVypWP3bbIuaibcFiJcpuPM10YSXp/tzqi84x9PJageob+eN4xVRIOto4wjSGNLyMD54D2xA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-glob/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-glob/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/fast-glob/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/fast-glob/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha512-eIgZvM9C3P05kg0qxfqaVU6Tma4QedCPIByQOcemV0vju8ot3cS2DpHi4m2G2JvbSMI152rjfLX0p1pkSdyPlQ==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fclone": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz", + "integrity": "sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==", + "license": "MIT" + }, + "node_modules/feature-policy": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/feature-policy/-/feature-policy-0.3.0.tgz", + "integrity": "sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/figgy-pudding": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", + "deprecated": "This module is no longer supported.", + "license": "ISC" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "license": "ISC" + }, + "node_modules/flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "node_modules/flush-write-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/flush-write-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/flush-write-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/flush-write-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.0.3.tgz", + "integrity": "sha512-5hGeMYKg817Hp6rvdc2EOS/T/Cq0JW9LLJDZtVUPlNIojIuP7YbOAdrHEk4Irw1097YQUr56kWIiYhqNPzfNzQ==", + "license": "MIT", + "dependencies": { + "babel-code-frame": "^6.22.0", + "chalk": "^2.4.1", + "micromatch": "^3.1.10", + "minimatch": "^3.0.4", + "semver": "^5.6.0", + "tapable": "^1.0.0", + "worker-rpc": "^0.1.0" + }, + "engines": { + "node": ">=6.11.5", + "yarn": ">=1.0.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", + "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/formstream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/formstream/-/formstream-1.5.2.tgz", + "integrity": "sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==", + "license": "MIT", + "dependencies": { + "destroy": "^1.0.4", + "mime": "^2.5.2", + "node-hex": "^1.0.1", + "pause-stream": "~0.0.11" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "license": "MIT", + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/from2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/from2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/from2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/from2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-ready": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-ready/-/get-ready-1.0.0.tgz", + "integrity": "sha512-mFXCZPJIlcYcth+N8267+mghfYN9h3EhsDa6JSnbA3Wrhh/XFpuowviFcsDeYZtKspQyWyJqfs4O6P8CHeTwzw==", + "license": "MIT" + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/git-node-fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/git-node-fs/-/git-node-fs-1.0.0.tgz", + "integrity": "sha512-bLQypt14llVXBg0S0u8q8HmU7g9p3ysH+NvVlae5vILuUvs759665HvmR5+wb04KjHyjFcDRxdYb4kyNnluMUQ==", + "license": "MIT" + }, + "node_modules/git-sha1": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/git-sha1/-/git-sha1-0.1.2.tgz", + "integrity": "sha512-2e/nZezdVlyCopOCYHeW0onkbZg7xP1Ad6pndPy1rCygeRykefUS6r7oA5cJRGEFvseiaz5a/qUHFVX1dd6Isg==", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "license": "ISC", + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", + "dev": true, + "license": "MIT" + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/har-validator/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/har-validator/node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "license": "MIT", + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "3.23.3", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.23.3.tgz", + "integrity": "sha512-U3MeYdzPJQhtvqAVBPntVgAvNSOJyagwZwyKsFdyRa8TV3pOKVFljalPOCxbw5Wwf2kncGhmP0qHjyazIdNdSA==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "dont-sniff-mimetype": "1.1.0", + "feature-policy": "0.3.0", + "helmet-crossdomain": "0.4.0", + "helmet-csp": "2.10.0", + "hide-powered-by": "1.1.0", + "hpkp": "2.0.0", + "hsts": "2.2.0", + "nocache": "2.1.0", + "referrer-policy": "1.2.0", + "x-xss-protection": "1.3.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/helmet-crossdomain": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz", + "integrity": "sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/helmet-csp": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.10.0.tgz", + "integrity": "sha512-Rz953ZNEFk8sT2XvewXkYN0Ho4GEZdjAZy4stjiEQV3eN7GDxg1QKmYggH7otDyIA7uGA6XnUMVSgeJwbR5X+w==", + "license": "MIT", + "dependencies": { + "bowser": "2.9.0", + "camelize": "1.0.0", + "content-security-policy-builder": "2.1.0", + "dasherize": "2.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/helmet-csp/node_modules/bowser": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.9.0.tgz", + "integrity": "sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA==", + "license": "MIT" + }, + "node_modules/hide-powered-by": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.1.0.tgz", + "integrity": "sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/highlight.js": { + "version": "9.18.5", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.5.tgz", + "integrity": "sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==", + "deprecated": "Support has ended for 9.x series. Upgrade to @latest", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/hpkp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz", + "integrity": "sha512-TaZpC6cO/k3DFsjfzz1LnOobbVSq+J+7WpJxrVtN4L+8+BPQj8iBDRB2Dx49613N+e7/+ZSQ9ra+xZm7Blf4wg==", + "license": "MIT" + }, + "node_modules/hsts": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.2.0.tgz", + "integrity": "sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^1.0.1" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==", + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.2.tgz", + "integrity": "sha512-AIbwAcazqP3R65dGvqk1V+a+vE5Fg1yu/ZKMOiBWSUIXXiwQkYmXQcVa2O0nh0tSDKDFKxG2mY7dB1Sr4hEP1g==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-class-hotfix": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/is-class-hotfix/-/is-class-hotfix-0.0.6.tgz", + "integrity": "sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-descriptor": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.4.tgz", + "integrity": "sha512-bv5z95W0dDtLfKwDfkTNxaRxmISBD3eQBKJeVxv2AQ7MjuUnDNG7cIQqvFtMOUYhsILWHhMayWdoGqNqYYYjww==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.2", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/iterare": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.0.tgz", + "integrity": "sha512-RxMV9p/UzdK0Iplnd8mVgRvNdXlsTOiuDrqMRnDi3wIhbT+JP4xDquAX9ay13R3CH72NBzQ91KWe0+C168QAyQ==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", + "integrity": "sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-local": "^2.0.0", + "jest-cli": "^24.9.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-changed-files": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.9.0.tgz", + "integrity": "sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^24.9.0", + "execa": "^1.0.0", + "throat": "^4.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-cli": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-24.9.0.tgz", + "integrity": "sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "import-local": "^2.0.0", + "is-ci": "^2.0.0", + "jest-config": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "prompts": "^2.0.1", + "realpath-native": "^1.1.0", + "yargs": "^13.3.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-config": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-24.9.0.tgz", + "integrity": "sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^24.9.0", + "@jest/types": "^24.9.0", + "babel-jest": "^24.9.0", + "chalk": "^2.0.1", + "glob": "^7.1.1", + "jest-environment-jsdom": "^24.9.0", + "jest-environment-node": "^24.9.0", + "jest-get-type": "^24.9.0", + "jest-jasmine2": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "micromatch": "^3.1.10", + "pretty-format": "^24.9.0", + "realpath-native": "^1.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-config/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-diff": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.0.1", + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-docblock": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.9.0.tgz", + "integrity": "sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^2.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-each": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.9.0.tgz", + "integrity": "sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "jest-get-type": "^24.9.0", + "jest-util": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz", + "integrity": "sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0", + "jsdom": "^11.5.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-node": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-24.9.0.tgz", + "integrity": "sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-haste-map": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.9.0.tgz", + "integrity": "sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^24.9.0", + "anymatch": "^2.0.0", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.1.15", + "invariant": "^2.2.4", + "jest-serializer": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.9.0", + "micromatch": "^3.1.10", + "sane": "^4.0.3", + "walker": "^1.0.7" + }, + "engines": { + "node": ">= 6" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/jest-jasmine2": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz", + "integrity": "sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "co": "^4.6.0", + "expect": "^24.9.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "pretty-format": "^24.9.0", + "throat": "^4.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-jasmine2/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-jasmine2/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-leak-detector": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz", + "integrity": "sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-matcher-utils": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz", + "integrity": "sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.0.1", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-message-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.9.0.tgz", + "integrity": "sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^2.0.1", + "micromatch": "^3.1.10", + "slash": "^2.0.0", + "stack-utils": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-message-util/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-mock": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-24.9.0.tgz", + "integrity": "sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz", + "integrity": "sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-resolve": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-24.9.0.tgz", + "integrity": "sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^24.9.0", + "browser-resolve": "^1.11.3", + "chalk": "^2.0.1", + "jest-pnp-resolver": "^1.2.1", + "realpath-native": "^1.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz", + "integrity": "sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-snapshot": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-runner": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-24.9.0.tgz", + "integrity": "sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.4.2", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-config": "^24.9.0", + "jest-docblock": "^24.3.0", + "jest-haste-map": "^24.9.0", + "jest-jasmine2": "^24.9.0", + "jest-leak-detector": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-resolve": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.6.0", + "source-map-support": "^0.5.6", + "throat": "^4.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-runtime": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-24.9.0.tgz", + "integrity": "sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.9.0", + "@jest/source-map": "^24.3.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/yargs": "^13.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.1.15", + "jest-config": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "strip-bom": "^3.0.0", + "yargs": "^13.3.0" + }, + "bin": { + "jest-runtime": "bin/jest-runtime.js" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-runtime/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-serializer": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-24.9.0.tgz", + "integrity": "sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-snapshot": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-24.9.0.tgz", + "integrity": "sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "expect": "^24.9.0", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-resolve": "^24.9.0", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^24.9.0", + "semver": "^6.2.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.9.0.tgz", + "integrity": "sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/source-map": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "callsites": "^3.0.0", + "chalk": "^2.0.1", + "graceful-fs": "^4.1.15", + "is-ci": "^2.0.0", + "mkdirp": "^0.5.1", + "slash": "^2.0.0", + "source-map": "^0.6.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-util/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-util/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-validate": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.9.0.tgz", + "integrity": "sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^24.9.0", + "camelcase": "^5.3.1", + "chalk": "^2.0.1", + "jest-get-type": "^24.9.0", + "leven": "^3.1.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-watcher": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-24.9.0.tgz", + "integrity": "sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/yargs": "^13.0.0", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "jest-util": "^24.9.0", + "string-length": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-watcher/node_modules/ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-worker": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", + "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "merge-stream": "^2.0.0", + "supports-color": "^6.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/js-base64": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", + "license": "BSD-3-Clause" + }, + "node_modules/js-git": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/js-git/-/js-git-0.7.8.tgz", + "integrity": "sha512-+E5ZH/HeRnoc/LW0AmAyhU+mNcWBzAKE+30+IDMLSLbbK+Tdt02AdkOKq9u15rlJsDEGFqtgckc8ZM59LhhiUA==", + "license": "MIT", + "dependencies": { + "bodec": "^0.1.0", + "culvert": "^0.1.2", + "git-sha1": "^0.1.2", + "pako": "^0.2.5" + } + }, + "node_modules/js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.0", + "acorn": "^5.5.3", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": "^1.0.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.1", + "escodegen": "^1.9.1", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.3.0", + "nwsapi": "^2.0.7", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.87.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.4", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^5.2.0", + "xml-name-validator": "^3.0.0" + } + }, + "node_modules/jsdom/node_modules/acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jstoxml": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/jstoxml/-/jstoxml-2.2.9.tgz", + "integrity": "sha512-OYWlK0j+roh+eyaMROlNbS5cd5R25Y+IUpdl7cNdB8HNrkgwQzIS7L9MegxOiWNBj9dQhA/yAxiMwCC5mwNoBw==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lazy": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/lazy/-/lazy-1.0.11.tgz", + "integrity": "sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA==", + "license": "MIT", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", + "deprecated": "use String.prototype.padStart()", + "dev": true, + "license": "WTFPL" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "license": "MIT", + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/loader-utils/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, + "node_modules/lodash.has": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", + "integrity": "sha512-rnYUdIo6xRCJnQmbVFEwcxF144erlD+M3YcJUVesflU9paQaE8p+fJDcIQrlMYbxoANFL+AB9hZrzSBBk5PL+g==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==", + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/macos-release": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz", + "integrity": "sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", + "license": "MIT" + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "license": "MIT", + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/marked": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.8.2.tgz", + "integrity": "sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw==", + "license": "MIT", + "bin": { + "marked": "bin/marked" + }, + "engines": { + "node": ">= 8.16.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "license": "MIT", + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/memory-fs/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/memory-fs/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/memory-fs/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/memory-fs/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/microevent.ts": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", + "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==", + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "license": "MIT", + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha512-7Wl+Jz+IGWuSdgsQEJ4JunV0si/iMhg42MnQQG6h1R6TNeVenp4U9x5CC5v/gYqz/fENLQITAWXidNtVL0NNbw==", + "license": "MIT" + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "license": "BSD-2-Clause", + "dependencies": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "license": "MIT", + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-deep/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mkdirp/node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, + "node_modules/move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "node_modules/move-concurrently/node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "license": "ISC" + }, + "node_modules/move-concurrently/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", + "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", + "deprecated": "Multer 1.x is affected by CVE-2022-24434. This is fixed in v1.4.4-lts.1 which drops support for versions of Node.js before 6. Please upgrade to at least Node.js 6 and version 1.4.4-lts.1 of Multer. If you need support for older versions of Node.js, we are open to accepting patches that would fix the CVE on the main 1.x release line, whilst maintaining compatibility with Node.js 0.10.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "license": "ISC" + }, + "node_modules/mysql2": { + "version": "3.22.6", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.6.tgz", + "integrity": "sha512-fPKmeDGUzvFP7bMD5SASlJ5zIgvCC4hbanTmhbUlEmhyrY1hR4Hi3xLOWgTd3luYjLVifx6uGvMNJ2m/LlqEpg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.4.0" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nan": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", + "license": "MIT", + "optional": true + }, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "license": "MIT", + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/needle": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "license": "MIT" + }, + "node_modules/nocache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", + "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", + "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", + "license": "MIT", + "dependencies": { + "lodash.toarray": "^4.4.0" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/node-hex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/node-hex/-/node-hex-1.0.1.tgz", + "integrity": "sha512-iwpZdvW6Umz12ICmu9IYPRxg0tOLGmU3Tq2tKetejCj3oZd7b2nUXwP3a7QA5M9glWy8wlPS1G3RwM/CdsUbdQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-ip2region": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/node-ip2region/-/node-ip2region-1.0.2.tgz", + "integrity": "sha512-tH/A46cbCSeE+G0w8cwPeQDV3EdVp5D900VLgDbjhC4bH7SKVPBOarpjH1He6QvWFBOam48lIroKTAyVUPC3CA==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "license": "MIT", + "dependencies": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + } + }, + "node_modules/node-libs-browser/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/node-libs-browser/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/node-libs-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, + "node_modules/node-libs-browser/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/node-libs-browser/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/node-libs-browser/node_modules/readable-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/node-libs-browser/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/node-notifier": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.5.tgz", + "integrity": "sha512-tVbHs7DyTLtzOiN78izLA85zRqB9NvEXkAf014Vx3jtSvn/xBl6bR8ZYifj+dFcFrKI21huSQgJZ6ZtL3B4HfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "growly": "^1.3.0", + "is-wsl": "^1.1.0", + "semver": "^5.5.0", + "shellwords": "^0.1.1", + "which": "^1.3.0" + } + }, + "node_modules/node-notifier/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/node-notifier/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/noms": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", + "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", + "license": "ISC", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "~1.0.31" + } + }, + "node_modules/noms/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nssocket": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/nssocket/-/nssocket-0.6.0.tgz", + "integrity": "sha512-a9GSOIql5IqgWJR3F/JXG4KpJTA3Z53Cj0MeMvGpglytB1nxE4PdFNC0jINe27CS7cGivoynwc054EzCcT3M3w==", + "license": "MIT", + "dependencies": { + "eventemitter2": "~0.4.14", + "lazy": "~1.0.11" + }, + "engines": { + "node": ">= 0.10.x" + } + }, + "node_modules/nssocket/node_modules/eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==", + "license": "MIT" + }, + "node_modules/nuid": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/nuid/-/nuid-1.1.6.tgz", + "integrity": "sha512-Eb3CPCupYscP1/S1FQcO5nxtu6l/F3k0MQ69h7f5osnsemVk5pkc8/5AyalVT+NCfra9M71U8POqF6EZa6IHvg==", + "deprecated": "Package deprecated. Use @nats-io/nuid instead: https://www.npmjs.com/package/@nats-io/nuid", + "license": "Apache-2.0", + "engines": { + "node": ">= 8.16.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "license": "MIT", + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.8.tgz", + "integrity": "sha512-SceYGWXvdqlWa/OnQ5FQuV+NxvNmMRhMw/w9AHkH71hTzveND4BTYgvp16g+oITK47qbOl/3D0bl0iygehWAWQ==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-hash": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz", + "integrity": "sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.9.tgz", + "integrity": "sha512-mt8YM6XwsTTovI+kdZdHSxoyF2DI59up034orlC9NfweclcWOt7CVascNNLp6U+bjFVCVCIh9PwS76tDM/rH8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.reduce": "^1.0.8", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "gopd": "^1.2.0", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/optional": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/optional/-/optional-0.1.4.tgz", + "integrity": "sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/ora/-/ora-4.0.3.tgz", + "integrity": "sha512-fnDebVFyz309A73cqCipVL1fBZewq4vwgSHfxh43vVy31mbyoQ8sCH3Oeaog/owYOs/lLlGVPCISQonTneg6Pg==", + "license": "MIT", + "dependencies": { + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.2.0", + "is-interactive": "^1.0.0", + "log-symbols": "^3.0.0", + "mute-stream": "0.0.8", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "license": "MIT" + }, + "node_modules/os-name": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", + "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", + "license": "MIT", + "dependencies": { + "macos-release": "^2.2.0", + "windows-release": "^3.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osx-release": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/osx-release/-/osx-release-1.1.0.tgz", + "integrity": "sha512-ixCMMwnVxyHFQLQnINhmIpWqXIfS2YOXchwQrk+OFzmo6nDjQ0E4KXAyyUh0T0MZgV4bUhkRrAbVqlE4yLVq4A==", + "license": "MIT", + "dependencies": { + "minimist": "^1.1.0" + }, + "bin": { + "osx-release": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-each-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", + "integrity": "sha512-J/e9xiZZQNrt+958FFzJ+auItsBGq+UrQ7nE89AUP7UOTtjHnkISANXLdayhVzh538UnLMCSlf13lFfRIAKQOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-reduce": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha512-3Tx1T3oM1xO/Y8Gj0sWyE78EIJZ+t+aEmXUdvQgvGmSMri7aPTHoovbXEreWKkL5j21Er60XAWLTzKbAKYOujQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "license": "MIT", + "dependencies": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "node_modules/parallel-transform/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/parallel-transform/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/parallel-transform/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/parallel-transform/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.5", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/passport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", + "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-jwt/node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/passport-jwt/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/passport-jwt/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "license": "MIT" + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", + "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "license": [ + "MIT", + "Apache2" + ], + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.6.tgz", + "integrity": "sha512-BT6eelPB1EyGHo8pC0o9Bl6k6SYVhKO1jEbd3lcTrtr7XHdjP8BW1YpfCV3G9Kwkxgattk+S5q2/RvuttCsS1g==", + "license": "MIT", + "dependencies": { + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidusage": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-3.0.2.tgz", + "integrity": "sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, + "node_modules/pm2": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/pm2/-/pm2-5.4.3.tgz", + "integrity": "sha512-4/I1htIHzZk1Y67UgOCo4F1cJtas1kSds31N8zN0PybO230id1nigyjGuGFzUnGmUFPmrJ0On22fO1ChFlp7VQ==", + "license": "AGPL-3.0", + "dependencies": { + "@pm2/agent": "~2.0.0", + "@pm2/io": "~6.0.1", + "@pm2/js-api": "~0.8.0", + "@pm2/pm2-version-check": "latest", + "async": "~3.2.0", + "blessed": "0.1.81", + "chalk": "3.0.0", + "chokidar": "^3.5.3", + "cli-tableau": "^2.0.0", + "commander": "2.15.1", + "croner": "~4.1.92", + "dayjs": "~1.11.5", + "debug": "^4.3.1", + "enquirer": "2.3.6", + "eventemitter2": "5.0.1", + "fclone": "1.0.11", + "js-yaml": "~4.1.0", + "mkdirp": "1.0.4", + "needle": "2.4.0", + "pidusage": "~3.0", + "pm2-axon": "~4.0.1", + "pm2-axon-rpc": "~0.7.1", + "pm2-deploy": "~1.0.2", + "pm2-multimeter": "^0.1.2", + "promptly": "^2", + "semver": "^7.2", + "source-map-support": "0.5.21", + "sprintf-js": "1.1.2", + "vizion": "~2.2.1" + }, + "bin": { + "pm2": "bin/pm2", + "pm2-dev": "bin/pm2-dev", + "pm2-docker": "bin/pm2-docker", + "pm2-runtime": "bin/pm2-runtime" + }, + "engines": { + "node": ">=12.0.0" + }, + "optionalDependencies": { + "pm2-sysmonit": "^1.2.8" + } + }, + "node_modules/pm2-axon": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pm2-axon/-/pm2-axon-4.0.1.tgz", + "integrity": "sha512-kES/PeSLS8orT8dR5jMlNl+Yu4Ty3nbvZRmaAtROuVm9nYYGiaoXqqKQqQYzWQzMYWUKHMQTvBlirjE5GIIxqg==", + "license": "MIT", + "dependencies": { + "amp": "~0.3.1", + "amp-message": "~0.1.1", + "debug": "^4.3.1", + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=5" + } + }, + "node_modules/pm2-axon-rpc": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/pm2-axon-rpc/-/pm2-axon-rpc-0.7.1.tgz", + "integrity": "sha512-FbLvW60w+vEyvMjP/xom2UPhUN/2bVpdtLfKJeYM3gwzYhoTEEChCOICfFzxkxuoEleOlnpjie+n1nue91bDQw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1" + }, + "engines": { + "node": ">=5" + } + }, + "node_modules/pm2-axon/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pm2-deploy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pm2-deploy/-/pm2-deploy-1.0.2.tgz", + "integrity": "sha512-YJx6RXKrVrWaphEYf++EdOOx9EH18vM8RSZN/P1Y+NokTKqYAca/ejXwVLyiEpNju4HPZEk3Y2uZouwMqUlcgg==", + "license": "MIT", + "dependencies": { + "run-series": "^1.1.8", + "tv4": "^1.3.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pm2-multimeter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/pm2-multimeter/-/pm2-multimeter-0.1.2.tgz", + "integrity": "sha512-S+wT6XfyKfd7SJIBqRgOctGxaBzUOmVQzTAS+cg04TsEUObJVreha7lvCfX8zzGVr871XwCSnHUU7DQQ5xEsfA==", + "license": "MIT/X11", + "dependencies": { + "charm": "~0.1.1" + } + }, + "node_modules/pm2-sysmonit": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/pm2-sysmonit/-/pm2-sysmonit-1.2.8.tgz", + "integrity": "sha512-ACOhlONEXdCTVwKieBIQLSi2tQZ8eKinhcr9JpZSUAL8Qy0ajIgRtsLxG/lwPOW3JEKqPyw/UaHmTWhUzpP4kA==", + "license": "Apache", + "optional": true, + "dependencies": { + "async": "^3.2.0", + "debug": "^4.3.1", + "pidusage": "^2.0.21", + "systeminformation": "^5.7", + "tx2": "~1.0.4" + } + }, + "node_modules/pm2-sysmonit/node_modules/pidusage": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.21.tgz", + "integrity": "sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pm2/node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/pm2/node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pm2/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/pm2/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/pm2/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/pm2/node_modules/commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "license": "MIT" + }, + "node_modules/pm2/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pm2/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pm2/node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/pm2/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/pm2/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pm2/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pm2/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/pm2/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true, + "license": "MIT" + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promptly": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/promptly/-/promptly-2.2.0.tgz", + "integrity": "sha512-aC9j+BZsRSSzEsXBNBwDnAxujdx19HycZoKgRgzWnS8eOHg1asuf9heuLprfbe739zY3IdUQx+Egv6Jn135WHA==", + "license": "MIT", + "dependencies": { + "read": "^1.0.4" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "license": "MIT", + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/pumpify/node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "license": "MIT", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/readdirp/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/readdirp/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/realpath-native": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", + "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "util.promisify": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/referrer-policy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz", + "integrity": "sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "license": "Apache-2.0" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regex-not/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regex-not/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "license": "ISC" + }, + "node_modules/repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lodash": "^4.17.19" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "deprecated": "request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", + "dev": true, + "license": "ISC", + "dependencies": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "engines": { + "node": ">=0.12.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.5.tgz", + "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz", + "integrity": "sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-in-the-middle/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "license": "MIT" + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "license": "MIT", + "engines": { + "node": ">=0.12" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/ripemd160/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/ripemd160/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/ripemd160/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ripemd160/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "6.* || >= 7.*" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==", + "license": "ISC", + "dependencies": { + "aproba": "^1.1.1" + } + }, + "node_modules/run-queue/node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "license": "ISC" + }, + "node_modules/run-series": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-series/-/run-series-1.1.9.tgz", + "integrity": "sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "license": "MIT", + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "deprecated": "some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added", + "dev": true, + "license": "MIT", + "dependencies": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + }, + "bin": { + "sane": "src/cli.js" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "license": "MIT", + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/sdk-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/sdk-base/-/sdk-base-2.0.1.tgz", + "integrity": "sha512-eeG26wRwhtwYuKGCDM3LixCaxY27Pa/5lK4rLKhQa7HBjJ3U3Y+f81MMZQRsDw/8SC2Dao/83yJTXJ8aULuN8Q==", + "license": "MIT", + "dependencies": { + "get-ready": "~1.0.0" + } + }, + "node_modules/segment": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/segment/-/segment-0.1.3.tgz", + "integrity": "sha512-4Kjk38q0ykMIK5a+uo2MjTM2EECJaLLQOkdptceiYHTFZU/iciWXiFiGTG8Or/cfO2RqjlLw/s6rqijxAFSKFQ==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true, + "license": "MIT" + }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "license": "BSD-2-Clause" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "license": "MIT", + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "license": "MIT", + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.8.tgz", + "integrity": "sha512-SceYGWXvdqlWa/OnQ5FQuV+NxvNmMRhMw/w9AHkH71hTzveND4BTYgvp16g+oITK47qbOl/3D0bl0iygehWAWQ==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/snapdragon/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/snapdragon/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "license": "MIT", + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "license": "MIT" + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "license": "BSD-3-Clause" + }, + "node_modules/sql-escaper": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.4.0.tgz", + "integrity": "sha512-Ti/Zx9J3aITMYUMFhCKbkplQjyi7Kk2SyYXp+rzrkyKZetIy19XbQtVZ+0ZR5aVm/178tIJ6+aVvBqXkH+Xs7w==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/stack-utils": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.5.tgz", + "integrity": "sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.8.tgz", + "integrity": "sha512-SceYGWXvdqlWa/OnQ5FQuV+NxvNmMRhMw/w9AHkH71hTzveND4BTYgvp16g+oITK47qbOl/3D0bl0iygehWAWQ==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/stream-browserify/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/stream-browserify/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stream-browserify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/stream-browserify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/stream-http": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.2.tgz", + "integrity": "sha512-QllfrBhqF1DPcz46WxKTs6Mz1Bpc+8Qm6vbqOpVav5odAXwbyzwnEczoWqtxrsmlO+cJqtPrp/8gWKWjaKLLlA==", + "license": "MIT", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/stream-http/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/stream-http/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stream-http/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/stream-http/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/stream-wormhole": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stream-wormhole/-/stream-wormhole-1.1.0.tgz", + "integrity": "sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/streamroller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, + "node_modules/string-length": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", + "integrity": "sha512-Qka42GGrS8Mm3SZ+7cH8UXiIWI867/b/Z/feQSpQx/rbfB8UGknGEZVaUQMOUVj+soY6NpWAxily63HI1OckVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "astral-regex": "^1.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "license": "MIT", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/superagent/node_modules/form-data": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", + "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/superagent/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/superagent/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/superagent/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/superagent/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/superagent/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/superagent/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/supertest": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz", + "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==", + "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^3.8.3" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-themes": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/swagger-themes/-/swagger-themes-1.4.3.tgz", + "integrity": "sha512-1G0CqJC1IBbNxkAOyJoREd9hfwXH1R6+3GOFxLhQho2w2i+AbaJqkF4mTJhkce4yhaEMUXvv4KKu1YO/qpe6nQ==", + "license": "MIT" + }, + "node_modules/swagger-ui-dist": { + "version": "5.32.8", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz", + "integrity": "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.3.tgz", + "integrity": "sha512-CDje4PndhTD2HkgyKH3pab+LKspDeB/NhPN2OF1j+piYIamQqBYwAXWESOT1Yju2xFg51bRW9sUng2WxDjzArw==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=4.11.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/systeminformation": { + "version": "5.31.16", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.16.tgz", + "integrity": "sha512-37NsFaeaqwYxanLgdJAGWj8OXIRgvSBsDXGhlC0uUoHyl9nf4HrNsZjqtQ9Gc99bj9FQpffzYccPVuQ6gpjfqg==", + "license": "MIT", + "optional": true, + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "license": "BSD-2-Clause", + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.6.tgz", + "integrity": "sha512-2lBVf/VMVIddjSn3GqbT90GvIJ/eYXJkt8cTzU7NbjKqK8fwv18Ftr4PlbF46b/e88743iZFL5Dtr/rC4hjIeA==", + "license": "MIT", + "dependencies": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^4.0.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "webpack": "^4.0.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "license": "ISC", + "dependencies": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/terser-webpack-plugin/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/terser-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ssri": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", + "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", + "license": "ISC", + "dependencies": { + "figgy-pudding": "^3.5.1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", + "integrity": "sha512-wCVxLDcFxw7ujDxaeJC6nfl2XfHJNYs8yUYJnvMgtPEFlttP9tHSfRUv2vBe6C4hkVFPWoP1P6ZccbYjmSEkKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "license": "MIT", + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==", + "license": "MIT" + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "license": "MIT", + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-jest": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-24.3.0.tgz", + "integrity": "sha512-Hb94C/+QRIgjVZlJyiWwouYUF+siNJHJHknyspaOcZ+OQAIdFG/UrdQVXw/0B8Z3No34xkUXZJpOTy9alOWdVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "mkdirp": "0.x", + "resolve": "1.x", + "semver": "^5.5", + "yargs-parser": "10.x" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "jest": ">=24 <25" + } + }, + "node_modules/ts-jest/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/ts-loader": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.2.2.tgz", + "integrity": "sha512-HDo5kXZCBml3EUPcc7RlZOV/JGlLHwppTLEHb3SHnr5V7NXD4klMEkrhJe5wgRbaWsSXi+Y1SIBN/K9B6zWGWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^4.0.0", + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8.6" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/ts-loader/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ts-loader/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/ts-loader/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ts-loader/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-S/gOOPOkV8rIL4LurZ1vUdYCVgo15iX9ZMJ6wx6w2OgcpT/G4wMyHB6WM+xheSqGMrWKuxFul+aXpCju3wmj/g==", + "license": "MIT", + "dependencies": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "tsconfig-paths": "^3.4.0" + } + }, + "node_modules/tsconfig-paths-webpack-plugin/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tsconfig-paths-webpack-plugin/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tslib": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", + "license": "Apache-2.0" + }, + "node_modules/tslint": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", + "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "bin": { + "tslint": "bin/tslint" + }, + "engines": { + "node": ">=4.8.0" + }, + "peerDependencies": { + "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev" + } + }, + "node_modules/tslint/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/tslint/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tslint/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslint/node_modules/js-yaml": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/tslint/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tslint/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tslint/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/tslint/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tslint/node_modules/tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "peerDependencies": { + "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" + } + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==", + "license": "MIT" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tv4": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", + "integrity": "sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==", + "license": [ + { + "type": "Public Domain", + "url": "http://geraintluff.github.io/tv4/LICENSE.txt" + }, + { + "type": "MIT", + "url": "http://jsonary.com/LICENSE.txt" + } + ], + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/tx2": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tx2/-/tx2-1.0.5.tgz", + "integrity": "sha512-sJ24w0y03Md/bxzK4FU8J8JveYYUbSs2FViLJ2D/8bytSiyPRbuE3DyL/9UKYXTZlV3yXq0L8GLlhobTnekCVg==", + "license": "MIT", + "optional": true, + "dependencies": { + "json-stringify-safe": "^5.0.1" + } + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typeorm": { + "version": "0.2.45", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.45.tgz", + "integrity": "sha512-c0rCO8VMJ3ER7JQ73xfk0zDnVv0WDjpsP6Q1m6CVKul7DB9iVdWLRjPzc8v2eaeBuomsbZ2+gTaYr8k1gm3bYA==", + "license": "MIT", + "dependencies": { + "@sqltools/formatter": "^1.2.2", + "app-root-path": "^3.0.0", + "buffer": "^6.0.3", + "chalk": "^4.1.0", + "cli-highlight": "^2.1.11", + "debug": "^4.3.1", + "dotenv": "^8.2.0", + "glob": "^7.1.6", + "js-yaml": "^4.0.0", + "mkdirp": "^1.0.4", + "reflect-metadata": "^0.1.13", + "sha.js": "^2.4.11", + "tslib": "^2.1.0", + "uuid": "^8.3.2", + "xml2js": "^0.4.23", + "yargs": "^17.0.1", + "zen-observable-ts": "^1.0.0" + }, + "bin": { + "typeorm": "cli.js" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@sap/hana-client": "^2.11.14", + "better-sqlite3": "^7.1.2", + "hdb-pool": "^0.1.6", + "ioredis": "^4.28.3", + "mongodb": "^3.6.0", + "mssql": "^6.3.1", + "mysql2": "^2.2.5", + "oracledb": "^5.1.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.2", + "typeorm-aurora-data-api-driver": "^2.0.0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "hdb-pool": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/typeorm/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/typeorm/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/typeorm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/typeorm/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/typeorm/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/typeorm/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typeorm/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/typeorm/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/typeorm/node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/typeorm/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/typeorm/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/typeorm/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", + "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unescape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unescape/-/unescape-1.0.1.tgz", + "integrity": "sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "license": "MIT", + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "license": "MIT", + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "license": "MIT", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "license": "MIT" + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, + "node_modules/urllib": { + "version": "2.44.1", + "resolved": "https://registry.npmjs.org/urllib/-/urllib-2.44.1.tgz", + "integrity": "sha512-vreOVvFizoiIz5NK9IYMgUknkriHHBVccn2VFfJhgKz6O2qwm0SgjFk4OpXFRDXpdrTx8EzM1DB0/pejrqXwPA==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.3.0", + "content-type": "^1.0.2", + "default-user-agent": "^1.0.0", + "digest-header": "^1.0.0", + "ee-first": "~1.1.1", + "formstream": "^1.1.0", + "humanize-ms": "^1.2.0", + "iconv-lite": "^0.6.3", + "pump": "^3.0.0", + "qs": "^6.4.0", + "statuses": "^1.3.1", + "utility": "^1.16.1" + }, + "engines": { + "node": ">= 0.10.0" + }, + "peerDependencies": { + "proxy-agent": "^5.0.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + } + } + }, + "node_modules/urllib/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/urllib/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/util.promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.1.3.tgz", + "integrity": "sha512-GIEaZ6o86fj09Wtf0VfZ5XP7tmd4t3jM5aZCgmBi231D0DB1AEBa3Aa6MP48DMsAIi96WkpWLimIWVwOjbDMOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "for-each": "^0.3.3", + "get-intrinsic": "^1.2.6", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "object.getownpropertydescriptors": "^2.1.8", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/utility": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/utility/-/utility-1.18.0.tgz", + "integrity": "sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==", + "license": "MIT", + "dependencies": { + "copy-to": "^2.0.1", + "escape-html": "^1.0.3", + "mkdirp": "^0.5.1", + "mz": "^2.7.0", + "unescape": "^1.0.1" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.1.tgz", + "integrity": "sha512-yqjRXZzSJm9Dbl84H2VDHpM3zMjzSJQ+hn6C4zqd5ilW+7P4ZmLEEqwho9LjP+tGuZlF4xrHQXT0h9QZUS/pWA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vizion": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vizion/-/vizion-2.2.1.tgz", + "integrity": "sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==", + "license": "Apache-2.0", + "dependencies": { + "async": "^2.6.3", + "git-node-fs": "^1.0.0", + "ini": "^1.3.5", + "js-git": "^0.7.8" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/vizion/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "license": "MIT" + }, + "node_modules/w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-process-hrtime": "^1.0.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", + "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + }, + "optionalDependencies": { + "chokidar": "^3.4.1", + "watchpack-chokidar2": "^2.0.1" + } + }, + "node_modules/watchpack-chokidar2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", + "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", + "license": "MIT", + "optional": true, + "dependencies": { + "chokidar": "^2.1.8" + } + }, + "node_modules/watchpack-chokidar2/node_modules/chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/watchpack-chokidar2/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack/node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "optional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/watchpack/node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/watchpack/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "optional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/watchpack/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "optional": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/watchpack/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/watchpack/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/watchpack/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "optional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/watchpack/node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/watchpack/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/watchpack/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "optional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/watchpack/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "4.41.5", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.5.tgz", + "integrity": "sha512-wp0Co4vpyumnp3KlkmpM5LWuzvZYayDwM2n17EHFr4qxBBbRokC7DJawPJC7TfSFZ9HZ6GsdH40EBj4UV0nmpw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/wasm-edit": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "acorn": "^6.2.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.1", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.6.0", + "webpack-sources": "^1.4.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-node-externals": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-1.7.2.tgz", + "integrity": "sha512-ajerHZ+BJKeCLviLUUmnyd5B4RavLF76uv3cs6KNuO8W+HuQaEs0y0L7o40NQxdPy5w0pcv8Ew7yPUAQG0UdCg==", + "license": "MIT" + }, + "node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "license": "MIT", + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/webpack-sources/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/webpack/node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/webpack/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/webpack/node_modules/memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==", + "license": "MIT", + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "node_modules/webpack/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/webpack/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/webpack/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/webpack/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.4.24" + } + }, + "node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/win-release": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/win-release/-/win-release-1.1.1.tgz", + "integrity": "sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==", + "license": "MIT", + "dependencies": { + "semver": "^5.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/win-release/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/windows-release": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz", + "integrity": "sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==", + "license": "MIT", + "dependencies": { + "execa": "^1.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "license": "MIT", + "dependencies": { + "errno": "~0.1.7" + } + }, + "node_modules/worker-rpc": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.1.tgz", + "integrity": "sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==", + "license": "MIT", + "dependencies": { + "microevent.ts": "~0.1.1" + } + }, + "node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", + "integrity": "sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "node_modules/ws": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.5.tgz", + "integrity": "sha512-G0gACQIjFmv7NqpaOAXEpe9nEtRYD6ZCy2Ip/B4EzR08qEwnf/wYJoQd3cjosPdnJsHkeh0j2tzOaIBZIOC1yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/x-xss-protection": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.3.0.tgz", + "integrity": "sha512-kpyBI9TlVipZO4diReZMAHWtS0MMa/7Kgx8hwG/EuZLiA6sg4Ah/4TRdASHhRRN3boobzcYgFRUFSgHRge6Qhg==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "license": "MIT", + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^4.1.0" + } + }, + "node_modules/yargs-parser/node_modules/camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==", + "license": "MIT" + }, + "node_modules/zen-observable-ts": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz", + "integrity": "sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA==", + "license": "MIT", + "dependencies": { + "@types/zen-observable": "0.8.3", + "zen-observable": "0.8.15" + } + } + } +} diff --git a/cli/server/public/favicon.png b/cli/server/public/favicon.png index f455437f..521edf80 100644 Binary files a/cli/server/public/favicon.png and b/cli/server/public/favicon.png differ diff --git a/cli/src/bin/reactpress-cli-shim.ts b/cli/src/bin/reactpress-cli-shim.ts new file mode 100644 index 00000000..f6c924be --- /dev/null +++ b/cli/src/bin/reactpress-cli-shim.ts @@ -0,0 +1,29 @@ +#!/usr/bin/env node +// @ts-nocheck + +/** + * @deprecated 3.0 起请使用 `reactpress`(@fecommunity/reactpress)。3.1 将移除此 bin。 + */ +const chalk = require('chalk'); +const { t } = require('../lib/i18n'); + +function mapLegacyArgv(argv) { + const [cmd, ...rest] = argv; + if (cmd === 'start') return ['server', 'start', ...rest]; + if (cmd === 'stop') return ['server', 'stop', ...rest]; + if (cmd === 'restart') return ['server', 'restart', ...rest]; + if (cmd === 'status') return ['server', 'status', ...rest]; + return argv; +} + +if (!process.env.REACTPRESS_SUPPRESS_DEPRECATION) { + console.warn( + chalk.yellow( + t('shim.deprecated') + ) + ); +} + +const mapped = mapLegacyArgv(process.argv.slice(2)); +process.argv = [process.argv[0], process.argv[1], ...mapped]; +require('./reactpress.js'); diff --git a/cli/src/bin/reactpress-theme-client.ts b/cli/src/bin/reactpress-theme-client.ts new file mode 100644 index 00000000..029ab9e8 --- /dev/null +++ b/cli/src/bin/reactpress-theme-client.ts @@ -0,0 +1,239 @@ +#!/usr/bin/env node +// @ts-nocheck + +/** + * Generic ReactPress theme client launcher (for themes without bin/reactpress-client.js). + * Set REACTPRESS_THEME_DIR to the active theme package root. + */ + +const path = require('path'); +const fs = require('fs'); +const { spawn, spawnSync } = require('child_process'); +const { hasUsableProductionBuild } = require('../lib/theme-prod'); +const { getPm2ClientMemoryRestart, resolveBuildNodeEnv } = require('../lib/prod-memory'); +const { resolvePm2Bin } = require('../lib/pm2-runtime'); +const { readActiveThemeManifest } = require('../lib/theme-runtime'); +const { + ensureAdminStaticForTheme, + shouldRebuildThemeAfterAdminSync, +} = require('../core/services/admin-static'); +const { patchThemeApiProxyRoute } = require('../lib/theme-api-proxy-patch'); + +const originalCwd = process.env.REACTPRESS_ORIGINAL_CWD || process.cwd(); +const args = process.argv.slice(2); +const usePM2 = args.includes('--pm2'); +const useBackground = args.includes('--bg'); +const usePm2Background = args.includes('--pm2-bg'); + +const clientDir = process.env.REACTPRESS_THEME_DIR + ? path.resolve(process.env.REACTPRESS_THEME_DIR) + : null; + +if (!clientDir || !fs.existsSync(path.join(clientDir, 'package.json'))) { + console.error('[ReactPress Client] REACTPRESS_THEME_DIR must point to a theme package'); + process.exit(1); +} + +const nextDir = path.join(clientDir, '.next'); +const startScript = fs.existsSync(path.join(clientDir, 'server.js')) ? 'server.js' : null; + +function runStartCommand() { + if (startScript) { + return ['node', [startScript]]; + } + return ['npm', ['run', 'start']]; +} + +function ensureBuilt() { + const { ensureBundledPlugins } = require('../core/services/local-site'); + if (ensureBundledPlugins(originalCwd)) { + console.log('[ReactPress Client] Seeded bundled plugins into project'); + } + + const adminSynced = ensureAdminStaticForTheme(originalCwd, clientDir); + const apiProxyPatched = patchThemeApiProxyRoute(clientDir); + const { activeTheme } = readActiveThemeManifest(originalCwd); + const themeId = process.env.REACTPRESS_THEME_ID || activeTheme || path.basename(clientDir); + const needsRebuild = + adminSynced || + apiProxyPatched || + shouldRebuildThemeAfterAdminSync(clientDir) || + !hasUsableProductionBuild(clientDir, themeId); + if (!needsRebuild) return; + console.log('[ReactPress Client] Client not built yet. Building…'); + const build = spawnSync('pnpm', ['run', 'build'], { + stdio: 'inherit', + cwd: clientDir, + env: resolveBuildNodeEnv({ ...process.env, REACTPRESS_BUILD_ACTIVE: '1' }), + }); + if (build.status !== 0) process.exit(build.status || 1); +} + +function buildPm2ClientArgs(cmd, cmdArgs) { + const pm2Mem = getPm2ClientMemoryRestart(); + return cmd === 'node' + ? [ + 'start', + cmd, + '--name', + 'reactpress-client', + '--update-env', + '--max-memory-restart', + pm2Mem, + '-f', + '--', + ...cmdArgs, + ] + : [ + 'start', + cmd, + '--name', + 'reactpress-client', + '--update-env', + '--max-memory-restart', + pm2Mem, + '-f', + '--', + ...cmdArgs, + ]; +} + +function startWithPm2() { + ensureBuilt(); + const [cmd, cmdArgs] = runStartCommand(); + const pm2Env = productionThemeEnv(); + const pm2Bin = resolvePm2Bin(originalCwd) || 'pm2'; + const pm2Args = buildPm2ClientArgs(cmd, cmdArgs); + + const child = spawn(pm2Bin, pm2Args, { stdio: 'inherit', cwd: clientDir, env: pm2Env, shell: cmd !== 'node' }); + child.on('close', (code) => process.exit(code ?? 0)); + child.on('error', (err) => { + console.error('[ReactPress Client] Failed to start site:', err); + process.exit(1); + }); +} + +function startWithPm2Background() { + ensureBuilt(); + const [cmd, cmdArgs] = runStartCommand(); + const pm2Env = productionThemeEnv(); + const pm2Bin = resolvePm2Bin(originalCwd); + if (!pm2Bin) { + startWithNodeBackground(); + return; + } + + const result = spawnSync( + pm2Bin, + buildPm2ClientArgs(cmd, cmdArgs), + { + cwd: clientDir, + env: pm2Env, + stdio: 'inherit', + shell: cmd !== 'node' && process.platform === 'win32', + }, + ); + if (result.status !== 0) { + console.warn('[ReactPress Client] Falling back to background node start'); + startWithNodeBackground(); + return; + } + console.log('[ReactPress Client] Site started in background'); + process.exit(0); +} + +function productionThemeEnv() { + const visitorPort = process.env.CLIENT_PORT || process.env.PORT || '3001'; + const apiPort = process.env.SERVER_PORT || '3002'; + const clientSiteUrl = + process.env.CLIENT_SITE_URL || process.env.NEXT_PUBLIC_CLIENT_SITE_URL || `http://127.0.0.1:${visitorPort}`; + const adminUrl = `${clientSiteUrl.replace(/\/$/, '')}/admin/`; + return { + ...process.env, + NODE_ENV: 'production', + PORT: String(visitorPort), + CLIENT_PORT: String(visitorPort), + CLIENT_SITE_URL: clientSiteUrl, + NEXT_PUBLIC_CLIENT_SITE_URL: clientSiteUrl, + REACTPRESS_ORIGINAL_CWD: originalCwd, + REACTPRESS_THEME_DIR: clientDir, + REACTPRESS_API_URL: process.env.REACTPRESS_API_URL || `http://127.0.0.1:${apiPort}/api`, + SERVER_API_URL: process.env.SERVER_API_URL || `http://127.0.0.1:${apiPort}/api`, + NEXT_PUBLIC_REACTPRESS_API_URL: + process.env.NEXT_PUBLIC_REACTPRESS_API_URL || `http://127.0.0.1:${apiPort}/api`, + NEXT_PUBLIC_REACTPRESS_ADMIN_URL: process.env.NEXT_PUBLIC_REACTPRESS_ADMIN_URL || adminUrl, + REACTPRESS_SKIP_DEV_PORT_REDIRECT: '1', + }; +} + +function resolveBackgroundStartCommand() { + if (startScript) { + return { cmd: 'node', args: [startScript], env: {} }; + } + + const env = productionThemeEnv(); + const port = String(env.PORT || '3001'); + const nextBin = path.join(clientDir, 'node_modules', 'next', 'dist', 'bin', 'next'); + if (fs.existsSync(nextBin)) { + return { + cmd: process.execPath, + args: [nextBin, 'start', '-p', port], + env: { INIT_CWD: clientDir }, + }; + } + + return { cmd: 'npm', args: ['run', 'start'], env: {}, shell: process.platform === 'win32' }; +} + +function startWithNodeBackground() { + ensureBuilt(); + const { cmd, args: cmdArgs, env: envExtra = {}, shell = false } = resolveBackgroundStartCommand(); + const logDir = path.join(originalCwd, '.reactpress', 'logs', 'client'); + fs.mkdirSync(logDir, { recursive: true }); + const outPath = path.join(logDir, 'stdout.log'); + const errPath = path.join(logDir, 'stderr.log'); + const outFd = fs.openSync(outPath, 'a'); + const errFd = fs.openSync(errPath, 'a'); + + const child = spawn(cmd, cmdArgs, { + detached: true, + stdio: ['ignore', outFd, errFd], + cwd: clientDir, + shell, + env: { + ...productionThemeEnv(), + ...envExtra, + }, + }); + + child.unref(); + fs.closeSync(outFd); + fs.closeSync(errFd); + + const { writeClientPid } = require('../lib/process'); + writeClientPid(originalCwd, child.pid); + console.log(`[ReactPress Client] Started in background (pid ${child.pid})`); + process.exit(0); +} + +function startWithNode() { + ensureBuilt(); + process.chdir(clientDir); + const [cmd, cmdArgs] = runStartCommand(); + const child = spawn(cmd, cmdArgs, { + stdio: 'inherit', + cwd: clientDir, + env: productionThemeEnv(), + }); + child.on('close', (code) => process.exit(code ?? 0)); +} + +if (usePM2) { + startWithPm2(); +} else if (usePm2Background) { + startWithPm2Background(); +} else if (useBackground) { + startWithNodeBackground(); +} else { + startWithNode(); +} diff --git a/cli/src/bin/reactpress.ts b/cli/src/bin/reactpress.ts new file mode 100755 index 00000000..8c44cd8b --- /dev/null +++ b/cli/src/bin/reactpress.ts @@ -0,0 +1,237 @@ +#!/usr/bin/env node +// @ts-nocheck + +/** + * ReactPress 4 — zero-dependency publishing platform. + * Commands: `reactpress init`, `reactpress doctor`. Run `reactpress` or `reactpress --help` for usage. + */ + +const { Command } = require('commander'); +const path = require('path'); +const chalk = require('chalk'); +const { brand, divider } = require('../ui/theme'); +const { initMonorepoProject, isMonorepoCheckout } = require('../lib/bootstrap'); +const { initProject } = require('../core/services/init'); +const { runLifecycleCommand } = require('../lib/lifecycle'); +const { startClientInBackground } = require('../lib/client-lifecycle'); +const { getCliVersion } = require('../lib/paths'); +const { + loadServerSiteUrl, + loadAdminConsoleUrl, + loadClientSiteUrl, + getApiPrefix, +} = require('../lib/http'); +const { t } = require('../lib/i18n'); +const { describeProject } = require('../lib/project-type'); +const { refreshBannerWithStartup } = require('../ui/banner-startup'); +const { runDoctor } = require('../lib/doctor'); +const { runDoctorLogs } = require('../lib/project-logs'); + +const LEGACY_COMMANDS = new Set([ + 'start', + 'dev', + 'docker', + 'nginx', + 'server', + 'build', + 'status', + 'publish', + 'theme', + 'plugin', + 'db', + 'desktop', + 'client', +]); + +function attachLogsOptions(cmd) { + return cmd + .option('--tail ', t('cli.logs.tailOption'), '50') + .option('--source ', t('cli.logs.sourceOption')) + .option('--grep ', t('cli.logs.grepOption')) + .option('--list', t('cli.logs.listOption')) + .option('-f, --follow', t('cli.logs.followOption')); +} + +function createLogsAction() { + return async (directory, options) => { + const projectRoot = path.resolve(directory || '.'); + process.env.REACTPRESS_ORIGINAL_CWD = projectRoot; + const code = await runDoctorLogs(projectRoot, options); + process.exit(code); + }; +} + +if (!process.env.REACTPRESS_LOCAL_MODE) { + process.env.REACTPRESS_LOCAL_MODE = '1'; +} +if (!process.env.REACTPRESS_SKIP_NGINX) { + process.env.REACTPRESS_SKIP_NGINX = '1'; +} + +const program = new Command(); + +program + .name('reactpress') + .description(t('cli.description')) + .version(getCliVersion()); + +function printRunningPanel(projectRoot) { + const apiUrl = loadServerSiteUrl(projectRoot); + const adminUrl = loadAdminConsoleUrl(projectRoot); + const siteUrl = loadClientSiteUrl(projectRoot); + const apiPrefix = getApiPrefix(projectRoot); + + console.log(''); + console.log(brand.bold(t('init.readyTitle'))); + console.log(divider(48)); + console.log(` ${brand.muted(t('init.label.site'))} ${brand.primary(siteUrl)}`); + console.log(` ${brand.muted(t('init.label.admin'))} ${brand.primary(adminUrl)}`); + console.log(` ${brand.muted(t('init.label.api'))} ${brand.primary(`${apiUrl}${apiPrefix}`)}`); + console.log(divider(48)); + console.log(` ${brand.dim(t('init.hint'))}`); + console.log(''); +} + +async function startServices(projectRoot) { + const { ensureBundledPlugins } = require('../core/services/local-site'); + if (ensureBundledPlugins(projectRoot)) { + console.log(brand.dim(t('init.pluginsSeeded'))); + } + + const code = await runLifecycleCommand('start', projectRoot); + if (code !== 0) process.exit(code); + + const { ensureDefaultTheme } = require('../core/services/theme-bootstrap'); + await ensureDefaultTheme(projectRoot); + + try { + const clientCode = await startClientInBackground(projectRoot); + if (clientCode !== 0) process.exit(clientCode); + } catch (err) { + if ( + err.code === 'REACTPRESS_THEME_NOT_FOUND' || + err.code === 'REACTPRESS_THEME_BIN_NOT_FOUND' + ) { + printRunningPanel(projectRoot); + console.log(brand.dim(t('init.apiOnlyHint'))); + return; + } + console.error(chalk.red('[reactpress]'), err.message || err); + process.exit(err.exitCode ?? 1); + } + + printRunningPanel(projectRoot); +} + +async function runInit(directory = '.', options = { force: false }) { + const projectRoot = path.resolve(directory); + process.env.REACTPRESS_ORIGINAL_CWD = projectRoot; + + const project = describeProject(projectRoot); + await refreshBannerWithStartup(projectRoot, project); + + const result = isMonorepoCheckout(projectRoot) + ? await initMonorepoProject(projectRoot, { force: !!options.force }) + : await initProject({ directory: projectRoot, force: !!options.force }); + + if (!result.ok) { + console.error(chalk.red('[reactpress]'), result.message); + process.exit(1); + } + + console.log(`${brand.success('✓')} ${result.message}`); + await startServices(projectRoot); +} + +program + .command('init [directory]') + .description(t('cli.init.description')) + .option('-f, --force', t('cli.init.force')) + .action(async (directory, options) => { + await runInit(directory || '.', options); + }); + +attachLogsOptions( + program.command('logs [directory]').description(t('cli.logs.description')), +).action(createLogsAction()); + +program + .command('stop [directory]') + .description(t('cli.stop.description')) + .action(async (directory) => { + const projectRoot = path.resolve(directory || '.'); + process.env.REACTPRESS_ORIGINAL_CWD = projectRoot; + await runLifecycleCommand('stop', projectRoot); + process.exit(0); + }); + +const doctorCommand = program + .command('doctor') + .description(t('cli.doctor.description')); + +attachLogsOptions( + doctorCommand + .command('logs [directory]') + .description(t('cli.logs.descriptionAlias')), +).action(createLogsAction()); + +doctorCommand + .command('check [directory]', { isDefault: true }) + .description(t('cli.doctor.description')) + .option('--show-logs', t('cli.doctor.showLogs')) + .action(async (directory, options) => { + const projectRoot = path.resolve(directory || '.'); + process.env.REACTPRESS_ORIGINAL_CWD = projectRoot; + const code = await runDoctor(projectRoot, options); + process.exit(code); + }); + +program.on('--help', () => { + console.log(''); + console.log(brand.bold(t('cli.help.examples'))); + console.log(divider(40)); + for (const line of [ + t('cli.help.init'), + t('cli.help.doctor'), + t('cli.help.logs'), + t('cli.help.stop'), + t('cli.help.zeroDep'), + ]) { + console.log(brand.dim(line)); + } + console.log(''); +}); + +async function showHelpBanner() { + const projectRoot = path.resolve(process.env.REACTPRESS_ORIGINAL_CWD || '.'); + const project = describeProject(projectRoot); + await refreshBannerWithStartup(projectRoot, project, { animated: false }); +} + +async function main() { + const argv = process.argv.slice(2); + + if (argv[0] && LEGACY_COMMANDS.has(argv[0])) { + console.error(chalk.red('[reactpress]'), t('init.unknownCommand', { cmd: argv[0] })); + console.error(brand.dim(t('init.useInitOnly'))); + process.exit(1); + } + + const wantsHelp = + argv.length === 0 || argv.includes('-h') || argv.includes('--help'); + + if (wantsHelp) { + await showHelpBanner(); + if (argv.length === 0) { + program.outputHelp(); + return; + } + } + + program.parse(process.argv); +} + +main().catch((err) => { + console.error(chalk.red('[reactpress]'), err.message || err); + process.exit(1); +}); diff --git a/cli/src/core/services/admin-static.ts b/cli/src/core/services/admin-static.ts new file mode 100644 index 00000000..61b034f5 --- /dev/null +++ b/cli/src/core/services/admin-static.ts @@ -0,0 +1,193 @@ +import fs from 'fs-extra'; +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; + +const ADMIN_REWRITES_MARKER = 'reactpress-admin-rewrites'; + +function getCliPackageRoot(): string { + return path.join(__dirname, '..', '..', '..'); +} + +export function resolveBundledAdminDistDir(): string | null { + const bundled = path.join(getCliPackageRoot(), 'admin-dist', 'index.html'); + return fs.existsSync(bundled) ? path.dirname(bundled) : null; +} + +export function resolveWebPackageRoot(projectRoot: string): string | null { + for (const candidate of [path.join(projectRoot, 'web'), path.join(projectRoot, '..', 'web')]) { + if (fs.existsSync(path.join(candidate, 'package.json'))) return candidate; + } + return null; +} + +function readAdminMainAssetId(indexHtmlPath: string): string | null { + try { + const html = fs.readFileSync(indexHtmlPath, 'utf8'); + return html.match(/assets\/(index-[^"]+\.js)/)?.[1] ?? null; + } catch { + return null; + } +} + +function resolveAdminDistSource(projectRoot: string): string | null { + const bundled = resolveBundledAdminDistDir(); + const webRoot = resolveWebPackageRoot(projectRoot); + const webDist = webRoot ? path.join(webRoot, 'dist') : null; + const webIndex = webDist ? path.join(webDist, 'index.html') : null; + + if (webIndex && fs.existsSync(webIndex)) { + if (!bundled) return webDist; + const bundledIndex = path.join(bundled, 'index.html'); + const webMtime = fs.statSync(webIndex).mtimeMs; + const bundledMtime = fs.statSync(bundledIndex).mtimeMs; + return webMtime >= bundledMtime ? webDist : bundled; + } + + return bundled; +} + +/** True when CLI/web admin-dist is newer than the theme's copied public/admin bundle. */ +export function adminSourceIsNewerThanTheme(projectRoot: string, themeDir: string): boolean { + const source = resolveAdminDistSource(projectRoot); + const sourceIndex = source ? path.join(source, 'index.html') : null; + const themeAdminIndex = path.join(themeDir, 'public', 'admin', 'index.html'); + if (!sourceIndex || !fs.existsSync(sourceIndex)) return false; + if (!fs.existsSync(themeAdminIndex)) return true; + + const sourceId = readAdminMainAssetId(sourceIndex); + const themeId = readAdminMainAssetId(themeAdminIndex); + if (sourceId && themeId) return sourceId !== themeId; + + return fs.statSync(sourceIndex).mtimeMs > fs.statSync(themeAdminIndex).mtimeMs; +} + +function syncDirToPublicAdmin(sourceDist: string, themeDir: string): string { + const target = path.join(themeDir, 'public', 'admin'); + fs.removeSync(target); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.copySync(sourceDist, target); + return target; +} + +function removeReactpressAdminMiddleware(themeDir: string): void { + for (const name of ['middleware.js', 'middleware.mjs']) { + const filePath = path.join(themeDir, name); + if (!fs.existsSync(filePath)) continue; + const content = fs.readFileSync(filePath, 'utf8'); + if (content.includes('reactpress-admin-middleware')) { + fs.removeSync(filePath); + } + } +} + +/** Patch theme next.config.js with afterFiles admin SPA rewrites. */ +export function ensureAdminRewritesInNextConfig(themeDir: string): boolean { + const configCandidates = ['next.config.js', 'next.config.mjs', 'next.config.ts']; + const configPath = configCandidates + .map((name) => path.join(themeDir, name)) + .find((candidate) => fs.existsSync(candidate)); + if (!configPath) return false; + + let src = fs.readFileSync(configPath, 'utf8'); + if (src.includes(ADMIN_REWRITES_MARKER)) return false; + + const arrayReturn = /async rewrites\(\)\s*\{\s*return (\[[\s\S]*?\])\s*\}/m.exec(src); + if (!arrayReturn) { + console.warn('[ReactPress Client] Could not patch next.config rewrites() for admin SPA'); + return false; + } + + const replacement = `async rewrites() { + // ${ADMIN_REWRITES_MARKER} + return { + beforeFiles: ${arrayReturn[1]}, + afterFiles: [ + { source: '/admin', destination: '/admin/index.html' }, + { source: '/admin/:path*', destination: '/admin/index.html' }, + ], + }; + }`; + + src = src.replace(arrayReturn[0], replacement); + fs.writeFileSync(configPath, src, 'utf8'); + removeReactpressAdminMiddleware(themeDir); + return true; +} + +function finalizeAdminThemeSetup(themeDir: string): boolean { + return ensureAdminRewritesInNextConfig(themeDir); +} + +function syncAdminFromWeb(projectRoot: string, themeDir: string): boolean { + const webRoot = resolveWebPackageRoot(projectRoot); + if (!webRoot) return false; + + const webDist = path.join(webRoot, 'dist', 'index.html'); + if (!fs.existsSync(webDist)) { + console.log('[ReactPress Client] Building admin SPA (web/)…'); + const buildWeb = spawnSync('pnpm', ['run', 'build'], { cwd: webRoot, stdio: 'inherit' }); + if (buildWeb.status !== 0) process.exit(buildWeb.status || 1); + } + + const syncScript = path.join(webRoot, 'bin', 'reactpress-web.js'); + const publicAdmin = path.join(themeDir, 'public', 'admin'); + console.log('[ReactPress Client] Syncing admin static → theme public/admin…'); + const sync = spawnSync(process.execPath, [syncScript, 'sync-public', publicAdmin], { + stdio: 'inherit', + cwd: webRoot, + }); + if (sync.status !== 0) process.exit(sync.status || 1); + return finalizeAdminThemeSetup(themeDir) || true; +} + +function syncAdminFromSource(sourceDist: string, themeDir: string): boolean { + console.log('[ReactPress Client] Syncing bundled admin static → theme public/admin…'); + syncDirToPublicAdmin(sourceDist, themeDir); + return finalizeAdminThemeSetup(themeDir) || true; +} + +/** Returns true when admin static or rewrites were newly applied (theme rebuild recommended). */ +export function ensureAdminStaticForTheme(projectRoot: string, themeDir: string): boolean { + const adminIndex = path.join(themeDir, 'public', 'admin', 'index.html'); + const needsResync = !fs.existsSync(adminIndex) || adminSourceIsNewerThanTheme(projectRoot, themeDir); + + if (!needsResync) { + return finalizeAdminThemeSetup(themeDir); + } + + const source = resolveAdminDistSource(projectRoot); + if (source && fs.existsSync(path.join(source, 'index.html'))) { + const webRoot = resolveWebPackageRoot(projectRoot); + if (webRoot && path.resolve(source) === path.resolve(path.join(webRoot, 'dist'))) { + return syncAdminFromWeb(projectRoot, themeDir); + } + return syncAdminFromSource(source, themeDir); + } + + if (syncAdminFromWeb(projectRoot, themeDir)) { + return true; + } + + console.warn('[ReactPress Client] Admin static missing and bundled admin-dist not found'); + return false; +} + +export function shouldRebuildThemeAfterAdminSync(themeDir: string): boolean { + const adminIndex = path.join(themeDir, 'public', 'admin', 'index.html'); + const nextDir = path.join(themeDir, '.next'); + const configPath = path.join(themeDir, 'next.config.js'); + if (!fs.existsSync(adminIndex)) return false; + if (!fs.existsSync(nextDir)) return true; + + const adminMtime = fs.statSync(adminIndex).mtimeMs; + const nextMtime = fs.statSync(nextDir).mtimeMs; + if (adminMtime > nextMtime) return true; + + if (fs.existsSync(configPath)) { + const configMtime = fs.statSync(configPath).mtimeMs; + if (configMtime > nextMtime && fs.readFileSync(configPath, 'utf8').includes(ADMIN_REWRITES_MARKER)) { + return true; + } + } + return false; +} diff --git a/cli/src/core/services/config.ts b/cli/src/core/services/config.ts new file mode 100644 index 00000000..a140fd77 --- /dev/null +++ b/cli/src/core/services/config.ts @@ -0,0 +1,144 @@ +import fs from 'fs-extra'; + +import type { EnvMap, ReactPressConfig } from '../../types/config'; +import { getProjectPaths, SQLITE_REL_PATH } from '../utils/paths'; + +export async function loadConfig(projectRoot: string): Promise { + const { configPath } = getProjectPaths(projectRoot); + if (!(await fs.pathExists(configPath))) { + throw new Error('未找到 ReactPress 项目。请先运行 reactpress init 初始化。'); + } + return fs.readJson(configPath) as Promise; +} + +export async function saveConfig(projectRoot: string, config: ReactPressConfig): Promise { + const { configPath, reactpressDir } = getProjectPaths(projectRoot); + await fs.ensureDir(reactpressDir); + await fs.writeJson(configPath, config, { spaces: 2 }); +} + +export async function loadEnvFile(envPath: string): Promise { + const content = await fs.readFile(envPath, 'utf8'); + const map: EnvMap = {}; + for (const line of content.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eq = trimmed.indexOf('='); + if (eq === -1) continue; + map[trimmed.slice(0, eq)] = trimmed.slice(eq + 1); + } + return map; +} + +export async function writeEnvFile(envPath: string, map: EnvMap): Promise { + const lines = [ + '# ReactPress — managed by reactpress-cli', + ...Object.entries(map).map(([k, v]) => `${k}=${v}`), + '', + ]; + await fs.writeFile(envPath, lines.join('\n'), 'utf8'); +} + +export function isSqliteMode(config: ReactPressConfig): boolean { + return config.database.mode === 'embedded-sqlite'; +} + +export async function syncEnvFromConfig( + projectRoot: string, + config: ReactPressConfig, +): Promise { + const { envPath, sqlitePath, uploadsDir } = getProjectPaths(projectRoot); + const existing = (await fs.pathExists(envPath)) ? await loadEnvFile(envPath) : {}; + + if (isSqliteMode(config)) { + const rawDbFile = config.database.sqlitePath ?? sqlitePath; + const dbFile = + rawDbFile === 'data/reactpress.db' || rawDbFile.endsWith('/data/reactpress.db') + ? SQLITE_REL_PATH + : rawDbFile; + const port = config.server.port; + const siteUrl = + config.server.serverUrl ?? config.server.siteUrl ?? `http://127.0.0.1:${port}`; + const clientUrl = config.server.clientUrl ?? 'http://localhost:3001'; + const merged: EnvMap = { + ...existing, + DB_TYPE: 'sqlite', + DB_DATABASE: dbFile, + SERVER_PORT: String(port), + CLIENT_SITE_URL: clientUrl, + SERVER_SITE_URL: siteUrl, + SERVER_API_PREFIX: config.server.apiPrefix ?? existing.SERVER_API_PREFIX ?? '/api', + REACTPRESS_UPLOAD_DIR: existing.REACTPRESS_UPLOAD_DIR ?? uploadsDir, + }; + await writeEnvFile(envPath, merged); + return; + } + + const merged: EnvMap = { + ...existing, + DB_TYPE: 'mysql', + DB_HOST: config.database.host ?? existing.DB_HOST ?? '127.0.0.1', + DB_PORT: String(config.database.port ?? existing.DB_PORT ?? 3306), + DB_USER: config.database.user ?? existing.DB_USER ?? 'reactpress', + DB_PASSWD: config.database.password ?? existing.DB_PASSWD ?? 'reactpress', + DB_DATABASE: config.database.database ?? existing.DB_DATABASE ?? 'reactpress', + SERVER_PORT: String(config.server.port), + CLIENT_SITE_URL: config.server.clientUrl ?? existing.CLIENT_SITE_URL ?? 'http://localhost:3001', + SERVER_SITE_URL: config.server.serverUrl ?? existing.SERVER_SITE_URL ?? 'http://localhost:3002', + }; + await writeEnvFile(envPath, merged); +} + +export function setConfigValue( + config: ReactPressConfig, + keyPath: string, + value: string, +): ReactPressConfig { + const parts = keyPath.split('.'); + const clone = structuredClone(config) as unknown as Record; + let cursor: Record = clone; + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (typeof cursor[part] !== 'object' || cursor[part] === null) { + cursor[part] = {}; + } + cursor = cursor[part] as Record; + } + const last = parts[parts.length - 1]; + const numericKeys = new Set(['port', 'version']); + cursor[last] = numericKeys.has(last) ? Number(value) : value; + return clone as unknown as ReactPressConfig; +} + +export function getConfigValue(config: ReactPressConfig, keyPath: string): string { + const parts = keyPath.split('.'); + let cursor: unknown = config; + for (const part of parts) { + if (typeof cursor !== 'object' || cursor === null) { + throw new Error(`配置项不存在: ${keyPath}`); + } + cursor = (cursor as Record)[part]; + } + if (cursor === undefined) { + throw new Error(`配置项不存在: ${keyPath}`); + } + return String(cursor); +} + +export function listConfigKeys(obj: Record, prefix = ''): string[] { + const keys: string[] = []; + for (const [key, value] of Object.entries(obj)) { + const fullPath = prefix ? `${prefix}.${key}` : key; + if (value !== null && typeof value === 'object' && !Array.isArray(value)) { + keys.push(...listConfigKeys(value as Record, fullPath)); + } else { + keys.push(fullPath); + } + } + return keys; +} + +export async function isReactPressProject(projectRoot: string): Promise { + const { configPath } = getProjectPaths(projectRoot); + return fs.pathExists(configPath); +} diff --git a/cli/src/core/services/database/index.ts b/cli/src/core/services/database/index.ts new file mode 100644 index 00000000..acc81ce8 --- /dev/null +++ b/cli/src/core/services/database/index.ts @@ -0,0 +1,316 @@ +import fs from 'fs-extra'; + +import type { + DatabaseEnsureResult, + MysqlCredentials, + ReactPressConfig, +} from '../../../types/config'; +import { getDockerComposeCommand } from '../../utils/platform'; +import { getProjectPaths } from '../../utils/paths'; +import { findAvailablePort, isDockerPortBindError, isPortAvailable } from '../../utils/port'; +import { isDockerAvailable, runSync, sleep } from '../exec'; +import { + isSqliteMode, + loadConfig, + loadEnvFile, + saveConfig, + syncEnvFromConfig, +} from '../config'; +import { + getDatabaseCredentials, + testMysqlConnection, + waitForMysql, +} from './mysql'; +import { ensureSqliteDatabase, isSqliteReady } from './sqlite'; + +export { getDatabaseCredentials, testMysqlConnection as testDatabaseConnection } from './mysql'; +export { ensureSqliteDatabase, probeSqliteDatabase, isSqliteReady } from './sqlite'; +export { resolveDatabaseProfile, requiresDocker, isLocalDatabaseMode } from './profile'; + +const DEFAULT_DB_HOST_PORT = 3306; +const EMBEDDED_DB_ROOT_PASSWORD = 'reactpress_root'; + +export async function waitForDatabase( + creds: MysqlCredentials, + maxAttempts = 40, + intervalMs = 1000, +): Promise { + return waitForMysql(creds, maxAttempts, intervalMs); +} + +export function parseDockerPublishedPort(output: string): number | null { + for (const line of output.split('\n')) { + const match = line.trim().match(/:(\d+)\s*$/); + if (match) return Number(match[1]); + } + return null; +} + +export async function getContainerPublishedHostPort( + containerName: string, + containerPort = 3306, +): Promise { + if (!isDockerAvailable()) return null; + const result = runSync('docker', ['port', containerName, `${containerPort}/tcp`], { + silent: true, + }); + if (!result.ok || !result.stdout.trim()) return null; + return parseDockerPublishedPort(result.stdout); +} + +async function persistDatabaseHostPort( + projectRoot: string, + config: ReactPressConfig, + port: number, +): Promise { + const { configPath, envPath } = getProjectPaths(projectRoot); + config.database.port = port; + if (await fs.pathExists(configPath)) { + await saveConfig(projectRoot, config); + } + if (await fs.pathExists(envPath)) { + await syncEnvFromConfig(projectRoot, config); + } +} + +async function isContainerHealthy(containerName: string): Promise { + const result = runSync( + 'docker', + [ + 'inspect', + '-f', + '{{if .State.Health}}{{.State.Health.Status}}{{else}}healthy{{end}}', + containerName, + ], + { silent: true }, + ); + if (!result.ok) return false; + return result.stdout.trim() === 'healthy'; +} + +async function buildConnectionFailureMessage( + projectRoot: string, + config: ReactPressConfig, + creds: MysqlCredentials, +): Promise { + const containerName = config.database.containerName ?? 'reactpress_cli_db'; + const published = await getContainerPublishedHostPort(containerName); + const port = published ?? creds.port; + const rootReachable = await testMysqlConnection({ + host: creds.host, + port, + user: 'root', + password: EMBEDDED_DB_ROOT_PASSWORD, + database: creds.database, + }); + const healthy = await isContainerHealthy(containerName); + if (rootReachable && healthy) { + return ( + `数据库容器已在端口 ${port} 运行,但账号「${creds.user}」无法连接(数据卷中的凭证与 .env 不一致)。` + + ` 请在项目目录执行: cd .reactpress && docker compose down -v && cd .. && reactpress init --force` + ); + } + return `数据库容器已启动,但连接超时。请执行 docker logs ${containerName} 查看详情。`; +} + +export async function ensureDatabaseHostPort( + projectRoot: string, + forcePort?: number, + configOverride?: ReactPressConfig, +): Promise<{ port: number; changed: boolean; previousPort: number }> { + const config = configOverride ?? (await loadConfig(projectRoot)); + if (isSqliteMode(config)) { + const port = config.server.port ?? DEFAULT_DB_HOST_PORT; + return { port, changed: false, previousPort: port }; + } + if (config.database.mode !== 'embedded-docker') { + const port = config.database.port ?? DEFAULT_DB_HOST_PORT; + return { port, changed: false, previousPort: port }; + } + + const { envPath } = getProjectPaths(projectRoot); + const existing = (await fs.pathExists(envPath)) ? await loadEnvFile(envPath) : {}; + const currentPort = Number(existing.DB_PORT ?? config.database.port ?? DEFAULT_DB_HOST_PORT); + const containerName = config.database.containerName ?? 'reactpress_cli_db'; + const containerPort = await getContainerPublishedHostPort(containerName); + + if (containerPort !== null && !forcePort) { + if (containerPort !== currentPort) { + await persistDatabaseHostPort(projectRoot, config, containerPort); + return { port: containerPort, changed: true, previousPort: currentPort }; + } + return { port: containerPort, changed: false, previousPort: currentPort }; + } + + if (!forcePort && (await isPortAvailable(currentPort))) { + return { port: currentPort, changed: false, previousPort: currentPort }; + } + + if (!forcePort && !(await isPortAvailable(currentPort))) { + const creds = await getDatabaseCredentials(projectRoot); + if (await testMysqlConnection({ ...creds, port: currentPort })) { + return { port: currentPort, changed: false, previousPort: currentPort }; + } + } + + let port: number; + if (forcePort && (await isPortAvailable(forcePort))) { + port = forcePort; + } else { + const start = + forcePort ?? + (currentPort === DEFAULT_DB_HOST_PORT ? DEFAULT_DB_HOST_PORT + 1 : currentPort + 1); + port = await findAvailablePort(start); + } + + if (currentPort === port && config.database.port === port) { + return { port, changed: false, previousPort: currentPort }; + } + + await persistDatabaseHostPort(projectRoot, config, port); + return { port, changed: true, previousPort: currentPort }; +} + +async function getDockerComposeEnv(projectRoot: string): Promise> { + const creds = await getDatabaseCredentials(projectRoot); + return { + DB_PORT: String(creds.port), + DB_USER: creds.user, + DB_PASSWD: creds.password, + DB_DATABASE: creds.database, + MYSQL_ROOT_PASSWORD: EMBEDDED_DB_ROOT_PASSWORD, + }; +} + +function runDockerCompose( + composeFile: string, + cwd: string, + subcommand: string[], + env: Record, +) { + const composeV2 = runSync('docker', ['compose', 'version'], { silent: true }); + if (composeV2.ok) { + return runSync('docker', ['compose', '-f', composeFile, ...subcommand], { + cwd, + silent: true, + env, + }); + } + return runSync(getDockerComposeCommand(), ['-f', composeFile, ...subcommand], { + cwd, + silent: true, + env, + }); +} + +export async function promoteToSqliteMode( + projectRoot: string, + config: ReactPressConfig, +): Promise { + const { sqlitePath } = getProjectPaths(projectRoot); + const next: ReactPressConfig = structuredClone(config); + next.database.mode = 'embedded-sqlite'; + next.database.sqlitePath = config.database.sqlitePath ?? sqlitePath; + await saveConfig(projectRoot, next); + await syncEnvFromConfig(projectRoot, next); + console.log( + '[reactpress] Switched to SQLite zero-dependency mode (no Docker/nginx required)', + ); + return ensureSqliteDatabase(projectRoot); +} + +export async function startEmbeddedDatabase( + projectRoot: string, + config: ReactPressConfig, +): Promise { + if (isSqliteMode(config)) { + await syncEnvFromConfig(projectRoot, config); + return ensureSqliteDatabase(projectRoot); + } + if (config.database.mode !== 'embedded-docker') { + return { ok: true }; + } + if (!isDockerAvailable()) { + return promoteToSqliteMode(projectRoot, config); + } + + const { dockerComposePath, reactpressDir } = getProjectPaths(projectRoot); + const maxPortRetries = 5; + + for (let attempt = 0; attempt < maxPortRetries; attempt++) { + const { port, changed, previousPort } = await ensureDatabaseHostPort( + projectRoot, + undefined, + config, + ); + if (changed) { + console.warn(`[reactpress] 宿主机端口 ${previousPort} 已被占用,已改用 ${port}(已更新 .env)`); + } + const composeEnv = await getDockerComposeEnv(projectRoot); + const result = runDockerCompose(dockerComposePath, reactpressDir, ['up', '-d'], composeEnv); + if (result.ok) break; + + const output = `${result.stderr}\n${result.stdout}`; + if (isDockerPortBindError(output) && attempt < maxPortRetries - 1) { + await ensureDatabaseHostPort(projectRoot, port + 1, config); + console.warn(`[reactpress] 端口 ${port} 绑定失败,正在尝试其他端口…`); + continue; + } + return { ok: false, message: `启动数据库容器失败: ${result.stderr || result.stdout}` }; + } + + const creds = await getDatabaseCredentials(projectRoot); + const ready = await waitForDatabase(creds); + if (!ready) { + return { + ok: false, + message: await buildConnectionFailureMessage(projectRoot, config, creds), + }; + } + return { ok: true }; +} + +export async function stopEmbeddedDatabase( + projectRoot: string, + config: ReactPressConfig, +): Promise { + if (config.database.mode !== 'embedded-docker') return; + if (!isDockerAvailable()) return; + const { dockerComposePath, reactpressDir } = getProjectPaths(projectRoot); + const composeEnv = await getDockerComposeEnv(projectRoot); + runDockerCompose(dockerComposePath, reactpressDir, ['down'], composeEnv); +} + +export async function ensureDatabase( + projectRoot: string, + config: ReactPressConfig, +): Promise { + if (isSqliteMode(config)) { + await syncEnvFromConfig(projectRoot, config); + return ensureSqliteDatabase(projectRoot); + } + + const creds = await getDatabaseCredentials(projectRoot); + if (await testMysqlConnection(creds)) { + return { ok: true }; + } + if (config.database.mode === 'embedded-docker') { + if (!isDockerAvailable()) { + return promoteToSqliteMode(projectRoot, config); + } + return startEmbeddedDatabase(projectRoot, config); + } + return { + ok: false, + message: `无法连接数据库 ${creds.host}:${creds.port},请检查 .env 中的 DB_* 配置。`, + }; +} + +export async function isDatabaseReady(projectRoot: string): Promise { + const config = await loadConfig(projectRoot); + if (isSqliteMode(config)) { + return isSqliteReady(projectRoot); + } + const creds = await getDatabaseCredentials(projectRoot); + return testMysqlConnection(creds); +} diff --git a/cli/src/core/services/database/mysql.ts b/cli/src/core/services/database/mysql.ts new file mode 100644 index 00000000..179b7cd7 --- /dev/null +++ b/cli/src/core/services/database/mysql.ts @@ -0,0 +1,92 @@ +import fs from 'fs-extra'; +import path from 'node:path'; + +import type { DatabaseEnsureResult, MysqlCredentials } from '../../../types/config'; + +export type ConnectionTester = (creds: MysqlCredentials) => Promise; + +async function defaultConnectionTester(creds: MysqlCredentials): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const mysql = require('mysql2/promise') as typeof import('mysql2/promise'); + const connection = await mysql.createConnection({ + host: creds.host, + port: creds.port, + user: creds.user, + password: creds.password, + database: creds.database, + connectTimeout: 5000, + }); + await connection.query('SELECT 1'); + await connection.end(); + return true; + } catch { + return false; + } +} + +let connectionTester: ConnectionTester = defaultConnectionTester; + +/** @internal test hook */ +export function setConnectionTesterForTests(tester: ConnectionTester | null): void { + connectionTester = tester ?? defaultConnectionTester; +} + +export async function testMysqlConnection(creds: MysqlCredentials): Promise { + return connectionTester(creds); +} + +export async function waitForMysql( + creds: MysqlCredentials, + maxAttempts = 40, + intervalMs = 1000, +): Promise { + for (let i = 0; i < maxAttempts; i++) { + if (await testMysqlConnection(creds)) return true; + await new Promise((r) => setTimeout(r, intervalMs)); + } + return false; +} + +import { loadEnvFile } from '../config'; +import { getProjectPaths } from '../../utils/paths'; + +export async function getDatabaseCredentials(projectRoot: string): Promise { + const { envPath } = getProjectPaths(projectRoot); + if (!(await fs.pathExists(envPath))) { + return { ...DEFAULT_CREDS }; + } + const env = await loadEnvFile(envPath); + return { + host: env.DB_HOST ?? DEFAULT_CREDS.host, + port: Number(env.DB_PORT ?? DEFAULT_CREDS.port), + user: env.DB_USER ?? DEFAULT_CREDS.user, + password: env.DB_PASSWD ?? DEFAULT_CREDS.password, + database: env.DB_DATABASE ?? DEFAULT_CREDS.database, + }; +} + +const DEFAULT_CREDS: MysqlCredentials = { + host: '127.0.0.1', + port: 3306, + user: 'reactpress', + password: 'reactpress', + database: 'reactpress', +}; + +export async function probeMysqlHost( + host: string, + port: number, + user: string, + password: string, + database: string, +): Promise<{ ok: boolean; error?: string }> { + try { + const ok = await testMysqlConnection({ host, port, user, password, database }); + return ok ? { ok: true } : { ok: false, error: 'connection refused' }; + } catch (err) { + return { ok: false, error: err instanceof Error ? err.message : String(err) }; + } +} + +export { DEFAULT_CREDS }; diff --git a/cli/src/core/services/database/profile.ts b/cli/src/core/services/database/profile.ts new file mode 100644 index 00000000..6e3bcc5b --- /dev/null +++ b/cli/src/core/services/database/profile.ts @@ -0,0 +1,58 @@ +import fs from 'fs-extra'; +import path from 'node:path'; + +import type { DatabaseProfile, EnvMap, ReactPressConfig } from '../../../types/config'; +import { getProjectPaths } from '../../utils/paths'; +import { isSqliteMode, loadConfig, loadEnvFile } from '../config'; + +const DEFAULT_MYSQL = { + host: '127.0.0.1', + port: 3306, + user: 'reactpress', + password: 'reactpress', + database: 'reactpress', +} as const; + +export function resolveDatabaseType(config: ReactPressConfig, env: EnvMap): 'mysql' | 'sqlite' { + const envType = String(env.DB_TYPE || '').toLowerCase(); + if (envType === 'sqlite') return 'sqlite'; + if (isSqliteMode(config)) return 'sqlite'; + return 'mysql'; +} + +export async function resolveDatabaseProfile(projectRoot: string): Promise { + const config = await loadConfig(projectRoot); + const { envPath, sqlitePath } = getProjectPaths(projectRoot); + const env = (await fs.pathExists(envPath)) ? await loadEnvFile(envPath) : {}; + const type = resolveDatabaseType(config, env); + + if (type === 'sqlite') { + const database = + env.DB_DATABASE ?? config.database.sqlitePath ?? sqlitePath; + return { + type: 'sqlite', + mode: config.database.mode, + sqlite: { database: path.resolve(projectRoot, database) }, + }; + } + + return { + type: 'mysql', + mode: config.database.mode, + mysql: { + host: env.DB_HOST ?? config.database.host ?? DEFAULT_MYSQL.host, + port: Number(env.DB_PORT ?? config.database.port ?? DEFAULT_MYSQL.port), + user: env.DB_USER ?? config.database.user ?? DEFAULT_MYSQL.user, + password: env.DB_PASSWD ?? config.database.password ?? DEFAULT_MYSQL.password, + database: env.DB_DATABASE ?? config.database.database ?? DEFAULT_MYSQL.database, + }, + }; +} + +export function isLocalDatabaseMode(config: ReactPressConfig): boolean { + return config.database.mode === 'embedded-sqlite'; +} + +export function requiresDocker(config: ReactPressConfig): boolean { + return config.database.mode === 'embedded-docker'; +} diff --git a/cli/src/core/services/database/sqlite.ts b/cli/src/core/services/database/sqlite.ts new file mode 100644 index 00000000..581d9409 --- /dev/null +++ b/cli/src/core/services/database/sqlite.ts @@ -0,0 +1,107 @@ +import fs from 'fs-extra'; +import path from 'node:path'; + +import type { DatabaseEnsureResult, SqliteCredentials } from '../../../types/config'; +import { getProjectPaths, SQLITE_REL_PATH } from '../../utils/paths'; +import { loadEnvFile } from '../config'; + +const LEGACY_SQLITE_REL_PATH = path.join('data', 'reactpress.db'); + +async function maybeMigrateLegacySqlite(projectRoot: string, targetPath: string): Promise { + const legacyPath = path.join(projectRoot, LEGACY_SQLITE_REL_PATH); + const normalizedTarget = path.resolve(projectRoot, targetPath); + const normalizedLegacy = path.resolve(legacyPath); + + if (normalizedLegacy === normalizedTarget) return; + if (!(await fs.pathExists(legacyPath))) return; + if (await fs.pathExists(normalizedTarget)) return; + + await fs.ensureDir(path.dirname(normalizedTarget)); + await fs.copyFile(legacyPath, normalizedTarget); + console.log( + `[reactpress] Migrated SQLite database: ${LEGACY_SQLITE_REL_PATH} → ${path.relative(projectRoot, normalizedTarget) || SQLITE_REL_PATH}`, + ); +} + +export async function resolveSqlitePath( + projectRoot: string, + override?: string, +): Promise { + const paths = getProjectPaths(projectRoot); + if (override) { + const resolved = path.resolve(projectRoot, override); + await maybeMigrateLegacySqlite(projectRoot, resolved); + return resolved; + } + + if (await fs.pathExists(paths.envPath)) { + const env = await loadEnvFile(paths.envPath); + if (env.DB_DATABASE) { + const resolved = path.resolve(projectRoot, env.DB_DATABASE); + await maybeMigrateLegacySqlite(projectRoot, resolved); + return resolved; + } + } + + const target = paths.sqlitePath; + await maybeMigrateLegacySqlite(projectRoot, target); + return target; +} + +export async function getSqliteCredentials(projectRoot: string): Promise { + const database = await resolveSqlitePath(projectRoot); + return { database }; +} + +export async function ensureSqliteDatabase(projectRoot: string): Promise { + const database = await resolveSqlitePath(projectRoot); + const dir = path.dirname(database); + await fs.ensureDir(dir); + + try { + await fs.access(dir, fs.constants.W_OK); + } catch { + return { ok: false, message: `SQLite 数据目录不可写: ${dir}` }; + } + + if (!(await fs.pathExists(database))) { + await fs.writeFile(database, Buffer.alloc(0)); + } + + return { ok: true }; +} + +export async function probeSqliteDatabase( + projectRoot: string, +): Promise<{ ok: boolean; message?: string }> { + const database = await resolveSqlitePath(projectRoot); + const dir = path.dirname(database); + + if (!(await fs.pathExists(dir))) { + return { ok: false, message: `SQLite 目录不存在: ${dir}` }; + } + + try { + await fs.access(dir, fs.constants.W_OK); + } catch { + return { ok: false, message: `SQLite 目录不可写: ${dir}` }; + } + + if (await fs.pathExists(database)) { + const stat = await fs.stat(database); + return { + ok: true, + message: `SQLite ${database} (${stat.size} bytes)`, + }; + } + + return { + ok: true, + message: `SQLite 将在启动时创建: ${database}`, + }; +} + +export async function isSqliteReady(projectRoot: string): Promise { + const result = await ensureSqliteDatabase(projectRoot); + return result.ok; +} diff --git a/cli/src/core/services/exec.ts b/cli/src/core/services/exec.ts new file mode 100644 index 00000000..deb69a54 --- /dev/null +++ b/cli/src/core/services/exec.ts @@ -0,0 +1,70 @@ +import { spawn, spawnSync, type ChildProcess } from 'node:child_process'; +import crossSpawn from 'cross-spawn'; + +import { isWindows } from '../utils/platform'; + +export interface RunSyncResult { + ok: boolean; + stdout: string; + stderr: string; + code: number | null; +} + +export function runSync( + command: string, + args: string[], + options: { + cwd?: string; + env?: NodeJS.ProcessEnv; + silent?: boolean; + } = {}, +): RunSyncResult { + const result = spawnSync(command, args, { + cwd: options.cwd, + env: { ...process.env, ...options.env }, + encoding: 'utf8', + shell: isWindows(), + stdio: options.silent ? 'pipe' : 'inherit', + }); + return { + ok: result.status === 0, + stdout: (result.stdout ?? '').toString(), + stderr: (result.stderr ?? '').toString(), + code: result.status, + }; +} + +export function spawnDetached( + command: string, + args: string[], + options: { cwd?: string; env?: NodeJS.ProcessEnv } = {}, +): ChildProcess { + return crossSpawn(command, args, { + cwd: options.cwd, + env: { ...process.env, ...options.env }, + detached: !isWindows(), + stdio: 'ignore', + shell: isWindows(), + }); +} + +export function isCommandAvailable(command: string): boolean { + const checkCmd = isWindows() ? 'where' : 'which'; + const result = spawnSync(checkCmd, [command], { + encoding: 'utf8', + shell: isWindows(), + stdio: 'pipe', + }); + return result.status === 0; +} + +export function isDockerAvailable(): boolean { + const result = runSync('docker', ['info'], { silent: true }); + return result.ok; +} + +export async function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export let spawnFn = spawn; diff --git a/cli/src/core/services/init.ts b/cli/src/core/services/init.ts new file mode 100644 index 00000000..12c363b6 --- /dev/null +++ b/cli/src/core/services/init.ts @@ -0,0 +1,14 @@ +import { initLocalProject } from './local-site'; + +export interface InitProjectOptions { + directory: string; + force?: boolean; + /** @deprecated v4 always uses SQLite */ + local?: boolean; +} + +export async function initProject( + options: InitProjectOptions, +): Promise<{ ok: boolean; projectRoot: string; message: string }> { + return initLocalProject(options.directory, { force: options.force }); +} diff --git a/cli/src/core/services/local-site.ts b/cli/src/core/services/local-site.ts new file mode 100644 index 00000000..019cb2da --- /dev/null +++ b/cli/src/core/services/local-site.ts @@ -0,0 +1,266 @@ +import fs from 'fs-extra'; +import path from 'node:path'; + +import type { ReactPressConfig } from '../../types/config'; +import { CONFIG_DIR, getPackageRoot, getProjectPaths, getTemplatesDir, SQLITE_REL_PATH } from '../utils/paths'; +import { saveConfig, syncEnvFromConfig } from '../services/config'; + +export interface LocalSitePaths { + siteRoot: string; + dataDir: string; + uploadsDir: string; + dbPath: string; + envPath: string; + reactpressDir: string; +} + +export function getLocalSitePaths(siteRoot: string): LocalSitePaths { + const reactpressDir = path.join(siteRoot, CONFIG_DIR); + return { + siteRoot, + dataDir: reactpressDir, + uploadsDir: path.join(siteRoot, 'uploads'), + dbPath: path.join(reactpressDir, 'reactpress.db'), + envPath: path.join(siteRoot, '.env'), + reactpressDir, + }; +} + +export interface EnsureLocalSiteOptions { + monorepoRoot?: string; + adminUser?: string; + adminPassword?: string; +} + +export function ensureLocalSite( + siteRoot: string, + port: number, + options: EnsureLocalSiteOptions = {}, +): LocalSitePaths { + const paths = getLocalSitePaths(siteRoot); + fs.mkdirSync(paths.reactpressDir, { recursive: true }); + fs.mkdirSync(paths.uploadsDir, { recursive: true }); + + const siteUrl = `http://127.0.0.1:${port}`; + const clientSiteUrl = 'http://localhost:3001'; + const envLines = [ + 'DB_TYPE=sqlite', + `DB_DATABASE=${SQLITE_REL_PATH}`, + `SERVER_PORT=${port}`, + `SERVER_SITE_URL=${siteUrl}`, + `CLIENT_SITE_URL=${clientSiteUrl}`, + 'SERVER_API_PREFIX=/api', + `REACTPRESS_UPLOAD_DIR=${paths.uploadsDir}`, + `ADMIN_USER=${options.adminUser ?? 'admin'}`, + `ADMIN_PASSWD=${options.adminPassword ?? 'admin'}`, + `REACTPRESS_LANG=${process.env.REACTPRESS_LANG ?? 'zh'}`, + '', + ]; + fs.writeFileSync(paths.envPath, envLines.join('\n'), 'utf8'); + + if (options.monorepoRoot) { + seedBundledAssets(siteRoot, options.monorepoRoot); + } + ensureBundledPlugins(siteRoot); + + const configPath = path.join(paths.reactpressDir, 'config.json'); + if (!fs.existsSync(configPath)) { + const config: ReactPressConfig = { + version: 1, + database: { mode: 'embedded-sqlite', sqlitePath: SQLITE_REL_PATH }, + server: { + port, + apiPrefix: '/api', + siteUrl, + clientUrl: clientSiteUrl, + serverUrl: siteUrl, + }, + }; + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8'); + } + + return paths; +} + +function seedBundledAssets(siteRoot: string, monorepoRoot: string): void { + seedSymlinkRegistry( + path.join(monorepoRoot, 'plugins'), + path.join(siteRoot, 'plugins'), + 'local', + ); + seedSymlinkRegistry( + path.join(monorepoRoot, 'themes'), + path.join(siteRoot, 'themes'), + 'local', + 'npm', + ); + seedRuntimeThemes(siteRoot, monorepoRoot); +} + +function seedSymlinkRegistry( + sourceDir: string, + targetDir: string, + ...registryKeys: string[] +): void { + const sourcePackageJson = path.join(sourceDir, 'package.json'); + const targetPackageJson = path.join(targetDir, 'package.json'); + if (!fs.existsSync(sourcePackageJson)) return; + + fs.mkdirSync(targetDir, { recursive: true }); + if (!fs.existsSync(targetPackageJson)) { + fs.copyFileSync(sourcePackageJson, targetPackageJson); + } + + let meta: { reactpress?: Record } = {}; + try { + meta = JSON.parse(fs.readFileSync(targetPackageJson, 'utf8')) as typeof meta; + } catch { + return; + } + + for (const key of registryKeys) { + const ids = Array.isArray(meta.reactpress?.[key]) ? meta.reactpress[key] : []; + for (const id of ids) { + if (typeof id !== 'string' || !id.trim()) continue; + const sourcePath = path.join(sourceDir, id.trim()); + const targetPath = path.join(targetDir, id.trim()); + if (!fs.existsSync(sourcePath) || fs.existsSync(targetPath)) continue; + fs.symlinkSync(sourcePath, targetPath, 'dir'); + } + } +} + +function seedRuntimeThemes(siteRoot: string, monorepoRoot: string): void { + const sourceRuntime = path.join(monorepoRoot, '.reactpress', 'runtime'); + const targetRuntime = path.join(siteRoot, '.reactpress', 'runtime'); + if (!fs.existsSync(sourceRuntime)) return; + + fs.mkdirSync(path.join(siteRoot, '.reactpress'), { recursive: true }); + + for (const entry of fs.readdirSync(sourceRuntime, { withFileTypes: true })) { + if (!entry.isDirectory() && !entry.isSymbolicLink()) continue; + const sourcePath = path.join(sourceRuntime, entry.name); + const targetPath = path.join(targetRuntime, entry.name); + if (fs.existsSync(targetPath)) continue; + try { + if (!fs.statSync(sourcePath).isDirectory()) continue; + fs.mkdirSync(targetRuntime, { recursive: true }); + fs.symlinkSync(sourcePath, targetPath, 'dir'); + } catch { + // skip broken entries + } + } +} + +const PLUGIN_COPY_SKIP_DIRS = new Set(['node_modules', '.git', '.turbo', 'src', 'coverage']); + +function hasUsablePluginsRegistry(pluginsDir: string): boolean { + const pkgPath = path.join(pluginsDir, 'package.json'); + if (!fs.existsSync(pkgPath)) return false; + + try { + const meta = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { + reactpress?: { local?: string[] }; + }; + const local = Array.isArray(meta.reactpress?.local) ? meta.reactpress.local : []; + return local.some( + (id) => + typeof id === 'string' && + id.trim() && + fs.existsSync(path.join(pluginsDir, id.trim(), 'plugin.json')), + ); + } catch { + return false; + } +} + +function copyPluginTree(sourceDir: string, targetDir: string): void { + fs.mkdirSync(targetDir, { recursive: true }); + for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) { + const sourcePath = path.join(sourceDir, entry.name); + const targetPath = path.join(targetDir, entry.name); + + if (entry.isDirectory()) { + if (PLUGIN_COPY_SKIP_DIRS.has(entry.name)) continue; + copyPluginTree(sourcePath, targetPath); + continue; + } + + fs.copyFileSync(sourcePath, targetPath); + } +} + +function copyBundledPlugins(sourceDir: string, targetDir: string): void { + const sourcePackageJson = path.join(sourceDir, 'package.json'); + if (!fs.existsSync(sourcePackageJson)) return; + + fs.mkdirSync(targetDir, { recursive: true }); + fs.copyFileSync(sourcePackageJson, path.join(targetDir, 'package.json')); + + let meta: { reactpress?: { local?: string[] } } = {}; + try { + meta = JSON.parse(fs.readFileSync(sourcePackageJson, 'utf8')) as typeof meta; + } catch { + return; + } + + const local = Array.isArray(meta.reactpress?.local) ? meta.reactpress.local : []; + for (const id of local) { + if (typeof id !== 'string' || !id.trim()) continue; + const sourcePath = path.join(sourceDir, id.trim()); + const targetPath = path.join(targetDir, id.trim()); + if (!fs.existsSync(path.join(sourcePath, 'plugin.json')) || fs.existsSync(targetPath)) continue; + copyPluginTree(sourcePath, targetPath); + } +} + +/** Seed default plugins from the CLI bundle when the site has no plugins registry yet. */ +export function ensureBundledPlugins(siteRoot: string): boolean { + const targetDir = path.join(siteRoot, 'plugins'); + if (hasUsablePluginsRegistry(targetDir)) return false; + + const bundledDir = path.join(getPackageRoot(), 'plugins'); + if (fs.existsSync(path.join(bundledDir, 'package.json'))) { + copyBundledPlugins(bundledDir, targetDir); + return hasUsablePluginsRegistry(targetDir); + } + + return false; +} + +export async function initLocalProject( + projectRoot: string, + options: { force?: boolean; port?: number } = {}, +): Promise<{ ok: boolean; projectRoot: string; message: string }> { + const paths = getProjectPaths(projectRoot); + const port = options.port ?? 3002; + + if ((await fs.pathExists(paths.configPath)) && !options.force) { + const { t } = require('../../lib/i18n'); + return { + ok: true, + projectRoot, + message: t('init.alreadyInitialized'), + }; + } + + await fs.ensureDir(projectRoot); + ensureLocalSite(projectRoot, port); + + const templatesDir = getTemplatesDir(); + const pkgTemplate = path.join(templatesDir, 'package.json'); + const pkgDest = path.join(projectRoot, 'package.json'); + if ((await fs.pathExists(pkgTemplate)) && (!(await fs.pathExists(pkgDest)) || options.force)) { + await fs.copy(pkgTemplate, pkgDest, { overwrite: true }); + } + + const config = (await fs.readJson(paths.configPath)) as ReactPressConfig; + await saveConfig(projectRoot, config); + await syncEnvFromConfig(projectRoot, config); + + return { + ok: true, + projectRoot, + message: 'ReactPress 项目初始化完成。', + }; +} diff --git a/cli/src/core/services/theme-bootstrap.ts b/cli/src/core/services/theme-bootstrap.ts new file mode 100644 index 00000000..3c2b0c27 --- /dev/null +++ b/cli/src/core/services/theme-bootstrap.ts @@ -0,0 +1,66 @@ +import fs from 'fs-extra'; +import path from 'node:path'; + +function writeActiveThemeManifest(projectRoot: string, themeId: string, themeDir: string): void { + const manifestPath = path.join(projectRoot, '.reactpress', 'active-theme.json'); + const themeDirRel = path.relative(projectRoot, themeDir); + fs.mkdirSync(path.dirname(manifestPath), { recursive: true }); + fs.writeFileSync( + manifestPath, + JSON.stringify( + { + activeTheme: themeId, + themeDir: themeDirRel, + updatedAt: new Date().toISOString(), + }, + null, + 2, + ), + 'utf8', + ); +} + +export interface EnsureDefaultThemeResult { + installed: boolean; + skipped?: boolean; + themeId?: string; + error?: string; +} + +/** Install catalog featured theme when no resolvable theme exists (npm standalone init). */ +export async function ensureDefaultTheme(projectRoot: string): Promise { + const { hasResolvableActiveTheme } = require('../../lib/theme-runtime'); + const { installThemeFromNpm } = require('../../lib/theme-install'); + const { readThemeCatalog } = require('../../lib/theme-registry'); + const { t } = require('../../lib/i18n'); + + const root = path.resolve(projectRoot); + if (hasResolvableActiveTheme(root)) { + return { installed: false, skipped: true }; + } + + const catalog = readThemeCatalog(root); + const featured = + catalog.themes.find((entry: { featured?: boolean; npm?: string }) => entry.featured && entry.npm) ?? + catalog.themes.find((entry: { npm?: string }) => entry.npm); + if (!featured?.npm) { + return { installed: false, error: 'NO_CATALOG_THEME' }; + } + + console.log(t('themeInstall.installing', { spec: featured.npm })); + try { + const result = await installThemeFromNpm(root, featured.npm); + writeActiveThemeManifest(root, result.themeId, result.themeDir); + console.log( + t('themeInstall.success', { + name: featured.name ?? result.themeId, + id: result.themeId, + dir: path.relative(root, result.themeDir), + }), + ); + return { installed: true, themeId: result.themeId }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return { installed: false, error: message }; + } +} diff --git a/cli/src/core/utils/cli-context.ts b/cli/src/core/utils/cli-context.ts new file mode 100644 index 00000000..1f171268 --- /dev/null +++ b/cli/src/core/utils/cli-context.ts @@ -0,0 +1,9 @@ +let projectCwd: string | undefined; + +export function setProjectCwd(cwd?: string): void { + projectCwd = cwd ? cwd : undefined; +} + +export function getProjectCwd(): string { + return projectCwd ?? process.cwd(); +} diff --git a/cli/src/core/utils/paths.ts b/cli/src/core/utils/paths.ts new file mode 100644 index 00000000..2a3671a4 --- /dev/null +++ b/cli/src/core/utils/paths.ts @@ -0,0 +1,44 @@ +import { join } from 'node:path'; + +export const CONFIG_DIR = '.reactpress'; +export const CONFIG_FILE = 'config.json'; +export const PID_FILE = 'server.pid'; +export const ENV_FILE = '.env'; +/** Relative SQLite file under project root (runtime data lives in `.reactpress/`). */ +export const SQLITE_REL_PATH = join(CONFIG_DIR, 'reactpress.db'); + +/** CLI 包根目录(编译后位于 out/core/utils → ../../../) */ +export function getPackageRoot(): string { + return join(__dirname, '..', '..', '..'); +} + +export function getTemplatesDir(): string { + return join(getPackageRoot(), 'templates'); +} + +export interface ProjectPaths { + projectRoot: string; + reactpressDir: string; + configPath: string; + pidPath: string; + envPath: string; + dockerComposePath: string; + dbDataDir: string; + sqlitePath: string; + uploadsDir: string; +} + +export function getProjectPaths(projectRoot: string): ProjectPaths { + const reactpressDir = join(projectRoot, CONFIG_DIR); + return { + projectRoot, + reactpressDir, + configPath: join(reactpressDir, CONFIG_FILE), + pidPath: join(reactpressDir, PID_FILE), + envPath: join(projectRoot, ENV_FILE), + dockerComposePath: join(reactpressDir, 'docker-compose.yml'), + dbDataDir: reactpressDir, + sqlitePath: join(reactpressDir, 'reactpress.db'), + uploadsDir: join(projectRoot, 'uploads'), + }; +} diff --git a/cli/src/core/utils/platform.ts b/cli/src/core/utils/platform.ts new file mode 100644 index 00000000..e044bfd9 --- /dev/null +++ b/cli/src/core/utils/platform.ts @@ -0,0 +1,25 @@ +import { platform } from 'node:os'; + +export function isWindows(): boolean { + return platform() === 'win32'; +} + +export function isMac(): boolean { + return platform() === 'darwin'; +} + +export function getDockerComposeCommand(): string { + return isWindows() ? 'docker-compose.exe' : 'docker-compose'; +} + +export function getNpmCommand(): string { + return isWindows() ? 'npm.cmd' : 'npm'; +} + +export function getNpxCommand(): string { + return isWindows() ? 'npx.cmd' : 'npx'; +} + +export function getNodeCommand(): string { + return process.execPath; +} diff --git a/cli/src/core/utils/port.ts b/cli/src/core/utils/port.ts new file mode 100644 index 00000000..ed26b07c --- /dev/null +++ b/cli/src/core/utils/port.ts @@ -0,0 +1,30 @@ +import net from 'node:net'; + +const DEFAULT_MAX_ATTEMPTS = 100; + +export function isPortAvailable(port: number, host = '0.0.0.0'): Promise { + return new Promise((resolve) => { + const server = net.createServer(); + server.once('error', () => resolve(false)); + server.once('listening', () => { + server.close(() => resolve(true)); + }); + server.listen(port, host); + }); +} + +export async function findAvailablePort( + startPort: number, + maxAttempts = DEFAULT_MAX_ATTEMPTS, +): Promise { + for (let port = startPort; port < startPort + maxAttempts; port++) { + if (await isPortAvailable(port)) { + return port; + } + } + throw new Error(`在 ${startPort}-${startPort + maxAttempts - 1} 范围内未找到可用端口`); +} + +export function isDockerPortBindError(output: string): boolean { + return /port is already allocated|address already in use/i.test(output); +} diff --git a/cli/src/lib/api-dev-runner.ts b/cli/src/lib/api-dev-runner.ts new file mode 100644 index 00000000..4caed570 --- /dev/null +++ b/cli/src/lib/api-dev-runner.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +// @ts-nocheck +const { runApiDev } = require('./api-dev'); +const { ensureOriginalCwd } = require('./root'); + +ensureOriginalCwd(); +runApiDev(); diff --git a/cli/src/lib/api-dev.ts b/cli/src/lib/api-dev.ts new file mode 100644 index 00000000..df5fc94b --- /dev/null +++ b/cli/src/lib/api-dev.ts @@ -0,0 +1,123 @@ +// @ts-nocheck +const { spawn } = require('child_process'); +const path = require('path'); +const { ensureProjectEnvironment } = require('./bootstrap'); +const { applyAutoLocalDevFallback } = require('./dev'); +const { + getServerBin, + getServerDir, + isUsingMonorepoServer, + canStartLocalApi, +} = require('./paths'); +const { stopApi } = require('./lifecycle'); +const { ensureOriginalCwd } = require('./root'); +const { t } = require('./i18n'); +const { ensureApiPortFree } = require('./ports'); +const { ensureDevDatabase } = require('./docker'); +const { ensureBundledServerDeps } = require('./server-bundle'); + +let apiChild; + +function stopApiDev(projectRoot) { + if (apiChild && !apiChild.killed) { + apiChild.kill('SIGTERM'); + } + if (!isUsingMonorepoServer(projectRoot)) { + stopApi(projectRoot); + } +} + +function startApiDev(projectRoot) { + if (isUsingMonorepoServer(projectRoot)) { + console.log(t('apiDev.modeServer')); + apiChild = spawn('pnpm', ['run', '--dir', './server', 'dev'], { + cwd: projectRoot, + stdio: 'inherit', + shell: true, + env: { + ...process.env, + REACTPRESS_ORIGINAL_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, + }, + }); + } else { + console.error(t('lifecycle.noServerAvailable')); + process.exit(1); + } + + if (apiChild) { + apiChild.on('close', (code) => { + process.exit(code ?? 0); + }); + console.log(t('apiDev.ctrlCHint')); + console.log(t('apiDev.stopHint')); + } +} + +async function runApiDev(projectRoot = ensureOriginalCwd()) { + const skipEnvBootstrap = process.env.REACTPRESS_DEV_DB_READY === '1'; + + if (!skipEnvBootstrap) { + try { + await ensureProjectEnvironment(projectRoot, { skipDatabase: true }); + } catch (err) { + console.error(t('dev.envFailed'), err.message || err); + process.exit(1); + } + + await applyAutoLocalDevFallback(projectRoot, { needsLocalApi: true }); + + try { + await ensureDevDatabase(projectRoot); + } catch (err) { + console.error(t('dev.dbEnsureFailed', { message: err.message || err })); + process.exit(1); + } + } + + process.on('SIGINT', () => { + stopApiDev(projectRoot); + process.exit(0); + }); + process.on('SIGTERM', () => { + stopApiDev(projectRoot); + process.exit(0); + }); + + if (process.env.REACTPRESS_DEV_PORTS_READY !== '1') { + const { reused, port: apiPort } = await ensureApiPortFree(projectRoot); + if (reused) { + console.log(t('dev.apiReusing', { port: apiPort })); + return; + } + } + + if (!isUsingMonorepoServer(projectRoot)) { + const bundled = await ensureBundledServerDeps(projectRoot); + if (!bundled.ok) { + console.error(bundled.message || t('bundle.serverBundle.notBuilt')); + process.exit(1); + } + } + + startApiDev(projectRoot); +} + +function getApiDevScriptPath() { + return path.join(__dirname, 'api-dev-runner.js'); +} + +module.exports = { + runApiDev, + stopApiDev, + getApiDevScriptPath, +}; diff --git a/cli/src/lib/bootstrap.ts b/cli/src/lib/bootstrap.ts new file mode 100644 index 00000000..d82ea229 --- /dev/null +++ b/cli/src/lib/bootstrap.ts @@ -0,0 +1,87 @@ +// @ts-nocheck +import fs from 'node:fs'; +import path from 'node:path'; + +import { setProjectCwd } from '../core/utils/cli-context'; +import { loadConfig, isReactPressProject } from '../core/services/config'; +import { ensureDatabase, ensureDatabaseHostPort } from '../core/services/database'; +import { initProject } from '../core/services/init'; +import { ensureOriginalCwd, isMonorepoCheckout } from './root'; +import { t } from './i18n'; + +function applyZeroDependencyDefaults(): void { + if (!process.env.REACTPRESS_LOCAL_MODE) { + process.env.REACTPRESS_LOCAL_MODE = '1'; + } + if (!process.env.REACTPRESS_SKIP_NGINX) { + process.env.REACTPRESS_SKIP_NGINX = '1'; + } +} + +export async function initMonorepoProject( + projectRoot: string, + { force = false }: { force?: boolean; local?: boolean } = {}, +): Promise<{ ok: boolean; projectRoot: string; message: string }> { + applyZeroDependencyDefaults(); + + const { getProjectPaths } = require('../core/utils/paths'); + const paths = getProjectPaths(projectRoot); + + if (fs.existsSync(paths.configPath) && !force) { + const config = await loadConfig(projectRoot); + await ensureDatabaseHostPort(projectRoot, undefined, config); + const dbResult = await ensureDatabase(projectRoot, config); + if (!dbResult.ok) { + return { ok: false, projectRoot, message: dbResult.message ?? t('bootstrap.dbPendingShort') }; + } + return { ok: true, projectRoot, message: t('bootstrap.configReady') }; + } + + return initProject({ directory: projectRoot, force }); +} + +export async function ensureProjectEnvironment( + projectRoot = ensureOriginalCwd(), + options: { skipDatabase?: boolean } = {}, +): Promise<{ ok: boolean; projectRoot: string; message: string | null }> { + applyZeroDependencyDefaults(); + + const root = path.resolve(projectRoot); + setProjectCwd(root); + + try { + const { ensureBundledPlugins } = require('../core/services/local-site'); + if (ensureBundledPlugins(root)) { + console.log(`[reactpress] ${t('init.pluginsSeeded')}`); + } + } catch { + // bundled plugins are optional; ignore when CLI core is unavailable + } + + if (!(await isReactPressProject(root))) { + const result = await initProject({ directory: root, force: false }); + if (!result.ok) { + throw new Error(result.message || t('bootstrap.cliInitFailed')); + } + return result; + } + + const config = await loadConfig(root); + if (options.skipDatabase) { + return { ok: true, projectRoot: root, message: null }; + } + + await ensureDatabaseHostPort(root, undefined, config); + const dbResult = await ensureDatabase(root, config); + if (!dbResult.ok) { + throw new Error( + t('bootstrap.dbNotReady', { + message: dbResult.message || t('bootstrap.dbPendingShort'), + }), + ); + } + + return { ok: true, projectRoot: root, message: t('bootstrap.dbReady') }; +} + +export { isMonorepoCheckout }; diff --git a/cli/src/lib/build.ts b/cli/src/lib/build.ts new file mode 100644 index 00000000..3042ebb7 --- /dev/null +++ b/cli/src/lib/build.ts @@ -0,0 +1,225 @@ +// @ts-nocheck +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 { hasWeb } = require('./project-type'); +const { t } = require('./i18n'); +const { shouldBuildToolkit } = require('./toolkit-build'); +const { hasUsableProductionBuild, readActiveThemeBuildState } = require('./theme-prod'); +const { resolveBuildNodeEnv } = require('./prod-memory'); + +const FORBIDDEN_SCRIPTS = new Set(['build']); + +/** @type {Record} */ +const BUILD_STEPS = { + toolkit: [{ script: 'build:toolkit', labelKey: 'build.label.toolkit' }], + plugins: [{ script: 'build:plugins', labelKey: 'build.label.plugins' }], + server: [{ script: 'build:server', labelKey: 'build.label.server' }], + web: [{ script: 'build:web', labelKey: 'build.label.web' }], + theme: [{ script: 'build:theme', labelKey: 'build.label.theme' }], + docs: [{ script: 'build:docs', labelKey: 'build.label.docs' }], +}; + +const TARGETS = [...Object.keys(BUILD_STEPS), 'all']; + +function getBuildSteps(target, projectRoot) { + if (target !== 'all') { + return BUILD_STEPS[target]; + } + + const steps = [ + { script: 'build:toolkit', labelKey: 'build.label.toolkit' }, + { script: 'build:plugins', labelKey: 'build.label.plugins' }, + { script: 'build:server', labelKey: 'build.label.server' }, + ]; + if (hasWeb(projectRoot)) { + steps.push({ script: 'build:web', labelKey: 'build.label.web' }); + } + steps.push({ script: 'build:theme', labelKey: 'build.label.theme' }); + return steps; +} + +const buildChildEnv = resolveBuildNodeEnv({ 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:plugins') { + const pluginsDir = path.join(root, 'plugins'); + if (fs.existsSync(path.join(pluginsDir, 'package.json'))) { + return { command: 'pnpm', args: ['run', 'build'], cwd: pluginsDir }; + } + const rootScripts = readPackageScripts(path.join(root, 'package.json')); + if (rootScripts['build:plugins']) { + return { command: 'pnpm', args: ['run', 'build:plugins'], cwd: root }; + } + } + + if (script === 'build:web') { + const webDir = path.join(root, 'web'); + if (fs.existsSync(path.join(webDir, 'package.json'))) { + return { command: 'pnpm', args: ['run', 'build'], cwd: webDir }; + } + const rootScripts = readPackageScripts(path.join(root, 'package.json')); + if (rootScripts['build:web']) { + return { command: 'pnpm', args: ['run', 'build:web'], cwd: root }; + } + return null; + } + + if (script === 'build:theme') { + const { readActiveThemeManifest, resolveThemeDirectory } = require('./theme-runtime'); + const { activeTheme } = readActiveThemeManifest(root); + const themeDir = resolveThemeDirectory(root, activeTheme); + if (themeDir && fs.existsSync(path.join(themeDir, 'package.json'))) { + return { command: 'pnpm', args: ['run', 'build'], cwd: themeDir }; + } + } + + 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')); + } + + const steps = getBuildSteps(target, projectRoot); + if (!steps) { + throw new Error( + t('build.unknownTarget', { + target, + available: TARGETS.join(', '), + }) + ); + } + + const total = steps.length; + const buildStarted = Date.now(); + + console.log(''); + if (total > 1) { + console.log(label(t('build.plan', { total }))); + console.log(''); + } + + for (let i = 0; i < steps.length; i++) { + const { script, labelKey } = steps[i]; + if (FORBIDDEN_SCRIPTS.has(script)) { + throw new Error(t('build.forbiddenScript', { script })); + } + + const current = i + 1; + const stepLabel = t(labelKey); + const stepStarted = Date.now(); + const badge = stepBadge(current, total); + + if (script === 'build:toolkit' && !shouldBuildToolkit(projectRoot)) { + console.log(` ${badge} ${ok(t('build.stepSkippedFresh', { label: stepLabel }))}`); + continue; + } + + if (script === 'build:theme') { + const themeState = readActiveThemeBuildState(projectRoot); + if ( + themeState && + hasUsableProductionBuild(themeState.themeDir, themeState.activeTheme) + ) { + console.log( + ` ${badge} ${ok(t('build.stepSkippedReuse', { label: stepLabel, id: themeState.activeTheme }))}`, + ); + continue; + } + } + + 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(); + + try { + runSync(invocation.command, invocation.args, { + cwd: invocation.cwd, + env: buildChildEnv, + }); + } catch (err) { + spinner.fail(`${badge} ${t('build.stepFailed', { current, total, label: stepLabel })}`); + throw err; + } + + const seconds = ((Date.now() - stepStarted) / 1000).toFixed(1); + 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(''); + console.log(` ${icon.spark} ${ok(t('build.done', { seconds: totalSeconds }))}`); + } + console.log(''); +} + +module.exports = { + runBuild, + TARGETS, + BUILD_STEPS, + getBuildSteps, + resolveBuildInvocation, +}; diff --git a/cli/src/lib/client-lifecycle.ts b/cli/src/lib/client-lifecycle.ts new file mode 100644 index 00000000..2e46fb3e --- /dev/null +++ b/cli/src/lib/client-lifecycle.ts @@ -0,0 +1,101 @@ +// @ts-nocheck +const ora = require('ora'); +const { getThemeBin } = require('./paths'); +const { loadClientSiteUrl } = require('./http'); +const { runNodeScript } = require('./spawn'); +const { readClientPid, isProcessRunning, clearClientPidFile } = require('./process'); +const { + PM2_CLIENT_APP, + isPm2RuntimeAvailable, + isPm2AppOnline, + getPm2AppPid, +} = require('./pm2-runtime'); +const { t } = require('./i18n'); + +function stopClient(projectRoot) { + const pid = readClientPid(projectRoot); + if (pid && isProcessRunning(pid)) { + try { + process.kill(pid, 'SIGTERM'); + console.log(t('lifecycle.clientStopped', { pid })); + } catch (err) { + console.warn(t('lifecycle.stopClientPidFailed', { pid }), err.message); + } + } + clearClientPidFile(projectRoot); +} + +async function startClientInBackground(projectRoot) { + const preferPm2 = isPm2RuntimeAvailable(projectRoot); + + if (preferPm2 && isPm2AppOnline(projectRoot, PM2_CLIENT_APP)) { + const pid = getPm2AppPid(projectRoot, PM2_CLIENT_APP); + console.log(t('lifecycle.clientAlreadyRunning', { pid: pid ?? PM2_CLIENT_APP })); + return 0; + } + + const existing = readClientPid(projectRoot); + if (!preferPm2 && existing && isProcessRunning(existing)) { + console.log(t('lifecycle.clientAlreadyRunning', { pid: existing })); + return 0; + } + clearClientPidFile(projectRoot); + + const themeBin = getThemeBin(projectRoot); + const { resolveThemeDirectory, readActiveThemeManifest } = require('./theme-runtime'); + const { activeTheme } = readActiveThemeManifest(projectRoot); + const themeDir = resolveThemeDirectory(projectRoot, activeTheme); + + const bgFlag = preferPm2 ? '--pm2-bg' : '--bg'; + await runNodeScript(themeBin, [bgFlag], { + cwd: projectRoot, + env: themeDir ? { REACTPRESS_THEME_DIR: themeDir } : undefined, + }); + + const supervised = isPm2AppOnline(projectRoot, PM2_CLIENT_APP); + const pid = supervised ? getPm2AppPid(projectRoot, PM2_CLIENT_APP) : readClientPid(projectRoot); + if (!supervised && !pid) { + console.error(t('lifecycle.clientPidMissing')); + return 1; + } + + const clientUrl = loadClientSiteUrl(projectRoot); + const spinner = ora({ + text: t('lifecycle.waitingClient', { url: clientUrl }), + color: 'magenta', + spinner: 'dots', + }).start(); + + const deadline = Date.now() + 180_000; + let ready = false; + while (Date.now() < deadline) { + const alive = supervised + ? isPm2AppOnline(projectRoot, PM2_CLIENT_APP) + : pid + ? isProcessRunning(pid) + : false; + if (!alive) { + spinner.fail(t('lifecycle.clientExitedEarly', { pid: pid ?? '—' })); + return 1; + } + if (await require('./http').isHttpResponding(clientUrl)) { + ready = true; + break; + } + await new Promise((r) => setTimeout(r, 500)); + } + + if (!ready) { + spinner.fail(t('lifecycle.clientTimeout', { url: clientUrl })); + return 1; + } + + const readyPid = supervised ? getPm2AppPid(projectRoot, PM2_CLIENT_APP) ?? PM2_CLIENT_APP : pid; + spinner.succeed(t('lifecycle.clientReady', { url: clientUrl, pid: readyPid })); + return 0; +} + +module.exports = { + startClientInBackground, + stopClient, +}; diff --git a/cli/src/lib/context-status.ts b/cli/src/lib/context-status.ts new file mode 100644 index 00000000..29df308d --- /dev/null +++ b/cli/src/lib/context-status.ts @@ -0,0 +1,164 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const { describeProject } = require('./project-type'); +const { isDockerRunning } = require('./docker'); +const { isNginxContainerRunning } = require('./nginx'); +const { + DEV_PORTS, + readEnvPort, + isPortListening, +} = require('./ports'); +const { + loadServerSiteUrl, + loadWebAdminUrl, + loadAdminConsoleUrl, + isHttpResponding, + checkHealth, + getHealthUrl, +} = require('./http'); + +const SERVICE_ORDER = ['sqlite', 'mysql', 'server', 'docker', 'nginx', 'web']; + +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 resolveDbType(projectRoot) { + try { + const { resolveDatabaseProfile } = require('../core/services/database/profile'); + const profile = await resolveDatabaseProfile(projectRoot); + return profile.type; + } catch { + const env = parseEnvFile(projectRoot); + if (String(env.DB_TYPE || '').toLowerCase() === 'sqlite') return 'sqlite'; + try { + const configPath = path.join(projectRoot, '.reactpress/config.json'); + if (fs.existsSync(configPath)) { + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + if (config.database && config.database.mode === 'embedded-sqlite') return 'sqlite'; + } + } catch { + // ignore + } + return 'mysql'; + } +} + +async function probeMysql(projectRoot) { + const port = readEnvPort(projectRoot, 'DB_PORT', DEV_PORTS.MYSQL); + if (isPortListening(port)) return true; + 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 || DEV_PORTS.MYSQL), + 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 probeSqlite(projectRoot) { + const { probeSqliteDatabase } = require('../core/services/database/sqlite'); + const result = await probeSqliteDatabase(projectRoot); + return result.ok; +} + +async function probeServer(projectRoot) { + const port = readEnvPort(projectRoot, 'SERVER_PORT', DEV_PORTS.API); + if (isPortListening(port)) return true; + const [httpOk, health] = await Promise.all([ + isHttpResponding(loadServerSiteUrl(projectRoot), 1500), + checkHealth(getHealthUrl(projectRoot)), + ]); + return httpOk || health.ok; +} + +async function probeWeb(projectRoot) { + if (process.env.REACTPRESS_LOCAL_MODE === '1' || process.env.REACTPRESS_SKIP_NGINX === '1') { + return isHttpResponding(loadAdminConsoleUrl(projectRoot), 1500); + } + const port = readEnvPort(projectRoot, 'WEB_ADMIN_PORT', DEV_PORTS.ADMIN_WEB); + if (isPortListening(port)) return true; + return isHttpResponding(loadWebAdminUrl(projectRoot), 1500); +} + +function resolveServiceChecks(dbType) { + const dbId = dbType === 'sqlite' ? 'sqlite' : 'mysql'; + if (dbType === 'sqlite') { + return [dbId, 'server', 'web']; + } + return [dbId, 'server', 'web']; +} + +async function probeService(projectRoot, id) { + switch (id) { + case 'sqlite': + return probeSqlite(projectRoot); + case 'mysql': + return probeMysql(projectRoot); + case 'server': + return probeServer(projectRoot); + case 'docker': + return Promise.resolve(isDockerRunning()); + case 'nginx': + return Promise.resolve(isNginxContainerRunning()); + case 'web': + return probeWeb(projectRoot); + default: + return false; + } +} + +async function emitProgress(onProgress, payload) { + if (!onProgress) return; + onProgress(payload); + await new Promise((resolve) => setImmediate(resolve)); +} + +async function fetchContextStatus(projectRoot, { onProgress } = {}) { + const project = describeProject(projectRoot); + await emitProgress(onProgress, { phase: 'start', id: '__config' }); + const dbType = await resolveDbType(projectRoot); + const ids = resolveServiceChecks(dbType); + await emitProgress(onProgress, { phase: 'ready', total: ids.length + 1 }); + await emitProgress(onProgress, { phase: 'done', id: '__config' }); + const components = await Promise.all( + ids.map(async (id) => { + await emitProgress(onProgress, { phase: 'start', id }); + const ok = await probeService(projectRoot, id); + await emitProgress(onProgress, { phase: 'done', id, ok }); + return { id, ok }; + }), + ); + return { components, project, dbType }; +} + +module.exports = { + fetchContextStatus, + resolveServiceChecks, + resolveDbType, + parseEnvFile, + probeService, + SERVICE_ORDER, +}; diff --git a/cli/src/lib/database-mode.ts b/cli/src/lib/database-mode.ts new file mode 100644 index 00000000..d84999cf --- /dev/null +++ b/cli/src/lib/database-mode.ts @@ -0,0 +1,41 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const { ensureOriginalCwd } = require('./root'); + +function isDesktopLocalMode() { + return process.env.REACTPRESS_DESKTOP_LOCAL === '1'; +} + +function readSqliteModeFromProject(projectRoot) { + try { + const configPath = path.join(projectRoot, '.reactpress/config.json'); + if (fs.existsSync(configPath)) { + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + if (config.database?.mode === 'embedded-sqlite') return true; + } + } catch { + // ignore + } + try { + const envPath = path.join(projectRoot, '.env'); + if (fs.existsSync(envPath)) { + const content = fs.readFileSync(envPath, 'utf8'); + if (/^DB_TYPE=sqlite/m.test(content)) return true; + } + } catch { + // ignore + } + return false; +} + +function isLocalSqliteMode(projectRoot = ensureOriginalCwd()) { + if (process.env.REACTPRESS_LOCAL_MODE === '1' || isDesktopLocalMode()) return true; + return readSqliteModeFromProject(projectRoot); +} + +module.exports = { + isDesktopLocalMode, + isLocalSqliteMode, + readSqliteModeFromProject, +}; diff --git a/cli/src/lib/db-backup.ts b/cli/src/lib/db-backup.ts new file mode 100644 index 00000000..89433e2a --- /dev/null +++ b/cli/src/lib/db-backup.ts @@ -0,0 +1,68 @@ +// @ts-nocheck +const fs = require('fs'); +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'); + const out = {}; + try { + const content = fs.readFileSync(envPath, 'utf8'); + for (const line of content.split('\n')) { + const m = line.match(/^([A-Z_]+)=(.*)$/); + if (m) out[m[1]] = m[2].trim().replace(/^['"]|['"]$/g, ''); + } + } catch { + // ignore + } + return out; +} + +async function runDbBackup(projectRoot, outputPath) { + const env = parseEnv(projectRoot); + const host = env.DB_HOST || '127.0.0.1'; + const port = env.DB_PORT || '3306'; + const user = env.DB_USER || 'root'; + const password = env.DB_PASSWD || env.DB_PASSWORD || 'root'; + const database = env.DB_DATABASE || 'reactpress'; + const out = + outputPath || + path.join(projectRoot, `reactpress-backup-${new Date().toISOString().replace(/[:.]/g, '-')}.sql`); + + const cmd = `mysqldump -h ${host} -P ${port} -u ${user} -p${password} ${database}`; + console.log(chalk.cyan('[reactpress]'), t('db.backup.to', { path: out })); + try { + const dump = execSync(cmd, { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 }); + fs.writeFileSync(out, dump, 'utf8'); + 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; + } +} + +module.exports = { runDbBackup }; diff --git a/cli/src/lib/dev-banner.ts b/cli/src/lib/dev-banner.ts new file mode 100644 index 00000000..3c2f24bd --- /dev/null +++ b/cli/src/lib/dev-banner.ts @@ -0,0 +1,159 @@ +// @ts-nocheck +const { + brand, + icon, + ok, + divider, + padRight, + terminalWidth, + gradientText, + palette, + pulseBar, + statusLights, +} = require('../ui/theme'); +const { + loadClientSiteUrl, + loadWebAdminUrl, + loadServerSiteUrl, + getApiPrefix, + getHealthUrl, +} = require('./http'); +const { hasWeb } = require('./project-type'); +const { nginxEntryUrl } = require('./nginx'); +const { t } = require('./i18n'); + +function normalizeUrl(value, fallback) { + const raw = String(value || fallback || '').trim(); + return raw.replace(/\/$/, ''); +} + +function getDevUrls(projectRoot) { + const client = normalizeUrl(loadClientSiteUrl(projectRoot), 'http://localhost:3001'); + const server = normalizeUrl(loadServerSiteUrl(projectRoot), 'http://localhost:3002'); + const prefix = normalizeUrl(getApiPrefix(projectRoot), '/api') || '/api'; + const admin = hasWeb(projectRoot) + ? normalizeUrl(loadWebAdminUrl(projectRoot), 'http://localhost:3000') + : `${client}/admin`; + return { + site: client, + admin, + api: `${server}${prefix}`, + swagger: `${server}${prefix}`, + health: getHealthUrl(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, + webOnly = false, + desktop = false, + localWeb = false, + nginx = false, + hasThemeSite = false, + dbOk = true, + adminApiOrigin = null, + clientApiOrigin = null, + localApiUrl = null, + dbType = null, + } = {} +) { + const urls = getDevUrls(projectRoot); + const w = Math.min(terminalWidth() - 4, 56); + const readyKey = apiOnly + ? 'devBanner.readyApi' + : desktop + ? 'devBanner.readyDesktop' + : localWeb + ? 'devBanner.readyLocalWeb' + : webOnly + ? 'devBanner.readyWeb' + : 'devBanner.ready'; + + const useLocalDesktopApi = Boolean((desktop || localWeb) && localApiUrl); + const lights = useLocalDesktopApi || dbOk ? 'online' : 'degraded'; + const readyGradient = + useLocalDesktopApi || dbOk ? [palette.green, palette.accent] : [palette.amber, palette.muted]; + + console.log(''); + console.log( + ` ${useLocalDesktopApi || dbOk ? icon.ok : icon.warn} ${gradientText(t(readyKey), readyGradient, { bold: true })} ${statusLights(lights)}` + ); + console.log(` ${brand.primary('╔' + '═'.repeat(w) + '╗')}`); + + if (useLocalDesktopApi) { + const dbLabel = + dbType === 'sqlite' ? t('devBanner.sqliteEmbedded') : t('devBanner.mysqlDocker'); + console.log(urlLine(t('devBanner.database'), dbLabel, { underline: false })); + console.log(urlLine(t('devBanner.api'), localApiUrl)); + console.log(urlLine(t('devBanner.admin'), urls.admin)); + if (hasThemeSite) { + console.log(urlLine(t('devBanner.site'), urls.site)); + } + const healthUrl = String(localApiUrl || '') + .replace(/\/api\/?$/, '') + '/api/health'; + console.log(urlLine(t('devBanner.health'), healthUrl, { underline: false })); + console.log( + ` ${brand.muted(' ')}${brand.dim(t(localWeb ? 'devBanner.localWebHint' : 'devBanner.desktopLocalHint'))}` + ); + } else if (nginx) { + const entry = nginxEntryUrl(projectRoot); + if (!apiOnly && (hasThemeSite || !webOnly)) { + console.log(urlLine(t('devBanner.site'), entry)); + } + if (!apiOnly && hasWeb(projectRoot)) { + console.log(urlLine(t('devBanner.admin'), `${entry}/admin/`)); + } + console.log(urlLine(t('devBanner.api'), `${entry}/api`, { underline: false })); + if (clientApiOrigin) { + console.log( + ` ${brand.muted(' ')}${brand.dim(t('devBanner.nginxRemoteHint', { url: clientApiOrigin }))}` + ); + } else { + console.log(` ${brand.muted(' ')}${brand.dim(t('devBanner.nginxHint'))}`); + } + if (adminApiOrigin) { + console.log( + ` ${brand.muted(' ')}${brand.dim(t('devBanner.adminRemoteHint', { url: adminApiOrigin }))}` + ); + } + } else { + if (!apiOnly) { + if (!webOnly) { + 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)} ${ + useLocalDesktopApi + ? brand.success(t('devBanner.localModeGo')) + : dbOk + ? brand.success(t('devBanner.allSystemsGo')) + : brand.warn(t('devBanner.dbDegraded')) + }` + ); + } + + console.log(` ${brand.primary('╚' + '═'.repeat(w) + '╝')}`); + console.log( + ` ${brand.dim(t('devBanner.hint'))} ${brand.muted('·')} ${brand.dim(t('devBanner.shortcuts'))}` + ); + console.log(''); +} + +module.exports = { getDevUrls, printDevReadyBanner }; diff --git a/cli/src/lib/dev-child-io.ts b/cli/src/lib/dev-child-io.ts new file mode 100644 index 00000000..ffa2c983 --- /dev/null +++ b/cli/src/lib/dev-child-io.ts @@ -0,0 +1,83 @@ +// @ts-nocheck +const { spawn } = require('child_process'); + +const DEV_OUTPUT_NOISE = [ + /^>/, + /^◇ injected env/, + /^◈ /, + /^warn\s+- Invalid next\.config/, + /^Browserslist:/, + /^event - /, + /^wait - /, + /^Warning: \[antd/, + /^Warning: Route file/, + /^If this file is not intended/, + /^Current configuration:/, + /^ \d\. Rename/, + /^ routeFileIgnore/, + /^See more info here/, + /^\s+- The root value/, + /^\s+- The value at/, + /^\(node:\d+\) MaxListenersExceededWarning/, + /^\(Use `node --trace-warnings/, + /^ready - started server/, + /^vite:react-swc\]/, + /^\s*VITE\+/, + /^\s*➜\s+Network:/, + /^\s*➜\s+Local:/, + /^\s*➜\s+press h \+/, +]; + +function isDevOutputQuiet() { + return process.env.REACTPRESS_DEV_VERBOSE !== '1'; +} + +function isNoiseLine(line) { + const trimmed = line.trim(); + if (!trimmed) return true; + return DEV_OUTPUT_NOISE.some((re) => re.test(trimmed)); +} + +function pipeFiltered(stream, target) { + let buffer = ''; + stream.on('data', (chunk) => { + buffer += chunk.toString(); + let idx; + while ((idx = buffer.indexOf('\n')) >= 0) { + const line = buffer.slice(0, idx); + buffer = buffer.slice(idx + 1); + if (!isNoiseLine(line)) { + target.write(`${line}\n`); + } + } + }); + stream.on('end', () => { + if (buffer.trim() && !isNoiseLine(buffer)) { + target.write(`${buffer}\n`); + } + }); +} + +/** + * Spawn with filtered stdout/stderr unless REACTPRESS_DEV_VERBOSE=1. + */ +function spawnDevChild(command, args, options = {}) { + if (!isDevOutputQuiet()) { + return spawn(command, args, { ...options, stdio: options.stdio ?? 'inherit' }); + } + + const child = spawn(command, args, { + ...options, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + if (child.stdout) pipeFiltered(child.stdout, process.stdout); + if (child.stderr) pipeFiltered(child.stderr, process.stderr); + + return child; +} + +module.exports = { + isDevOutputQuiet, + spawnDevChild, +}; diff --git a/cli/src/lib/dev-log.ts b/cli/src/lib/dev-log.ts new file mode 100644 index 00000000..c811d22d --- /dev/null +++ b/cli/src/lib/dev-log.ts @@ -0,0 +1,68 @@ +// @ts-nocheck +const { t } = require('./i18n'); + +let startedAt = 0; +/** @type {Map} */ +const marks = new Map(); + +function startDevTimer() { + startedAt = Date.now(); + marks.clear(); + marks.set('start', 0); +} + +function markDevPhase(name) { + if (!startedAt) startDevTimer(); + marks.set(name, Date.now() - startedAt); +} + +function isDevVerbose() { + return process.env.REACTPRESS_DEV_VERBOSE === '1'; +} + +function formatDuration(ms) { + if (ms < 1000) return `${ms}ms`; + return `${(ms / 1000).toFixed(1)}s`; +} + +function logDevSection(messageKey, vars = {}) { + console.log(`\n${t(messageKey, vars)}`); +} + +function logDevStatus(messageKey, vars = {}) { + console.log(` ${t(messageKey, vars)}`); +} + +function logDevLine(messageKey, vars = {}) { + const msg = t(messageKey, vars); + console.log(msg.startsWith('[reactpress]') ? msg : `[reactpress] ${msg}`); +} + +function logDevDetail(messageKey, vars = {}) { + if (isDevVerbose()) logDevStatus(messageKey, vars); +} + +function logDevTimingSummary(extra = {}) { + markDevPhase('ready'); + const readyMs = marks.get('ready') ?? Date.now() - startedAt; + const infraMs = marks.has('infra') ? marks.get('infra') - (marks.get('start') || 0) : null; + const servicesMs = marks.has('services') ? marks.get('services') - (marks.get('infra') || 0) : null; + + const parts = [`${(readyMs / 1000).toFixed(1)}s`]; + if (infraMs != null) parts.push(`${t('dev.timingInfra')} ${formatDuration(infraMs)}`); + if (servicesMs != null) parts.push(`${t('dev.timingServices')} ${formatDuration(servicesMs)}`); + if (extra.apiReused) parts.push(t('dev.timingApiReused')); + + logDevStatus('dev.timingReady', { summary: parts.join(' · ') }); +} + +module.exports = { + startDevTimer, + markDevPhase, + isDevVerbose, + logDevSection, + logDevStatus, + logDevLine, + logDevDetail, + logDevTimingSummary, +}; diff --git a/cli/src/lib/dev-session.ts b/cli/src/lib/dev-session.ts new file mode 100644 index 00000000..b6f60b8c --- /dev/null +++ b/cli/src/lib/dev-session.ts @@ -0,0 +1,120 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); + +function lockFilePath(projectRoot) { + const { devSessionSuffix } = require('./ports'); + return path.join(projectRoot, '.reactpress', `dev-session${devSessionSuffix()}.json`); +} + +function isPidAlive(pid) { + const n = parseInt(pid, 10); + if (!Number.isFinite(n) || n <= 0) return false; + try { + process.kill(n, 0); + return true; + } catch { + return false; + } +} + +function readDevSession(projectRoot) { + try { + return JSON.parse(fs.readFileSync(lockFilePath(projectRoot), 'utf8')); + } catch { + return null; + } +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Ensure a single `reactpress dev` owner per project directory. + * Stops a stale lock holder from a crashed prior run when its PID is gone, + * or signals a still-running prior session before taking over ports. + */ +async function acquireDevSession(projectRoot) { + const resolvedRoot = path.resolve(projectRoot); + const lockPath = lockFilePath(resolvedRoot); + const existing = readDevSession(resolvedRoot); + + if (existing?.pid && existing.pid !== process.pid) { + if (isPidAlive(existing.pid)) { + console.warn( + `[reactpress] Replacing dev session pid ${existing.pid} (started ${existing.startedAt || 'unknown'})`, + ); + try { + process.kill(existing.pid, 'SIGTERM'); + } catch { + // prior session may have exited during signal + } + await sleep(400); + if (isPidAlive(existing.pid)) { + try { + process.kill(existing.pid, 'SIGKILL'); + } catch { + // ignore + } + await sleep(200); + } + // Do not run `docker compose down` here — DB/nginx containers must survive dev restarts. + } + } + + const { + releaseStaleDevStackPorts, + resolveDevStackPorts, + applyDevStackPortsToEnv, + readInstanceIndex, + } = require('./ports'); + const stack = resolveDevStackPorts(resolvedRoot); + applyDevStackPortsToEnv(stack); + const instance = stack.admin !== 3000 || stack.api !== 3002 ? readInstanceIndex() : 0; + if (instance > 0) { + console.log( + `[reactpress] Dev instance ${instance} — admin :${stack.admin}, api :${stack.api}, visitor :${stack.visitor}, preview :${stack.preview}`, + ); + } + await releaseStaleDevStackPorts(resolvedRoot); + + fs.mkdirSync(path.dirname(lockPath), { recursive: true }); + fs.writeFileSync( + lockPath, + `${JSON.stringify( + { + pid: process.pid, + ppid: process.ppid, + startedAt: new Date().toISOString(), + projectRoot: resolvedRoot, + }, + null, + 2, + )}\n`, + ); +} + +function releaseDevSession(projectRoot) { + try { + const existing = readDevSession(projectRoot); + if (existing?.pid === process.pid) { + fs.unlinkSync(lockFilePath(projectRoot)); + } + } catch { + // ignore + } +} + +function isDevSessionOwner(projectRoot) { + const existing = readDevSession(projectRoot); + return !existing?.pid || existing.pid === process.pid; +} + +module.exports = { + acquireDevSession, + releaseDevSession, + readDevSession, + isDevSessionOwner, + isPidAlive, +}; diff --git a/cli/src/lib/dev.ts b/cli/src/lib/dev.ts new file mode 100644 index 00000000..177abcd1 --- /dev/null +++ b/cli/src/lib/dev.ts @@ -0,0 +1,1112 @@ +// @ts-nocheck +const { spawn, spawnSync } = require('child_process'); +const { spawnDevChild } = require('./dev-child-io'); +const fs = require('fs'); +const path = require('path'); +const ora = require('ora'); +const { runBuild } = require('./build'); +const { ensureProjectEnvironment } = require('./bootstrap'); +const { + loadWebAdminUrl, + loadClientSiteUrl, + loadServerSiteUrl, + getHealthUrl, + checkHealth, + waitForHttp, +} = require('./http'); +const { printDevReadyBanner } = require('./dev-banner'); +const { startDevNginx, stopDevNginx, nginxEntryUrl } = require('./nginx'); +const { ensureOriginalCwd, isMonorepoCheckout } = require('./root'); +const { detectProjectType, hasWeb, hasToolkit } = require('./project-type'); +const { + hasResolvableActiveTheme, + hasThemePackages, + readActiveThemeManifest, + resolveThemeDirectory, +} = require('./theme-runtime'); +const { shouldBuildToolkit } = require('./toolkit-build'); +const { buildLocalPlugins } = require('./plugin-build'); +const { startThemeSiteWithWatch, stopThemeSite } = require('./theme-dev'); +const { scheduleBackgroundThemeBuilds } = require('./theme-prod'); +const { + shouldBlockOnThemeWarmup, + warmupThemeDevRoutes, + warmupThemeDevRoutesInBackground, +} = require('./theme-warmup'); +const { DEV_PORTS, ensureApiPortFree, ensurePortFree, readEnvPort, isPortListening } = + require('./ports'); +const { ensureDevDatabase, probeMysqlHost } = require('./docker'); +const { acquireDevSession, releaseDevSession } = require('./dev-session'); +const { checkNodeVersion, checkDocker } = require('./doctor'); +const { + startDevTimer, + markDevPhase, + isDevVerbose, + logDevLine, + logDevDetail, + logDevStatus, + logDevTimingSummary, +} = require('./dev-log'); +const { t } = require('./i18n'); +const { isDesktopLocalMode, isLocalSqliteMode } = require('./database-mode'); +const { + resolveRemoteThemeApiBase, + readDevClientApiOrigin, + normalizeRemoteOrigin, +} = require('./remote-dev'); + +const CLIENT_READY_TIMEOUT_MS = 120_000; +const API_READY_TIMEOUT_MS = 180_000; +const DEV_POLL_MS = 250; +const DEV_POLL_FAST_MS = 150; + +function shouldWaitForThemeInForeground() { + if (process.env.REACTPRESS_DESKTOP_LOCAL === '1') return true; + return process.env.REACTPRESS_DEV_WAIT_THEME === '1'; +} + +function logDevPhase(step, total, messageKey, vars = {}) { + console.log(''); + console.log(`[reactpress] [${step}/${total}] ${t(messageKey, vars)}`); +} + +async function applyAutoLocalDevFallback(projectRoot, { needsLocalApi = true } = {}) { + if (!needsLocalApi) return false; + if (isLocalSqliteMode(projectRoot)) { + process.env.REACTPRESS_SKIP_NGINX = '1'; + return false; + } + if (process.env.REACTPRESS_FORCE_MYSQL === '1') return false; + + const docker = checkDocker(); + const mysqlOk = docker.ok ? await probeMysqlHost(projectRoot) : false; + if (docker.ok && mysqlOk) return false; + + process.env.REACTPRESS_LOCAL_MODE = '1'; + process.env.REACTPRESS_SKIP_NGINX = '1'; + console.log(''); + console.log(`[reactpress] ${t(docker.ok ? 'dev.autoLocalNoMysql' : 'dev.autoLocalNoDocker')}`); + return true; +} + +function desktopPhaseKey(defaultKey) { + if (isLocalSqliteMode()) { + const localMap = { + 'dev.phasePrerequisites': 'dev.phasePrerequisitesDesktop', + 'dev.phaseInfra': 'dev.phaseInfraDesktop', + 'dev.phaseServices': 'dev.phaseServicesLocalWeb', + }; + if (localMap[defaultKey]) return localMap[defaultKey]; + } + if (!isDesktopLocalMode()) return defaultKey; + const map = { + 'dev.phasePrerequisites': 'dev.phasePrerequisitesDesktop', + 'dev.phaseInfra': 'dev.phaseInfraDesktop', + 'dev.phaseServices': 'dev.phaseServicesDesktop', + }; + return map[defaultKey] || defaultKey; +} + +function formatDevFailureHint() { + return [ + t('dev.nextSteps'), + t('dev.nextDoctor'), + t('dev.nextDocker'), + t('dev.nextEnv'), + ].join('\n'); +} + +let apiChild; +let webChild; +let desktopChild; +let shuttingDown = false; +let nginxEnabled = false; +/** When false, admin/API child exit during startup must not tear down the stack. */ +let devServicesReady = false; + +function shutdown(signal = 'SIGINT') { + if (shuttingDown) return; + shuttingDown = true; + stopThemeSite(); + if (nginxEnabled) stopDevNginx(ensureOriginalCwd()); + const stopEmbeddedApi = + signal === 'SIGINT' || devServicesReady || !isDesktopLocalMode(); + if (stopEmbeddedApi) { + try { + const { stopLocalServer } = require(path.join(ensureOriginalCwd(), 'desktop/out/main/local-server.js')); + stopLocalServer(); + } catch { + // desktop local API not running + } + } + if (desktopChild && !desktopChild.killed) desktopChild.kill(signal); + if (webChild && !webChild.killed) webChild.kill(signal); + if (apiChild && !apiChild.killed) apiChild.kill(signal); + try { + releaseDevSession(ensureOriginalCwd()); + } catch { + // ignore + } +} + +async function buildToolkit(projectRoot) { + if (!hasToolkit(projectRoot)) return; + if (!shouldBuildToolkit(projectRoot)) { + logDevDetail('dev.toolkitUpToDate'); + } else { + await runBuild('toolkit', projectRoot); + } + try { + buildLocalPlugins(projectRoot); + } catch (err) { + console.error(`[reactpress] ${err.message || err}`); + throw err; + } +} + +async function spawnApi(projectRoot) { + const { reused, port: apiPort } = await ensureApiPortFree(projectRoot, { allowReuse: true }); + if (reused) { + logDevStatus('dev.apiReusing', { port: apiPort }); + return { reused: true, port: apiPort }; + } + + const apiDevRunner = path.join(__dirname, 'api-dev-runner.js'); + logDevStatus('dev.startingApi'); + apiChild = spawn(process.execPath, [apiDevRunner], { + stdio: 'inherit', + cwd: projectRoot, + env: { + ...process.env, + REACTPRESS_ORIGINAL_CWD: projectRoot, + REACTPRESS_DEV_SESSION_PID: String(process.pid), + REACTPRESS_DEV_DB_READY: '1', + REACTPRESS_DEV_PORTS_READY: '1', + }, + }); + + apiChild.on('close', (code) => { + if (shuttingDown) { + process.exit(code ?? 0); + return; + } + if (webChild && !webChild.killed) webChild.kill('SIGINT'); + process.exit(code ?? 1); + }); + return { reused: false, port: apiPort }; +} + +async function waitForApiReady( + projectRoot, + { readyMessageKey = 'dev.apiReady', alreadyHealthy = false } = {}, +) { + const healthUrl = getHealthUrl(projectRoot); + const apiPort = readEnvPort(projectRoot, 'SERVER_PORT', DEV_PORTS.API); + + if (alreadyHealthy) { + const health = await checkHealth(healthUrl, 2000); + if (health.ok) { + logDevStatus(readyMessageKey); + return; + } + } + + const useSpinner = isDevVerbose() && process.stdout.isTTY; + const spinner = useSpinner + ? ora({ + text: t('dev.waitingApi', { url: healthUrl }), + color: 'magenta', + spinner: 'dots', + }).start() + : null; + if (!useSpinner) logDevStatus('dev.waitingApiQuiet'); + + const deadline = Date.now() + API_READY_TIMEOUT_MS; + let lastHint = ''; + + while (Date.now() < deadline) { + if (!isPortListening(apiPort)) { + lastHint = t('dev.waitingApiCompile', { port: apiPort }); + if (spinner) { + spinner.text = `${t('dev.waitingApi', { url: healthUrl })} — ${lastHint}`; + } + await new Promise((r) => setTimeout(r, DEV_POLL_MS)); + continue; + } + + const health = await checkHealth(healthUrl, 2500); + if (health.ok) { + if (spinner) spinner.succeed(t(readyMessageKey)); + else logDevStatus(readyMessageKey); + return; + } + + if (health.data?.database === 'down') { + lastHint = t('dev.healthDbDown'); + } else if (health.statusCode === 200 && health.data?.status === 'degraded') { + lastHint = t('dev.healthDegraded'); + } else if (health.statusCode === 0) { + lastHint = t('dev.waitingApiStarting'); + } else if (health.statusCode > 0) { + lastHint = `HTTP ${health.statusCode}`; + } + + if (spinner && lastHint) { + spinner.text = `${t('dev.waitingApi', { url: healthUrl })} — ${lastHint}`; + } + await new Promise((r) => setTimeout(r, DEV_POLL_FAST_MS)); + } + + if (spinner) spinner.fail(t('dev.apiTimeout', { seconds: API_READY_TIMEOUT_MS / 1000 })); + else console.error(t('dev.apiTimeout', { seconds: API_READY_TIMEOUT_MS / 1000 })); + shutdown('SIGINT'); + process.exit(1); +} + +function handlePrimaryDevChildClose(code, label = 'dev', projectRoot = ensureOriginalCwd()) { + if (!devServicesReady) { + console.warn( + `[reactpress] ${label} process exited during startup (code ${code ?? 'unknown'}) — waiting for services…`, + ); + return; + } + const adminPort = readEnvPort(projectRoot, 'WEB_ADMIN_PORT', DEV_PORTS.ADMIN_WEB); + if (isDesktopLocalMode() && label === 'Admin dev') { + return; + } + if (label === 'Admin dev' && isPortListening(adminPort)) { + return; + } + if (!shuttingDown) shutdown('SIGINT'); + process.exit(code ?? 0); +} + +async function spawnAdminWeb( + projectRoot, + { + behindNginx = false, + integratedStack = false, + adminApiOrigin = null, + waitForReady = true, + } = {}, +) { + const adminPort = readEnvPort(projectRoot, 'WEB_ADMIN_PORT', DEV_PORTS.ADMIN_WEB); + await ensurePortFree(adminPort, { label: 'admin' }); + + logDevDetail('dev.startingAdmin', { url: loadWebAdminUrl(projectRoot) }); + const adminEnv = { + ...process.env, + REACTPRESS_ORIGINAL_CWD: projectRoot, + WEB_ADMIN_PORT: String(adminPort), + }; + if (nginxEnabled && process.env.REACTPRESS_NGINX_ENTRY_URL) { + adminEnv.REACTPRESS_NGINX_ENTRY_URL = process.env.REACTPRESS_NGINX_ENTRY_URL; + } else { + adminEnv.REACTPRESS_SKIP_DEV_PORT_REDIRECT = '1'; + } + if (behindNginx) { + adminEnv.VITE_ADMIN_BASE = '/admin/'; + process.env.REACTPRESS_BEHIND_NGINX = '1'; + } + // Full stack (API + theme site): theme install/activate must hit Nest, not MSW. + if (integratedStack || adminApiOrigin) { + adminEnv.VITE_ENABLE_MOCK = 'false'; + adminEnv.VITE_AUTH_MODE = 'server'; + if (adminApiOrigin) { + // Vite proxies `/api` → `${target}/api/...`; target is host-only origin. + adminEnv.VITE_DEV_API_PROXY_TARGET = normalizeRemoteOrigin(adminApiOrigin) || adminApiOrigin; + } else if (isDesktopLocalMode() && process.env.REACTPRESS_DESKTOP_LOCAL_API) { + // Local SQLite API uses the same port as SERVER_PORT (default :3002). + adminEnv.VITE_DEV_API_PROXY_TARGET = process.env.REACTPRESS_DESKTOP_LOCAL_API.replace( + /\/api\/?$/, + '', + ); + } else { + adminEnv.VITE_DEV_API_PROXY_TARGET = loadServerSiteUrl(projectRoot).replace(/\/$/, ''); + } + } + + webChild = isDesktopLocalMode() + ? spawn(process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm', ['exec', 'vp', 'dev'], { + cwd: path.join(projectRoot, 'web'), + env: adminEnv, + stdio: 'inherit', + shell: process.platform === 'win32', + }) + : spawnDevChild('pnpm', ['run', '--dir', './web', 'dev'], { + shell: true, + cwd: projectRoot, + env: adminEnv, + }); + + webChild.on('close', (code) => { + handlePrimaryDevChildClose(code, 'Admin dev', projectRoot); + }); + + if (!waitForReady) return Promise.resolve(true); + + const readyUrl = loadWebAdminUrl(projectRoot); + return waitForHttp(readyUrl, CLIENT_READY_TIMEOUT_MS, DEV_POLL_MS).then((ready) => { + if (!ready) { + console.warn( + t('dev.adminSlow', { + seconds: CLIENT_READY_TIMEOUT_MS / 1000, + url: readyUrl, + }), + ); + } + return ready; + }); +} + +function assertDevPrerequisites() { + const node = checkNodeVersion(); + if (!node.ok) { + console.error(`[reactpress] ${node.message}`); + if (node.fix) console.error(` → ${node.fix}`); + console.error(formatDevFailureHint()); + process.exit(1); + } + if (!isLocalSqliteMode()) { + const docker = checkDocker(); + if (!docker.ok) { + console.error(`[reactpress] ${docker.message}`); + if (docker.fix) console.error(` → ${docker.fix}`); + console.error(formatDevFailureHint()); + process.exit(1); + } + } + logDevStatus( + isDesktopLocalMode() || process.env.REACTPRESS_LOCAL_MODE === '1' + ? 'dev.prerequisitesOkDesktop' + : 'dev.prerequisitesOk', + { version: process.version }, + ); +} + +async function prepareDevInfrastructure(projectRoot, { needsLocalApi = true } = {}) { + await acquireDevSession(projectRoot); + if (isLocalSqliteMode(projectRoot)) { + const { ensureLocalSite } = require('../core/services/local-site'); + const { ensureSqliteDatabase } = require('../core/services/database/sqlite'); + const { syncEnvFromConfig, loadConfig } = require('../core/services/config'); + const { getMonorepoRoot } = require('./root'); + const port = readEnvPort(projectRoot, 'SERVER_PORT', DEV_PORTS.API); + if (process.env.REACTPRESS_LOCAL_MODE === '1') { + ensureLocalSite(projectRoot, port, { monorepoRoot: getMonorepoRoot() }); + } else { + const config = await loadConfig(projectRoot); + await syncEnvFromConfig(projectRoot, config); + } + const sqliteResult = await ensureSqliteDatabase(projectRoot); + if (!sqliteResult.ok) { + throw new Error(sqliteResult.message || 'SQLite 数据库未就绪'); + } + nginxEnabled = false; + delete process.env.REACTPRESS_NGINX_ENTRY_URL; + process.env.REACTPRESS_SKIP_DEV_PORT_REDIRECT = '1'; + return; + } + const planNginx = process.env.REACTPRESS_SKIP_NGINX !== '1'; + const clientApiOrigin = readDevClientApiOrigin(projectRoot); + + const [, nginxResult] = await Promise.all([ + needsLocalApi ? ensureDevDatabase(projectRoot, { quiet: true }) : Promise.resolve(true), + planNginx ? startDevNginx(projectRoot) : Promise.resolve(false), + ]); + + nginxEnabled = nginxResult; + if (nginxEnabled) { + process.env.REACTPRESS_NGINX_ENTRY_URL = nginxEntryUrl(projectRoot); + delete process.env.REACTPRESS_SKIP_DEV_PORT_REDIRECT; + if (clientApiOrigin) { + logDevStatus('dev.nginxReadyRemote', { + url: nginxEntryUrl(projectRoot), + api: clientApiOrigin, + }); + } else { + logDevStatus('dev.nginxReady', { url: nginxEntryUrl(projectRoot) }); + } + } else { + delete process.env.REACTPRESS_NGINX_ENTRY_URL; + process.env.REACTPRESS_SKIP_DEV_PORT_REDIRECT = '1'; + } +} + +async function startDevStack( + projectRoot, + { + webOnly = false, + themeOnly = false, + desktopMode = false, + localWebMode = false, + infraDone = false, + apiOrigins = { admin: null, client: null, needsLocalApi: true }, + } = {}, +) { + const { admin: adminApiOrigin, client: clientApiOrigin, needsLocalApi } = apiOrigins; + if (!infraDone) { + logDevPhase(1, 3, desktopPhaseKey('dev.phasePrerequisites')); + assertDevPrerequisites(); + logDevPhase(2, 3, desktopPhaseKey('dev.phaseInfra')); + try { + await prepareDevInfrastructure(projectRoot, { needsLocalApi }); + } catch (err) { + console.error(t('dev.dbEnsureFailed', { message: err.message || err })); + console.error(formatDevFailureHint()); + process.exit(1); + } + } else if ( + needsLocalApi && + !isLocalSqliteMode(projectRoot) && + !(await probeMysqlHost(projectRoot)) + ) { + console.error( + t('dev.dbEnsureFailed', { + message: t('docker.devStartBlocked', { + port: readEnvPort(projectRoot, 'DB_PORT', DEV_PORTS.MYSQL), + }), + }), + ); + console.error(formatDevFailureHint()); + process.exit(1); + } + + const includeAdmin = webOnly && hasWeb(projectRoot); + // Always run theme watchers when packages exist — admin activate/preview writes manifests + // and relies on fs.watch even if the current active theme is not yet resolvable. + const includeThemeSite = + themeOnly || + (desktopMode && hasThemePackages(projectRoot)) || + (!webOnly && hasThemePackages(projectRoot)); + if (!webOnly && !themeOnly && includeThemeSite && !hasResolvableActiveTheme(projectRoot)) { + const { activeTheme } = readActiveThemeManifest(projectRoot); + console.warn( + `[reactpress] ${t('themeDev.notFound', { id: activeTheme })} — ${t('dev.themeSiteSkipped')}`, + ); + } + if (includeThemeSite) { + const { validateBundledThemes, validateCatalogThemes } = require('./theme-registry'); + const bundled = validateBundledThemes(projectRoot); + if (bundled.missing.length) { + console.warn( + `[reactpress] themes/package.json lists bundled theme(s) without theme.json: ${bundled.missing.join(', ')}`, + ); + } + const catalog = validateCatalogThemes(projectRoot); + if (catalog.missing.length) { + console.warn( + `[reactpress] themes/package.json lists catalog dir(s) without reactpress.theme in package.json: ${catalog.missing.join(', ')}`, + ); + } + } + const planNginx = process.env.REACTPRESS_SKIP_NGINX !== '1'; + const includeWebInStack = hasWeb(projectRoot) && !webOnly && !themeOnly; + + if (infraDone) { + logDevPhase( + 3, + 3, + localWebMode ? 'dev.phaseServicesLocalWeb' : desktopPhaseKey('dev.phaseServices'), + ); + } + + markDevPhase('services'); + const readinessWaits = []; + let apiSpawn = null; + + const prewarmPreviewBuilds = + includeThemeSite && + isDesktopLocalMode() && + process.env.REACTPRESS_SKIP_PREVIEW_BUILD !== '1'; + if (prewarmPreviewBuilds) { + const { warmupAllPreviewThemeBuilds } = require('./theme-prod'); + logDevStatus('dev.previewPrewarmStarting'); + readinessWaits.push(warmupAllPreviewThemeBuilds(projectRoot)); + } + + if (adminApiOrigin || clientApiOrigin) { + if (adminApiOrigin) { + logDevStatus('dev.adminApiRemote', { url: adminApiOrigin }); + } + if (clientApiOrigin) { + logDevStatus('dev.clientApiRemote', { url: clientApiOrigin }); + } + } + if (needsLocalApi) { + logDevStatus('dev.phaseApi'); + apiSpawn = await spawnApi(projectRoot); + const readyMessageKey = webOnly || hasWeb(projectRoot) ? 'dev.apiReadyAdmin' : 'dev.apiReady'; + readinessWaits.push( + waitForApiReady(projectRoot, { + readyMessageKey, + alreadyHealthy: Boolean(apiSpawn?.reused), + }), + ); + } + + if (!includeAdmin && !includeThemeSite && !includeWebInStack) { + await Promise.all(readinessWaits); + printDevReadyBanner(projectRoot, { apiOnly: true, nginx: nginxEnabled }); + console.log(t('dev.standaloneHint')); + return; + } + + const waitThemeInForeground = includeThemeSite && shouldWaitForThemeInForeground(); + let themeSiteStarted = false; + + if (includeWebInStack) { + logDevDetail('dev.phaseAdmin'); + if (planNginx) process.env.REACTPRESS_BEHIND_NGINX = '1'; + logDevDetail('dev.startingAdmin', { url: loadWebAdminUrl(projectRoot) }); + spawnAdminWeb(projectRoot, { + behindNginx: planNginx, + integratedStack: true, + adminApiOrigin, + waitForReady: false, + }); + const adminBase = loadWebAdminUrl(projectRoot).replace(/\/$/, ''); + const adminProbe = planNginx ? `${adminBase}/admin/` : `${adminBase}/`; + readinessWaits.push(waitForHttp(adminProbe, 120_000, DEV_POLL_MS)); + } else if (includeAdmin) { + logDevDetail('dev.phaseAdmin'); + spawnAdminWeb(projectRoot, { + behindNginx: desktopMode || localWebMode ? false : planNginx, + integratedStack: desktopMode || localWebMode || isLocalSqliteMode(), + adminApiOrigin, + waitForReady: false, + }); + const adminBase = loadWebAdminUrl(projectRoot).replace(/\/$/, ''); + const adminProbe = planNginx ? `${adminBase}/admin/` : `${adminBase}/`; + readinessWaits.push(waitForHttp(adminProbe, 120_000, DEV_POLL_MS)); + } + + if (includeThemeSite) { + const { activeTheme } = readActiveThemeManifest(projectRoot); + logDevStatus('dev.themeStarting', { + id: activeTheme, + port: readEnvPort(projectRoot, 'CLIENT_PORT', DEV_PORTS.VISITOR), + }); + if (planNginx) process.env.REACTPRESS_BEHIND_NGINX = '1'; + if (clientApiOrigin) { + delete process.env.REACTPRESS_DEV_FORCE_LOCAL_THEME_API; + const themeApiBase = resolveRemoteThemeApiBase(clientApiOrigin); + process.env.REACTPRESS_THEME_API_URL = themeApiBase; + process.env.REACTPRESS_THEME_PUBLIC_API_URL = planNginx + ? `${nginxEntryUrl(projectRoot).replace(/\/$/, '')}/api` + : themeApiBase; + const themeDir = resolveThemeDirectory(projectRoot, activeTheme); + if (themeDir) { + const nextCache = path.join(themeDir, '.next'); + if (fs.existsSync(nextCache)) { + fs.rmSync(nextCache, { recursive: true, force: true }); + logDevDetail('dev.themeCacheClearedForRemote'); + } + } + } else { + process.env.REACTPRESS_DEV_FORCE_LOCAL_THEME_API = '1'; + } + const themeBoot = startThemeSiteWithWatch(projectRoot); + const themeWait = themeBoot.then(async (started) => { + themeSiteStarted = started; + if (!started) return false; + const clientUrl = loadClientSiteUrl(projectRoot); + const port = readEnvPort(projectRoot, 'CLIENT_PORT', DEV_PORTS.VISITOR); + const portOpen = await (async () => { + const deadline = Date.now() + 120_000; + while (Date.now() < deadline) { + if (isPortListening(port)) return true; + await new Promise((r) => setTimeout(r, DEV_POLL_MS)); + } + return false; + })(); + if (portOpen) { + if (isDesktopLocalMode()) { + const { warmupThemeHomepage } = require('./theme-warmup'); + await warmupThemeHomepage(projectRoot, clientUrl); + } else if (shouldBlockOnThemeWarmup()) { + await warmupThemeDevRoutes(projectRoot); + } else { + warmupThemeDevRoutesInBackground(projectRoot); + } + } + return portOpen; + }); + if (waitThemeInForeground) { + readinessWaits.push(themeWait); + } else { + themeWait.then((ready) => { + if (ready) logDevDetail('dev.themeReadyQuiet', { url: loadClientSiteUrl(projectRoot) }); + }).catch(() => {}); + } + } + + if (readinessWaits.length > 1) { + logDevStatus('dev.waitingProxies'); + } + await Promise.all(readinessWaits); + if (readinessWaits.length > 1) { + logDevStatus('dev.proxiesReady'); + } + + if (includeWebInStack && planNginx && nginxEnabled) { + const adminViaNginx = `${nginxEntryUrl(projectRoot).replace(/\/$/, '')}/admin/`; + waitForHttp(adminViaNginx, 15_000, DEV_POLL_MS).then((adminOk) => { + if (!adminOk) { + console.warn(t('dev.adminNginxSlow', { url: adminViaNginx })); + } + }); + } else if (planNginx && !nginxEnabled) { + startDevNginx(projectRoot).then((enabled) => { + nginxEnabled = enabled; + if (enabled) { + console.log(t('dev.nginxReady', { url: nginxEntryUrl(projectRoot) })); + } + }); + } + + let dbOk = true; + if (needsLocalApi) { + if (isDesktopLocalMode() || isLocalSqliteMode(projectRoot)) { + const { probeSqliteDatabase } = require('../core/services/database/sqlite'); + dbOk = (await probeSqliteDatabase(projectRoot)).ok; + } else { + dbOk = await probeMysqlHost(projectRoot); + } + } + if (needsLocalApi && !dbOk && !isDesktopLocalMode() && !isLocalSqliteMode(projectRoot)) { + console.warn(t('dev.mysqlUnreachable')); + } + + if (includeThemeSite && process.env.REACTPRESS_SKIP_PREVIEW_BUILD !== '1' && !prewarmPreviewBuilds) { + const { activeTheme } = readActiveThemeManifest(projectRoot); + scheduleBackgroundThemeBuilds(projectRoot, { excludeThemeId: activeTheme }); + } + + printDevReadyBanner(projectRoot, { + webOnly: (includeAdmin && !includeThemeSite) || localWebMode, + desktop: desktopMode && !localWebMode, + localWeb: localWebMode, + nginx: nginxEnabled, + hasThemeSite: includeThemeSite, + dbOk, + adminApiOrigin, + clientApiOrigin, + localApiUrl: isDesktopLocalMode() ? process.env.REACTPRESS_DESKTOP_LOCAL_API || null : null, + dbType: + isDesktopLocalMode() || isLocalSqliteMode(projectRoot) + ? 'sqlite' + : needsLocalApi + ? 'mysql' + : null, + }); + + logDevTimingSummary({ + apiReused: Boolean(apiSpawn?.reused), + }); + + devServicesReady = true; +} + +async function runDevStartup( + projectRoot, + { + webOnly = false, + themeOnly = false, + desktopMode = false, + localWebMode = false, + skipPrepareInfra = false, + apiOrigins = { admin: null, client: null, needsLocalApi: true }, + } = {}, +) { + startDevTimer(); + try { + const result = await ensureProjectEnvironment(projectRoot, { skipDatabase: true }); + if (result.message && isDevVerbose()) console.log(`[reactpress] ${result.message}`); + } catch (err) { + console.error(t('dev.envFailed'), err.message || err); + console.error(formatDevFailureHint()); + process.exit(1); + } + + await applyAutoLocalDevFallback(projectRoot, { needsLocalApi: apiOrigins.needsLocalApi }); + + logDevPhase(1, 3, desktopPhaseKey('dev.phasePrerequisites')); + assertDevPrerequisites(); + + logDevPhase(2, 3, desktopPhaseKey('dev.phaseInfra')); + try { + const infraTasks = [buildToolkit(projectRoot)]; + if (!skipPrepareInfra) { + infraTasks.unshift( + prepareDevInfrastructure(projectRoot, { needsLocalApi: apiOrigins.needsLocalApi }), + ); + } + await Promise.all(infraTasks); + markDevPhase('infra'); + } catch (err) { + console.error(t('dev.dbEnsureFailed', { message: err.message || err })); + console.error(formatDevFailureHint()); + process.exit(1); + } + + await startDevStack(projectRoot, { + webOnly, + themeOnly, + desktopMode, + localWebMode, + infraDone: true, + apiOrigins, + }); +} + +function loadDesktopBootstrap(projectRoot) { + return require(path.join(projectRoot, 'desktop/scripts/bootstrap-local-api.cjs')); +} + +async function startDesktopLocalApi(projectRoot, { forceRestart = false } = {}) { + const desktopDir = path.join(projectRoot, 'desktop'); + if (!fs.existsSync(path.join(desktopDir, 'package.json'))) { + console.error(`[reactpress] ${t('dev.desktopMissing')}`); + process.exit(1); + } + + const { + resolveDevStackPorts, + applyDevStackPortsToEnv, + resolveApiPortForBind, + } = require('./ports'); + let stack = resolveDevStackPorts(projectRoot); + applyDevStackPortsToEnv(stack); + + const boot = await loadDesktopBootstrap(projectRoot); + console.log(''); + logDevStatus('dev.desktopLocalApiStarting'); + + const resolved = await resolveApiPortForBind(projectRoot, { preferred: stack.api }); + if (resolved.shifted || resolved.port !== stack.api) { + stack = { ...stack, api: resolved.port }; + applyDevStackPortsToEnv(stack); + } + + const { siteRoot, localApiBase } = await boot.startDesktopLocalApi(projectRoot, { + forceRestart, + port: resolved.port, + }); + process.env.REACTPRESS_DESKTOP_LOCAL_API = localApiBase; + process.env.REACTPRESS_DESKTOP_SITE_ROOT = siteRoot; + process.env.REACTPRESS_THEME_API_URL = localApiBase; + process.env.REACTPRESS_THEME_PUBLIC_API_URL = localApiBase; + const localApiOrigin = localApiBase.replace(/\/api\/?$/, ''); + process.env.VITE_DEV_API_PROXY_TARGET = localApiOrigin; + logDevStatus('dev.desktopLocalApiReady', { url: localApiBase, db: t('dev.dbTypeSqlite') }); + return { siteRoot, localApiBase }; +} + +async function ensureDesktopLocalApiHealthy(projectRoot, { forceRestart = false } = {}) { + if (!isDesktopLocalMode()) return true; + const base = process.env.REACTPRESS_DESKTOP_LOCAL_API?.trim(); + if (!base) return false; + const healthUrl = `${base.replace(/\/api\/?$/, '')}/api/health`; + const health = await checkHealth(healthUrl, 2500); + if (health.ok && !forceRestart) return true; + console.warn('[reactpress] Local API unreachable — restarting embedded SQLite API…'); + await startDesktopLocalApi(projectRoot, { forceRestart: true }); + const retry = await checkHealth(healthUrl, 5000); + if (!retry.ok) { + console.warn(`[reactpress] Local API still unhealthy after restart (${healthUrl})`); + } + return retry.ok; +} + +function registerDevShutdownHandlers(projectRoot) { + process.on('SIGINT', () => shutdown('SIGINT')); + process.on('SIGTERM', () => { + // Stray SIGTERM during local web boot must not tear down the embedded API (Vite still starting). + if (!devServicesReady && isDesktopLocalMode()) return; + shutdown('SIGTERM'); + }); + process.on('exit', () => { + try { + releaseDevSession(projectRoot); + } catch { + // ignore + } + }); +} + +/** Block until the primary dev child exits (admin Vite, Electron shell, or Nest API). */ +function waitUntilDevChildExit(projectRoot = ensureOriginalCwd()) { + return new Promise((resolve) => { + const adminPort = readEnvPort(projectRoot, 'WEB_ADMIN_PORT', DEV_PORTS.ADMIN_WEB); + + const waitForShutdownSignal = () => { + const onSignal = () => resolve(0); + process.once('SIGINT', onSignal); + process.once('SIGTERM', onSignal); + }; + + const child = webChild || desktopChild || apiChild; + if (!child) { + waitForShutdownSignal(); + return; + } + + const finish = (code) => { + if (child === webChild && isPortListening(adminPort)) { + waitForShutdownSignal(); + return; + } + resolve(code ?? 0); + }; + + if (child.exitCode != null) { + finish(child.exitCode); + return; + } + if (child.killed) { + finish(1); + return; + } + child.once('close', (code) => finish(code)); + }); +} + +function canUseDesktopLocalStack(projectRoot) { + return ( + isMonorepoCheckout(projectRoot) && + fs.existsSync(path.join(projectRoot, 'desktop', 'package.json')) + ); +} + +async function spawnDesktopApp(projectRoot) { + const desktopDir = path.join(projectRoot, 'desktop'); + if (!fs.existsSync(path.join(desktopDir, 'package.json'))) { + console.error(`[reactpress] ${t('dev.desktopMissing')}`); + shutdown('SIGINT'); + process.exit(1); + } + + const adminUrl = loadWebAdminUrl(projectRoot).replace(/\/$/, ''); + logDevStatus('dev.desktopStarting', { url: adminUrl }); + + const boot = await loadDesktopBootstrap(projectRoot); + boot.ensureDesktopBuilt(projectRoot); + + desktopChild = spawnDevChild( + 'pnpm', + ['exec', 'cross-env', `VITE_DEV_SERVER_URL=${adminUrl}`, 'ELECTRON_IS_DEV=1', 'pnpm', 'run', 'dev:shell'], + { + shell: true, + cwd: desktopDir, + env: { + ...process.env, + REACTPRESS_ORIGINAL_CWD: projectRoot, + VITE_DEV_SERVER_URL: adminUrl, + ELECTRON_IS_DEV: '1', + }, + }, + ); + + desktopChild.on('close', (code) => { + handlePrimaryDevChildClose(code, 'Desktop shell'); + }); +} + +async function runLocalMonorepoDev(projectRoot = ensureOriginalCwd()) { + if (!hasWeb(projectRoot)) { + console.error(t('dev.noWeb')); + process.exit(1); + } + + process.env.REACTPRESS_SKIP_NGINX = '1'; + process.env.REACTPRESS_DESKTOP_LOCAL = '1'; + registerDevShutdownHandlers(projectRoot); + + console.log(''); + logDevLine('dev.localFullIntro'); + + await prepareDevInfrastructure(projectRoot, { needsLocalApi: false }); + await startDesktopLocalApi(projectRoot, { forceRestart: true }); + + await runDevStartup(projectRoot, { + skipPrepareInfra: true, + apiOrigins: { admin: null, client: null, needsLocalApi: false }, + }); + await ensureDesktopLocalApiHealthy(projectRoot); + await new Promise((resolve) => { + const interval = setInterval(() => { + void ensureDesktopLocalApiHealthy(projectRoot).catch(() => {}); + }, 20_000); + const onStop = (signal) => { + clearInterval(interval); + shutdown(signal); + resolve(0); + }; + process.once('SIGINT', () => onStop('SIGINT')); + process.once('SIGTERM', () => onStop('SIGTERM')); + }); + process.exit(0); +} + +async function runDesktopDev(projectRoot = ensureOriginalCwd()) { + if (!hasWeb(projectRoot)) { + console.error(t('dev.noWeb')); + process.exit(1); + } + + process.env.REACTPRESS_SKIP_NGINX = '1'; + process.env.REACTPRESS_DESKTOP_LOCAL = '1'; + registerDevShutdownHandlers(projectRoot); + + console.log(''); + logDevLine('dev.desktopIntro'); + + await prepareDevInfrastructure(projectRoot, { needsLocalApi: false }); + await startDesktopLocalApi(projectRoot, { forceRestart: true }); + + await runDevStartup(projectRoot, { + webOnly: true, + desktopMode: true, + skipPrepareInfra: true, + apiOrigins: { admin: null, client: null, needsLocalApi: false }, + }); + await spawnDesktopApp(projectRoot); + const exitCode = await waitUntilDevChildExit(projectRoot); + process.exit(exitCode); +} + +async function runLocalWebDev( + projectRoot = ensureOriginalCwd(), + { apiOrigins = { admin: null, client: null, needsLocalApi: true } } = {}, +) { + if (!hasWeb(projectRoot)) { + console.error(t('dev.noWeb')); + process.exit(1); + } + + registerDevShutdownHandlers(projectRoot); + + if (canUseDesktopLocalStack(projectRoot)) { + delete process.env.REACTPRESS_LOCAL_MODE; + process.env.REACTPRESS_SKIP_NGINX = '1'; + process.env.REACTPRESS_DESKTOP_LOCAL = '1'; + + console.log(''); + logDevLine('dev.localWebIntro'); + + await prepareDevInfrastructure(projectRoot, { needsLocalApi: false }); + await startDesktopLocalApi(projectRoot, { forceRestart: true }); + + await runDevStartup(projectRoot, { + webOnly: true, + desktopMode: true, + localWebMode: true, + skipPrepareInfra: true, + apiOrigins: { admin: null, client: null, needsLocalApi: false }, + }); + await ensureDesktopLocalApiHealthy(projectRoot); + await new Promise((resolve) => { + const interval = setInterval(() => { + void ensureDesktopLocalApiHealthy(projectRoot).catch(() => {}); + }, 20_000); + const onStop = (signal) => { + clearInterval(interval); + shutdown(signal); + resolve(0); + }; + process.once('SIGINT', () => onStop('SIGINT')); + process.once('SIGTERM', () => onStop('SIGTERM')); + }); + process.exit(0); + return; + } + + process.env.REACTPRESS_LOCAL_MODE = '1'; + process.env.REACTPRESS_SKIP_NGINX = '1'; + + console.log(''); + logDevLine('dev.localWebIntro'); + + await runDevStartup(projectRoot, { webOnly: true, apiOrigins }); +} + +async function runThemeDev( + projectRoot = ensureOriginalCwd(), + { apiOrigins = { admin: null, client: null, needsLocalApi: true } } = {}, +) { + if (!hasResolvableActiveTheme(projectRoot)) { + console.error(t('dev.themeSiteSkipped')); + process.exit(1); + } + + process.env.REACTPRESS_THEME_DEV_ONLY = '1'; + + process.on('SIGINT', () => shutdown('SIGINT')); + process.on('SIGTERM', () => shutdown('SIGTERM')); + process.on('exit', () => { + try { + releaseDevSession(projectRoot); + } catch { + // ignore + } + }); + + await runDevStartup(projectRoot, { themeOnly: true, apiOrigins }); +} + +async function runWebDev( + projectRoot = ensureOriginalCwd(), + { apiOrigins = { admin: null, client: null, needsLocalApi: true } } = {}, +) { + if (!hasWeb(projectRoot)) { + console.error(t('dev.noWeb')); + process.exit(1); + } + + process.on('SIGINT', () => shutdown('SIGINT')); + process.on('SIGTERM', () => shutdown('SIGTERM')); + process.on('exit', () => { + try { + releaseDevSession(projectRoot); + } catch { + // ignore + } + }); + + await runDevStartup(projectRoot, { webOnly: true, apiOrigins }); +} + +async function runDev( + projectRoot = ensureOriginalCwd(), + { apiOrigins = { admin: null, client: null, needsLocalApi: true } } = {}, +) { + process.on('SIGINT', () => shutdown('SIGINT')); + process.on('SIGTERM', () => shutdown('SIGTERM')); + process.on('exit', () => { + try { + releaseDevSession(projectRoot); + } catch { + // ignore + } + }); + + await runDevStartup(projectRoot, { apiOrigins }); +} + +module.exports = { + runDev, + runWebDev, + runLocalWebDev, + runLocalMonorepoDev, + runThemeDev, + runDesktopDev, + runDevStartup, + buildToolkit, + assertDevPrerequisites, + applyAutoLocalDevFallback, + prepareDevInfrastructure, + startDevStack, + detectProjectType, + nginxEntryUrl, +}; diff --git a/cli/src/lib/docker.ts b/cli/src/lib/docker.ts new file mode 100644 index 00000000..e2d07c42 --- /dev/null +++ b/cli/src/lib/docker.ts @@ -0,0 +1,434 @@ +// @ts-nocheck +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 } = require('./project-type'); +const { t } = require('./i18n'); + +function isDockerRunning() { + try { + execSync('docker info', { stdio: 'ignore' }); + return true; + } catch { + return false; + } +} + +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')); + 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 parseEnvValue(projectRoot, key, fallback) { + const envPath = path.join(projectRoot, '.env'); + try { + const content = fs.readFileSync(envPath, 'utf8'); + const match = content.match(new RegExp(`^${key}=(.+)$`, 'm')); + if (match) return match[1].trim().replace(/^['"]|['"]$/g, ''); + } catch { + // ignore + } + return fallback; +} + +function parseDbPort(projectRoot) { + const raw = parseEnvValue(projectRoot, 'DB_PORT', '3306'); + const port = parseInt(raw, 10); + return Number.isFinite(port) && port > 0 ? port : 3306; +} + +function isPortListening(port, host = '127.0.0.1') { + const byLsof = spawnSync('lsof', [`-iTCP:${port}`, '-sTCP:LISTEN'], { encoding: 'utf8' }); + if (byLsof.status === 0 && byLsof.stdout.trim()) return true; + const byNc = spawnSync('nc', ['-z', host, String(port)], { stdio: 'ignore' }); + return byNc.status === 0; +} + +function isDbContainerRunning(container) { + const res = spawnSync('docker', ['inspect', '-f', '{{.State.Running}}', container], { + encoding: 'utf8', + }); + return res.status === 0 && res.stdout.trim() === 'true'; +} + +async function startDockerServices(projectRoot) { + console.log(t('docker.starting')); + if (!isDockerRunning()) { + throw new Error(t('docker.notRunning')); + } + 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 dbPort = parseDbPort(projectRoot); + + if (isPortListening(dbPort)) { + if (await probeMysqlHost(projectRoot)) { + console.log(t('docker.dbReuseExisting', { port: dbPort })); + const nginxOnly = runCompose(['up', '-d', '--no-deps', 'nginx'], ctx); + if (nginxOnly.status !== 0) { + throw new Error(t('docker.notRunning')); + } + console.log(t('docker.started')); + return; + } + console.warn(t('docker.dbPortInUseRecycle', { port: dbPort })); + spawnSync('docker', ['rm', '-f', 'reactpress_db'], { stdio: 'ignore' }); + const result = runCompose(['up', '-d', '--no-deps', 'nginx'], ctx); + if (result.status !== 0) { + throw new Error(t('docker.notRunning')); + } + console.log(t('docker.started')); + return; + } + + const dbResult = runCompose(['up', '-d', 'db'], ctx); + if (dbResult.status !== 0) { + throw new Error(t('docker.notRunning')); + } + const nginxResult = runCompose(['up', '-d', '--no-deps', 'nginx'], ctx); + if (nginxResult.status !== 0) { + throw new Error(t('docker.notRunning')); + } + console.log(t('docker.started')); +} + +async function ensureDevDatabase(projectRoot, { quiet = false } = {}) { + const { isLocalSqliteMode } = require('./database-mode'); + if (isLocalSqliteMode(projectRoot)) { + const { ensureSqliteDatabase } = require('../core/services/database/sqlite'); + const { syncEnvFromConfig, loadConfig } = require('../core/services/config'); + const config = await loadConfig(projectRoot); + await syncEnvFromConfig(projectRoot, config); + const result = await ensureSqliteDatabase(projectRoot); + if (!result.ok) { + throw new Error(result.message || 'SQLite 数据库未就绪'); + } + return; + } + + const dbPort = parseDbPort(projectRoot); + const ctx = resolveComposeContext(projectRoot); + const container = resolveDbContainerName(ctx, projectRoot); + + const finishWhenReady = async (maxAttempts = 60) => { + if (await waitForMysql(projectRoot, maxAttempts, { quiet })) return true; + return false; + }; + + if (await probeMysqlHost(projectRoot) && (await finishWhenReady(4))) { + return; + } + + if (!quiet) console.log(t('docker.ensureDevDb')); + if (!isDockerRunning()) { + throw new Error(t('docker.devStartBlocked', { port: dbPort })); + } + + for (const name of new Set([container, 'reactpress_db', 'reactpress_cli_db'])) { + spawnSync('docker', ['start', name], { stdio: 'ignore' }); + } + await new Promise((r) => setTimeout(r, 1000)); + if (await finishWhenReady(45)) return; + + const composeStdio = quiet ? 'ignore' : 'inherit'; + + if (!isPortListening(dbPort)) { + const dbResult = runCompose(['up', '-d', '--remove-orphans', 'db'], ctx, { stdio: composeStdio }); + if (dbResult.status !== 0) { + throw new Error(t('docker.mysqlNotReady')); + } + } else if (await probeMysqlHost(projectRoot)) { + if (!quiet) console.log(t('docker.dbReuseExisting', { port: dbPort })); + if (await finishWhenReady(30)) return; + } else { + if (!quiet) console.warn(t('docker.dbPortInUseRecycle', { port: dbPort })); + spawnSync('docker', ['rm', '-f', 'reactpress_db'], { stdio: 'ignore' }); + await new Promise((r) => setTimeout(r, 500)); + const recreate = runCompose(['up', '-d', '--remove-orphans', 'db'], ctx, { stdio: composeStdio }); + if (recreate.status !== 0) { + throw new Error(t('docker.mysqlNotReady')); + } + } + + if (await finishWhenReady(60)) return; + + throw new Error( + t('docker.dbPortConflict', { + port: dbPort, + }), + ); +} + +/** Gate API boot — MySQL must accept connections on DB_PORT (avoids Nest retrying on :3307). */ +async function requireMysqlBeforeApi(projectRoot) { + const dbPort = parseDbPort(projectRoot); + if (await probeMysqlHost(projectRoot) && (await waitForMysql(projectRoot, 5))) { + return; + } + await ensureDevDatabase(projectRoot); + if (!(await probeMysqlHost(projectRoot))) { + throw new Error( + t('docker.dbPortConflict', { + port: dbPort, + }), + ); + } +} + +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 probeMysqlHost(projectRoot) { + const host = parseEnvValue(projectRoot, 'DB_HOST', '127.0.0.1'); + const port = parseDbPort(projectRoot); + const user = parseEnvValue(projectRoot, 'DB_USER', 'reactpress'); + const password = + parseEnvValue(projectRoot, 'DB_PASSWD', '') || + parseEnvValue(projectRoot, 'DB_PASSWORD', 'reactpress'); + const database = parseEnvValue(projectRoot, 'DB_DATABASE', 'reactpress'); + + let mysql; + try { + mysql = require('mysql2/promise'); + } catch { + try { + mysql = require(path.join(projectRoot, 'server/node_modules/mysql2/promise')); + } catch { + return false; + } + } + + try { + const conn = await mysql.createConnection({ + host, + port, + user, + password, + database, + connectTimeout: 3000, + }); + await conn.ping(); + await conn.end(); + return true; + } catch { + return false; + } +} + +async function waitForMysql(projectRoot, maxAttempts = 30, { quiet = false } = {}) { + const ctx = resolveComposeContext(projectRoot); + const container = resolveDbContainerName(ctx, projectRoot); + const { user, password } = resolveDbCredentialsFromEnv(projectRoot); + const dbPort = parseDbPort(projectRoot); + + if (!isDbContainerRunning(container) && isPortListening(dbPort)) { + const ready = await probeMysqlHost(projectRoot); + if (ready) { + if (!quiet) console.log(t('docker.mysqlExternalReady', { port: dbPort })); + return true; + } + } + + const useSpinner = !quiet && process.stdout.isTTY; + const spinner = useSpinner + ? ora({ + text: t('docker.waitingMysql'), + color: 'magenta', + spinner: 'dots', + }).start() + : null; + + let attempts = 0; + while (attempts < maxAttempts) { + if (isDbContainerRunning(container)) { + const probe = spawnSync( + 'docker', + ['exec', container, 'mysql', `-u${user}`, `-p${password}`, '-e', 'SELECT 1'], + { stdio: 'ignore' } + ); + if (probe.status === 0) { + if (spinner) spinner.succeed(t('docker.mysqlReady')); + return true; + } + } else if (await probeMysqlHost(projectRoot)) { + if (spinner) spinner.succeed(t('docker.mysqlExternalReady', { port: dbPort })); + return true; + } + attempts += 1; + if (spinner) { + spinner.text = t('docker.waitingMysqlProgress', { attempts, max: maxAttempts }); + } + await new Promise((r) => setTimeout(r, 1000)); + } + if (spinner) spinner.fail(t('docker.mysqlTimeout')); + return false; +} + +async function dockerStartWithDev(projectRoot) { + await startDockerServices(projectRoot); + const ready = await waitForMysql(projectRoot); + if (!ready) { + throw new Error(t('docker.mysqlNotReady')); + } + + const { runDev } = require('./dev'); + await runDev(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': + await startDockerServices(projectRoot); + await waitForMysql(projectRoot); + return; + case 'down': + case 'stop': + stopDockerServices(projectRoot); + return; + case 'start': + await dockerStartWithDev(projectRoot); + return; + case 'restart': + stopDockerServices(projectRoot); + await new Promise((r) => setTimeout(r, 2000)); + await startDockerServices(projectRoot); + await waitForMysql(projectRoot); + return; + 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]; + const args = ['logs', '-f']; + if (service) args.push(service); + runCompose(args, ctx); + return; + } + default: + throw new Error(t('docker.unknownCommand', { command })); + } +} + +module.exports = { + runDockerCommand, + startDockerServices, + stopDockerServices, + waitForMysql, + ensureDevDatabase, + requireMysqlBeforeApi, + probeMysqlHost, + isDockerRunning, + resolveComposeContext, + pickDockerComposeCommand, + mysqldumpFromDbContainer, +}; diff --git a/cli/src/lib/doctor.ts b/cli/src/lib/doctor.ts new file mode 100644 index 00000000..aa866e65 --- /dev/null +++ b/cli/src/lib/doctor.ts @@ -0,0 +1,432 @@ +// @ts-nocheck +const fs = require('fs'); +const net = require('net'); +const path = require('path'); +const { execSync } = require('child_process'); +const ora = require('ora'); +const { + brand, + icon, + ok, + warn, + divider, + sectionHeader, + terminalWidth, + gradientText, + palette, +} = require('../ui/theme'); +const { + getHealthUrl, + checkHealth, + loadAdminConsoleUrl, + loadClientSiteUrl, + isHttpResponding, +} = require('./http'); +const { isDockerRunning } = require('./docker'); +const { checkNginx } = require('./nginx'); +const { envFileStatus } = require('./status'); +const { describeProject } = require('./project-type'); +const { t } = require('./i18n'); +const { runDoctorLogs } = require('./project-logs'); + +function checkNodeVersion() { + const major = parseInt(process.versions.node.split('.')[0], 10); + if (major >= 18) { + return { ok: true, message: `Node.js ${process.version}` }; + } + return { + ok: false, + message: t('doctor.nodeBad', { version: process.version }), + fix: t('doctor.nodeFix'), + }; +} + +function checkDocker() { + if (isDockerRunning()) { + return { ok: true, message: t('doctor.dockerOk') }; + } + return { + ok: false, + message: t('doctor.dockerBad'), + fix: t('doctor.dockerFix'), + }; +} + +function isLocalZeroDepMode(projectRoot) { + if (process.env.REACTPRESS_LOCAL_MODE === '1' || process.env.REACTPRESS_SKIP_NGINX === '1') { + return true; + } + const env = parseEnv(projectRoot); + if (String(env.DB_TYPE || '').toLowerCase() === 'sqlite') return true; + try { + const configPath = path.join(projectRoot, '.reactpress', 'config.json'); + if (fs.existsSync(configPath)) { + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + if (config.database?.mode === 'embedded-sqlite') return true; + } + } catch { + // ignore + } + return false; +} + +async function resolveProjectProfile(projectRoot) { + const env = parseEnv(projectRoot); + let configMode; + try { + const configPath = path.join(projectRoot, '.reactpress', 'config.json'); + if (fs.existsSync(configPath)) { + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + configMode = config.database?.mode; + } + } catch { + // ignore + } + + const envDbType = String(env.DB_TYPE || '').toLowerCase(); + if ( + process.env.REACTPRESS_LOCAL_MODE === '1' || + process.env.REACTPRESS_SKIP_NGINX === '1' || + envDbType === 'sqlite' || + configMode === 'embedded-sqlite' + ) { + return { localMode: true, dbType: 'sqlite', requiresDocker: false }; + } + + const dbType = await resolveDbType(projectRoot); + if (dbType === 'sqlite') { + return { localMode: true, dbType: 'sqlite', requiresDocker: false }; + } + + if (configMode === 'embedded-docker') { + return { localMode: false, dbType: 'mysql', requiresDocker: true }; + } + + return { localMode: false, dbType, requiresDocker: true }; +} + +function applyProjectRuntimeEnv(profile) { + if (profile.localMode) { + process.env.REACTPRESS_LOCAL_MODE = '1'; + process.env.REACTPRESS_SKIP_NGINX = '1'; + } +} + +function checkDockerForProject(projectRoot, profile) { + if (profile?.requiresDocker === false || profile?.localMode || isLocalZeroDepMode(projectRoot)) { + return { ok: true, message: t('doctor.dockerSkipped') }; + } + return checkDocker(); +} + +function checkPnpmForProject(projectRoot) { + const project = describeProject(projectRoot); + if (project.kind !== 'monorepo') { + return { ok: true, message: t('doctor.pnpmSkipped') }; + } + return checkPnpm(); +} + +function parseEnv(projectRoot) { + const envPath = path.join(projectRoot, '.env'); + const out = {}; + try { + const content = fs.readFileSync(envPath, 'utf8'); + for (const line of content.split('\n')) { + const m = line.match(/^([A-Z_]+)=(.*)$/); + if (m) out[m[1]] = m[2].trim().replace(/^['"]|['"]$/g, ''); + } + } catch { + // ignore + } + return out; +} + +function checkPort(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(1000, () => { + socket.destroy(); + resolve(false); + }); + }); +} + +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 })); + if (clientBusy) issues.push(t('doctor.portClientBusy', { port: clientPort })); + if (issues.length) { + return { + ok: false, + message: issues.join('; '), + fix: t('doctor.portFix'), + }; + } + return { + ok: true, + message: t('doctor.portOk', { apiPort, clientPort }), + }; +} + +async function resolveDbType(projectRoot) { + const env = parseEnv(projectRoot); + if (String(env.DB_TYPE || '').toLowerCase() === 'sqlite') return 'sqlite'; + try { + const configPath = path.join(projectRoot, '.reactpress', 'config.json'); + if (fs.existsSync(configPath)) { + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + if (config.database?.mode === 'embedded-sqlite') return 'sqlite'; + } + } catch { + // ignore + } + return 'mysql'; +} + +async function checkDatabase(projectRoot) { + const dbType = await resolveDbType(projectRoot); + + if (dbType === 'sqlite') { + const { probeSqliteDatabase } = require('../core/services/database/sqlite'); + const result = await probeSqliteDatabase(projectRoot); + return { + ok: result.ok, + message: result.ok + ? t('doctor.dbSqliteOk', { detail: result.message ?? '' }) + : t('doctor.dbSqliteBad', { error: result.message ?? '' }), + fix: result.ok ? undefined : t('doctor.dbSqliteFix'), + }; + } + + const env = parseEnv(projectRoot); + const host = env.DB_HOST || '127.0.0.1'; + const port = parseInt(env.DB_PORT || '3306', 10); + const user = env.DB_USER || 'root'; + const password = env.DB_PASSWD || env.DB_PASSWORD || 'root'; + const database = env.DB_DATABASE || 'reactpress'; + + return new Promise((resolve) => { + let mysql; + try { + mysql = require('mysql2/promise'); + } catch { + try { + mysql = require(path.join(projectRoot, 'server/node_modules/mysql2/promise')); + } catch { + resolve({ + ok: false, + message: t('doctor.dbNoMysql2'), + fix: t('doctor.dbMysql2Fix'), + }); + return; + } + } + + mysql + .createConnection({ host, port, user, password, database, connectTimeout: 5000 }) + .then(async (conn) => { + await conn.ping(); + await conn.end(); + resolve({ + ok: true, + message: t('doctor.dbOk', { host, port, database }), + }); + }) + .catch((err) => { + resolve({ + ok: false, + message: t('doctor.dbBad', { error: err.message }), + fix: t('doctor.dbFix'), + }); + }); + }); +} + +async function checkApiHealth(projectRoot, profile) { + const healthUrl = getHealthUrl(projectRoot); + const result = await checkHealth(healthUrl); + if (result.ok) { + return { ok: true, message: t('doctor.apiOk', { url: healthUrl }) }; + } + const fix = + profile?.localMode || isLocalZeroDepMode(projectRoot) + ? t('doctor.apiFixInit') + : t('doctor.apiFix'); + return { + ok: false, + message: t('doctor.apiBad', { url: healthUrl }), + fix, + }; +} + +async function checkThemeSite(projectRoot) { + const url = loadClientSiteUrl(projectRoot); + const ok = await isHttpResponding(url, 2500); + if (ok) { + return { ok: true, message: t('doctor.siteOk', { url }) }; + } + return { + ok: false, + message: t('doctor.siteBad', { url }), + fix: t('doctor.siteFix'), + }; +} + +async function checkAdminConsole(projectRoot) { + const url = loadAdminConsoleUrl(projectRoot); + const ok = await isHttpResponding(url, 2500); + if (ok) { + return { ok: true, message: t('doctor.adminOk', { url }) }; + } + return { + ok: false, + message: t('doctor.adminBad', { url }), + fix: t('doctor.adminFix', { url }), + }; +} + +function checkPnpm() { + try { + const v = execSync('pnpm -v', { encoding: 'utf8' }).trim(); + return { ok: true, message: `pnpm ${v}` }; + } catch { + return { + ok: false, + message: t('doctor.pnpmBad'), + fix: t('doctor.pnpmFix'), + }; + } +} + +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, options = {}) { + const profile = await resolveProjectProfile(projectRoot); + applyProjectRuntimeEnv(profile); + const env = envFileStatus(projectRoot); + const checks = [ + { name: 'Node.js', run: () => checkNodeVersion() }, + { name: 'pnpm', run: () => checkPnpmForProject(projectRoot) }, + { + name: t('doctor.check.config'), + run: () => ({ + ok: env.config, + message: env.config ? t('doctor.configOk') : t('doctor.configBad'), + fix: env.config ? undefined : t('doctor.configFix'), + }), + }, + { + name: t('doctor.check.env'), + run: () => ({ + ok: env.env, + message: env.env ? t('doctor.envOk') : t('doctor.envBad'), + fix: env.env ? undefined : t('doctor.envFix'), + }), + }, + { name: 'Docker', run: () => checkDockerForProject(projectRoot, profile) }, + { 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, profile) }, + ]; + + if (profile.localMode) { + checks.push( + { name: t('doctor.check.site'), run: () => checkThemeSite(projectRoot) }, + { name: t('doctor.check.admin'), run: () => checkAdminConsole(projectRoot) } + ); + } + + const w = Math.min(terminalWidth() - 4, 52); + const results = []; + const fixes = []; + + console.log(''); + 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)}`); + + for (const { name, run } of checks) { + 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) { + fixes.push({ name, fix: result.fix }); + } + } + + 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(` ${ok(t('doctor.allPass'))}`); + } else { + 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)}`); + } + } + if (!options.showLogs) { + console.log(''); + console.log(` ${brand.dim(t('doctor.logsHint'))}`); + } + } + if (options.showLogs) { + await runDoctorLogs(projectRoot, { tail: 40, source: 'error' }); + } + console.log(''); + return failed === 0 ? 0 : 1; +} + +module.exports = { + runDoctor, + checkNodeVersion, + checkDocker, + checkDockerForProject, + resolveProjectProfile, + isLocalZeroDepMode, +}; diff --git a/cli/src/lib/health-parse.test.ts b/cli/src/lib/health-parse.test.ts new file mode 100644 index 00000000..30ee285d --- /dev/null +++ b/cli/src/lib/health-parse.test.ts @@ -0,0 +1,37 @@ +// @ts-nocheck +const assert = require('assert'); +const { + normalizeHealthPayload, + isHealthPayloadReady, + isHealthHttpReady, +} = require('./health-parse'); + +assert.strictEqual( + isHealthHttpReady(200, { + statusCode: 200, + success: true, + data: { status: 'ok', database: 'up', version: '3.0.0' }, + }), + true, +); + +assert.strictEqual( + isHealthHttpReady(200, { status: 'ok', database: 'up' }), + true, +); + +assert.strictEqual( + isHealthHttpReady(200, { + statusCode: 200, + success: true, + data: { status: 'degraded', database: 'down' }, + }), + false, +); + +assert.strictEqual( + isHealthPayloadReady(normalizeHealthPayload({ status: 'ok', database: 'up' })), + true, +); + +console.log('health-parse.test.js: ok'); diff --git a/cli/src/lib/health-parse.ts b/cli/src/lib/health-parse.ts new file mode 100644 index 00000000..cef94280 --- /dev/null +++ b/cli/src/lib/health-parse.ts @@ -0,0 +1,36 @@ +// @ts-nocheck +/** + * Parse `/api/health` JSON — supports raw Nest payload and TransformInterceptor wrapper. + */ + +function normalizeHealthPayload(body) { + if (!body || typeof body !== 'object') return null; + if ( + body.data && + typeof body.data === 'object' && + (Object.prototype.hasOwnProperty.call(body.data, 'status') || + Object.prototype.hasOwnProperty.call(body.data, 'database')) + ) { + return body.data; + } + return body; +} + +function isHealthPayloadReady(payload) { + if (!payload || typeof payload !== 'object') return false; + if (Object.prototype.hasOwnProperty.call(payload, 'database')) { + return payload.status === 'ok' && payload.database === 'up'; + } + return payload.status === 'ok' || payload.status === 'OK'; +} + +function isHealthHttpReady(statusCode, body) { + if (statusCode !== 200) return false; + return isHealthPayloadReady(normalizeHealthPayload(body)); +} + +module.exports = { + normalizeHealthPayload, + isHealthPayloadReady, + isHealthHttpReady, +}; diff --git a/cli/src/lib/http.ts b/cli/src/lib/http.ts new file mode 100644 index 00000000..b6839f20 --- /dev/null +++ b/cli/src/lib/http.ts @@ -0,0 +1,269 @@ +// @ts-nocheck +const fs = require('fs'); +const http = require('http'); +const path = require('path'); +const { DEV_PORTS } = require('./ports'); +const { + normalizeHealthPayload, + isHealthPayloadReady, + isHealthHttpReady, +} = require('./health-parse'); + +function loadServerSiteUrl(projectRoot) { + const envPath = path.join(projectRoot, '.env'); + try { + const content = fs.readFileSync(envPath, 'utf8'); + const match = content.match(/^SERVER_SITE_URL=(.+)$/m); + if (match) { + return match[1].trim().replace(/^['"]|['"]$/g, ''); + } + } catch { + // ignore + } + return 'http://localhost:3002'; +} + +function loadWebAdminUrl(projectRoot) { + const envPath = path.join(projectRoot, '.env'); + try { + const content = fs.readFileSync(envPath, 'utf8'); + const urlMatch = content.match(/^WEB_ADMIN_URL=(.+)$/m); + if (urlMatch) { + return urlMatch[1].trim().replace(/^['"]|['"]$/g, ''); + } + const portMatch = content.match(/^WEB_ADMIN_PORT=(.+)$/m); + if (portMatch) { + const port = parseInt(portMatch[1].trim(), 10); + if (Number.isInteger(port) && port > 0) { + return `http://localhost:${port}`; + } + } + } catch { + // ignore + } + return `http://localhost:${DEV_PORTS.ADMIN_WEB}`; +} + +/** Admin console URL for the running stack (Vite dev vs theme-embedded SPA). */ +function loadAdminConsoleUrl(projectRoot) { + const envPath = path.join(projectRoot, '.env'); + try { + const content = fs.readFileSync(envPath, 'utf8'); + const urlMatch = content.match(/^WEB_ADMIN_URL=(.+)$/m); + if (urlMatch) { + return urlMatch[1].trim().replace(/^['"]|['"]$/g, ''); + } + } catch { + // ignore + } + if (process.env.REACTPRESS_LOCAL_MODE === '1' || process.env.REACTPRESS_SKIP_NGINX === '1') { + const client = loadClientSiteUrl(projectRoot).replace(/\/$/, ''); + return `${client}/admin/`; + } + return loadWebAdminUrl(projectRoot); +} + +function loadClientSiteUrl(projectRoot) { + const envPath = path.join(projectRoot, '.env'); + try { + const content = fs.readFileSync(envPath, 'utf8'); + const match = content.match(/^CLIENT_SITE_URL=(.+)$/m); + if (match) { + return match[1].trim().replace(/^['"]|['"]$/g, ''); + } + } catch { + // ignore + } + return 'http://localhost:3001'; +} + +function getApiPrefix(projectRoot) { + const envPath = path.join(projectRoot, '.env'); + try { + const content = fs.readFileSync(envPath, 'utf8'); + const match = content.match(/^SERVER_API_PREFIX=(.+)$/m); + if (match) { + return match[1].trim().replace(/^['"]|['"]$/g, ''); + } + } catch { + // ignore + } + return '/api'; +} + +function getHealthUrl(projectRoot) { + const base = loadServerSiteUrl(projectRoot).replace(/\/$/, ''); + const prefix = getApiPrefix(projectRoot).replace(/\/$/, ''); + return `${base}${prefix}/health`; +} + +/** Use IPv4 loopback for probes — `localhost` often resolves to `::1` while Nest binds IPv4. */ +function parseProbeTarget(urlString) { + const parsed = new URL(urlString); + const host = parsed.hostname; + if (host === 'localhost' || host === '::1' || host === '[::1]') { + parsed.hostname = '127.0.0.1'; + } + return parsed; +} + +function normalizeProbeUrl(urlString) { + return parseProbeTarget(urlString).toString(); +} + +function probeHttp(url, timeoutMs = 3000) { + return new Promise((resolve) => { + let parsed; + try { + parsed = parseProbeTarget(url); + } catch { + resolve({ ok: false, statusCode: 0, data: null }); + return; + } + const port = parsed.port || (parsed.protocol === 'https:' ? 443 : 80); + const req = http.request( + { + hostname: parsed.hostname, + port, + family: 4, + path: parsed.pathname + (parsed.search || ''), + method: 'GET', + timeout: timeoutMs, + }, + (res) => { + let body = ''; + res.on('data', (chunk) => { + body += chunk; + }); + res.on('end', () => { + const ok = res.statusCode === 200; + let data = null; + try { + data = JSON.parse(body); + } catch { + // ignore + } + resolve({ ok, statusCode: res.statusCode, data }); + }); + } + ); + req.on('timeout', () => { + req.destroy(); + resolve({ ok: false, statusCode: 0, data: null }); + }); + 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(normalizeProbeUrl(url), timeoutMs); + const payload = normalizeHealthPayload(primary.data); + if (isHealthHttpReady(primary.statusCode, primary.data)) { + return { ok: true, statusCode: primary.statusCode, data: payload }; + } + if (primary.ok) { + return { ok: false, statusCode: primary.statusCode, data: payload ?? primary.data }; + } + + if (primary.statusCode === 404 || primary.statusCode === 0) { + try { + const parsed = parseProbeTarget(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: false, + statusCode: 200, + data: { status: 'degraded', database: 'unknown' }, + }; + } + } + } catch { + // ignore + } + } + + return primary; +} + +function isHttpResponding(url, timeoutMs = 2000) { + return new Promise((resolve) => { + let parsed; + try { + parsed = parseProbeTarget(url); + } catch { + resolve(false); + return; + } + + const port = parsed.port || (parsed.protocol === 'https:' ? 443 : 80); + const req = http.request( + { + hostname: parsed.hostname, + port, + family: 4, + path: parsed.pathname || '/', + method: 'GET', + timeout: timeoutMs, + }, + (res) => resolve(res.statusCode > 0) + ); + + req.on('timeout', () => { + req.destroy(); + resolve(false); + }); + req.on('error', () => resolve(false)); + req.end(); + }); +} + +async function waitForHttp(url, timeoutMs = 120_000, intervalMs = 500) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (await isHttpResponding(url)) { + return true; + } + await new Promise((r) => setTimeout(r, intervalMs)); + } + return false; +} + +/** Poll until HTTP 200 — used for theme dev homepage compile readiness. */ +async function waitForHttpOk(url, timeoutMs = 120_000, intervalMs = 500) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const result = await probeHttp(normalizeProbeUrl(url), Math.min(intervalMs + 500, 3000)); + if (result.ok) return true; + await new Promise((r) => setTimeout(r, intervalMs)); + } + return false; +} + +module.exports = { + isHealthPayloadReady, + loadServerSiteUrl, + loadWebAdminUrl, + loadAdminConsoleUrl, + loadClientSiteUrl, + getApiPrefix, + getHealthUrl, + normalizeProbeUrl, + checkHealth, + isHttpResponding, + waitForHttp, + waitForHttpOk, + probeHttp, + normalizeProbeUrl, +}; diff --git a/cli/src/lib/i18n/index.ts b/cli/src/lib/i18n/index.ts new file mode 100644 index 00000000..299ae618 --- /dev/null +++ b/cli/src/lib/i18n/index.ts @@ -0,0 +1,36 @@ +const { STRINGS } = require('./strings'); + +function resolveLocale() { + const raw = process.env.REACTPRESS_LANG || process.env.LANG || 'en'; + const code = String(raw).split(/[._-]/)[0].toLowerCase(); + return code === 'zh' ? 'zh' : 'en'; +} + +let locale = resolveLocale(); + +/** + * @param {string} key + * @param {Record} [vars] + */ +function t(key, vars = {}) { + const table = STRINGS[locale] || STRINGS.en; + let text = table[key] ?? STRINGS.en[key] ?? key; + if (text == null) { + text = key == null ? '' : String(key); + } + text = String(text); + for (const [name, value] of Object.entries(vars)) { + text = text.replace(new RegExp(`\\{${name}\\}`, 'g'), String(value ?? '')); + } + return text; +} + +function getLocale() { + return locale; +} + +function setLocale(next) { + locale = next === 'zh' ? 'zh' : 'en'; +} + +module.exports = { t, getLocale, setLocale, resolveLocale }; diff --git a/cli/src/lib/i18n/strings.ts b/cli/src/lib/i18n/strings.ts new file mode 100644 index 00000000..a2a086f1 --- /dev/null +++ b/cli/src/lib/i18n/strings.ts @@ -0,0 +1,1393 @@ +/** CLI user-facing strings — English first, Chinese via REACTPRESS_LANG=zh or zh LANG */ +const STRINGS = { + en: { + 'cli.description': + 'ReactPress 4 — zero-dependency publishing platform. Run `reactpress init` or `reactpress doctor`.', + 'cli.init.description': 'Initialize and start ReactPress (SQLite API + admin + theme)', + 'cli.init.directory': 'Project directory', + 'cli.init.force': 'Overwrite existing config', + 'cli.init.local': 'Initialize with embedded SQLite (no Docker)', + 'cli.dev.description': 'Zero-config dev: env check + toolkit build + API + frontend', + 'cli.dev.apiOnly': 'API only (watch)', + 'cli.dev.local': 'Local SQLite mode (no Docker/nginx)', + 'cli.dev.clientOnly': 'Frontend only', + 'cli.dev.webOnly': 'Admin SPA + API (web/)', + 'cli.dev.remoteOrigin': 'Default remote API URL; with no admin/client flags, both use remote', + 'cli.dev.adminOrigin': 'Admin API: local | remote | URL (remote uses --remote-origin default)', + 'cli.dev.clientOrigin': 'Client/theme API (nginx /api): local | remote | URL', + 'cli.dev.remoteOriginRequired': '--remote-origin requires a URL (e.g. api.gaoredu.com)', + 'cli.dev.remoteDefaultRequired': 'Use remote with a URL: --remote-origin or pass a URL to admin/client-origin', + 'cli.dev.invalidOrigin': 'Invalid origin; use local, remote, or a host/URL', + 'cli.dev.remoteOriginIncompatibleApiOnly': 'Remote API flags cannot be used with --api-only', + 'cli.desktopDev.description': 'Desktop dev: embedded SQLite API + admin SPA + Electron (no Docker/MySQL)', + 'cli.server.description': 'Manage API service', + 'cli.server.start.description': 'Start API (wait until HTTP ready)', + 'cli.server.start.pm2': 'Start with PM2 (production)', + 'cli.server.start.bg': 'Start in background without waiting for HTTP', + 'cli.server.stop': 'Stop API', + 'cli.server.restart': 'Restart API', + 'cli.server.status': 'API status', + 'cli.client.description': 'Manage frontend', + 'cli.client.start': 'Start Next.js client', + 'cli.client.start.pm2': 'Start with PM2', + 'cli.client.restart': 'Rebuild active theme and restart visitor client', + 'themeProd.building': 'Building active theme "{id}"…', + 'themeProd.installingDeps': 'Installing dependencies for theme "{id}"…', + 'themeProd.reusingBuild': 'Reusing existing build for theme "{id}" (skip rebuild)', + 'themeProd.restarting': 'Restarting visitor client for theme "{id}"…', + 'themeProd.restarted': 'Visitor client is running theme "{id}".', + 'themePreview.backgroundBuildScheduled': + 'Scheduling background production builds for {count} preview theme(s)…', + 'themePreview.warmingAll': 'Pre-building {count} theme preview(s) for fast switching…', + 'themePreview.warmingAllSkipped': '{count} preview build(s) already up to date', + 'themePreview.installingDeps': 'Installing dependencies for preview theme "{id}"…', + 'themePreview.building': 'Building preview theme "{id}"…', + 'themePreview.reusingBuild': 'Reusing existing build for preview theme "{id}" (skip rebuild)', + 'themePreview.buildDone': 'Preview theme "{id}" build finished.', + 'themePreview.buildFailed': 'Preview theme "{id}" build failed: {message}', + 'themePreview.starting': + 'Preview theme "{id}" → {url} (port {port}, {dir}, {mode})', + 'themePreview.ready': 'Preview ready: {url} (theme: {id})', + 'cli.build.description': 'Build production artifacts', + 'cli.docker.description': 'Docker dev environment (MySQL + nginx)', + 'cli.docker.up': 'Start Docker services and wait for MySQL', + 'cli.docker.down': 'Stop Docker services', + 'cli.docker.start': 'Start Docker + full-stack dev (API + frontend)', + '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.doctor.showLogs': 'Show recent API error logs after diagnostics', + 'cli.doctor.logs.description': 'View recent API logs (error, request, response)', + 'cli.doctor.logs.tailOption': 'Number of lines to show per log file', + 'cli.doctor.logs.sourceOption': 'Log source: error, request, response, or all', + 'cli.doctor.logs.grepOption': 'Filter log lines by regex', + 'cli.doctor.logs.listOption': 'List available log files only', + 'cli.logs.description': 'View API logs (PM2 process output or structured error/request/response)', + 'cli.logs.descriptionAlias': 'View API logs (alias of reactpress logs)', + 'cli.logs.tailOption': 'Number of lines to show per log file', + 'cli.logs.sourceOption': 'Log source: process (PM2), error, request, response, or all', + 'cli.logs.grepOption': 'Filter log lines by regex', + 'cli.logs.listOption': 'List available log files only', + 'cli.logs.followOption': 'Stream logs continuously (PM2 when supervised, otherwise tail -f)', + 'cli.stop.description': 'Stop API and site services started by init', + 'cli.db.description': 'Database operations', + 'cli.db.backup': 'Backup current project database with mysqldump', + 'cli.db.backup.output': 'Output SQL file path', + 'cli.publish.description': 'Build and publish npm packages', + 'cli.publish.build': 'Build publish artifacts only', + 'cli.publish.publish': 'Publish core packages to npm', + 'cli.start.description': 'Start ReactPress (SQLite API + admin + theme)', + 'cli.help.examples': 'Usage:', + 'cli.help.init': ' reactpress init [dir] Initialize project', + 'cli.help.doctor': ' reactpress doctor Diagnose environment issues', + 'cli.help.logs': ' reactpress logs Tail API logs for quick debugging', + 'cli.help.stop': ' reactpress stop Stop the background API', + 'cli.help.doctorLogs': ' reactpress doctor logs Alias of reactpress logs', + 'cli.help.zeroDep': ' Zero deps: Node.js 18+ only — no Docker, nginx, or MySQL', + 'cli.plugin.description': 'Manage ReactPress plugins', + 'cli.plugin.install.description': 'Install a local plugin into .reactpress/plugins', + 'cli.plugin.install.id': 'Plugin id from plugins/ registry', + 'cli.plugin.list.description': 'List registered plugins', + 'cli.theme.description': 'Install and manage themes', + 'cli.theme.add.description': 'Install a theme from an npm package spec or .tgz file', + 'cli.theme.add.spec': 'npm package spec (e.g. @fecommunity/reactpress-theme-starter@1.0.0-beta.0)', + 'cli.theme.add.catalog': 'Install from themes/{dir}/package.json by theme id', + 'cli.theme.add.skipDeps': 'Skip pnpm/npm install in the theme directory', + 'cli.theme.list.description': 'List available theme packages', + 'themeInstall.specRequired': 'Theme npm spec is required', + 'themeInstall.installing': 'Installing theme from npm: {spec}', + 'themeInstall.success': 'Theme "{name}" installed as "{id}" → {dir}', + 'themeInstall.nextActivate': 'Enable it in Admin → Appearance → Themes, or: POST /extension/themes/{id}/activate', + 'themeInstall.listHeading': 'Available themes:', + 'themeInstall.listEmpty': 'No theme packages found under themes/ or .reactpress/runtime/', + 'cli.build.target': 'Build target: toolkit | plugins | server | web | theme | docs | all', + 'cli.build.lowMem': 'Cap Node heap for builds and skip unchanged steps (2GB VPS)', + 'init.readyTitle': 'ReactPress is running', + 'init.label.path': 'Path', + 'init.label.db': 'Database', + 'init.label.site': 'Site', + 'init.label.admin': 'Admin', + 'init.label.api': 'API', + 'init.hint': 'Login: admin / admin · Services run in background · Stop: reactpress stop · Logs: reactpress logs', + 'init.alreadyInitialized': 'Project already initialized — ensuring services are running', + 'init.apiOnlyHint': + 'API is running. Install a theme in Admin → Appearance to enable the public site.', + 'init.pluginsSeeded': 'Seeded bundled plugins (hello-world, seo, image-optimizer).', + 'init.unknownCommand': 'Unknown command: {cmd}', + 'init.useInitOnly': 'ReactPress 4: reactpress init · doctor · logs · stop', + 'banner.subtitle': '· Publishing platform · v4.0', + 'banner.tagline': 'Zero deps · SQLite · ship in ~60s', + /** 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.service.sqlite': 'SQLite', + 'banner.service.mysql': 'MySQL', + 'banner.service.server': 'Server', + 'banner.service.docker': 'Docker', + 'banner.service.nginx': 'Nginx', + 'banner.service.web': 'Web', + 'banner.nginxRunning': 'running', + 'banner.nginxStopped': 'stopped', + 'banner.feat.sqlite': 'SQLite', + 'banner.feat.plugins': 'Plugins', + 'banner.feat.desktop': 'Desktop', + 'banner.feat.themes': 'Themes', + 'banner.mode.standalone': 'STANDALONE', + 'banner.mode.monorepo': 'MONOREPO', + 'banner.mode.uninitialized': 'UNINITIALIZED', + 'banner.systemLabel': 'SYSTEM', + 'banner.systemOnline': 'ONLINE', + 'banner.systemPartial': 'PARTIAL', + 'banner.systemError': 'ERROR', + 'banner.systemPending': 'PENDING', + 'menu.dev': 'Full-stack dev', + 'menu.init': 'Initialize project', + 'menu.status': 'View project status', + 'menu.doctor': 'Environment check', + 'menu.devApi': 'API only (watch)', + 'menu.devClient': 'Frontend only', + 'menu.serverStart': 'Start API (production)', + 'menu.serverStop': 'Stop API', + 'menu.serverRestart': 'Restart API', + 'menu.build': 'Production build', + 'menu.buildTarget': 'What do you want to build?', + 'menu.buildAll': 'All (toolkit → server → client)', + 'menu.dockerStart': 'Docker dev (DB + nginx + full stack)', + 'menu.dockerUp': 'Docker: database only', + 'menu.dockerStop': 'Stop Docker services', + 'menu.openAdmin': 'Open admin in browser', + 'menu.publish': 'Publish npm packages (interactive)', + 'menu.exit': 'Exit', + 'menu.prompt': 'Choose an action', + 'menu.subPrompt': '{section}', + 'menu.backToMain': 'Back to main menu', + 'menu.back': 'Return to main menu?', + 'menu.retry': 'Return to main menu and retry?', + 'menu.startingDev': 'Starting full-stack dev…', + 'menu.initProject': 'Initializing project…', + 'menu.done': 'Done', + 'menu.opening': 'Opening {url}', + 'menu.goodbye': ' Goodbye.', + 'menu.section.run': 'Run', + 'menu.section.quick': 'Quick start', + 'menu.section.explore': 'More', + 'menu.section.extend': 'Dev alternatives', + 'menu.section.lifecycle': 'Service control', + 'menu.section.build': 'Build & deploy', + 'menu.section.tools': 'Tools', + 'menu.group.extend': 'Dev alternatives', + 'menu.group.lifecycle': 'Service control', + 'menu.group.build': 'Build & deploy', + 'menu.group.tools': 'Tools & utilities', + 'menu.groupHint.extend': 'desktop · web · SQLite · themes', + 'menu.groupHint.lifecycle': 'API · frontend · start/stop', + 'menu.groupHint.build': 'build · Docker · publish', + 'menu.groupHint.tools': 'nginx · backup · admin', + 'menu.devDesktop': 'Desktop dev', + 'menu.hint.devDesktop': 'SQLite + Electron', + 'menu.devWeb': 'Admin SPA + API', + 'menu.hint.devWeb': 'web/ dev server', + 'menu.devLocalWeb': 'Local web', + 'menu.hint.devLocalWeb': 'SQLite in browser', + 'menu.initLocal': 'Initialize (SQLite)', + 'menu.hint.initLocal': 'no Docker', + 'menu.themeList': 'List themes', + 'menu.hint.themeList': 'theme list', + 'menu.pluginList': 'List plugins', + 'menu.hint.pluginList': 'plugin list', + 'menu.dbBackup': 'Backup database', + 'menu.hint.dbBackup': 'mysqldump', + 'menu.tip': 'Tip: arrow keys to navigate, Enter to select, Ctrl+C to quit.', + 'menu.shortcuts': '↑/↓ navigate · 1-9 quick pick · enter select · 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.phaseApi': 'API → :3002', + 'dev.phasePrerequisites': 'Checking Node.js and Docker…', + 'dev.phaseInfra': 'Starting MySQL and nginx…', + 'dev.phaseServices': 'Starting API, admin SPA, and theme…', + 'dev.phasePrerequisitesDesktop': 'Checking Node.js…', + 'dev.phaseInfraDesktop': 'Building toolkit & local workspace…', + 'dev.phaseServicesDesktop': 'Starting admin SPA and Electron…', + 'dev.phaseServicesLocalWeb': 'Starting admin SPA (browser preview)…', + 'dev.previewPrewarmStarting': 'Pre-building theme previews for fast switching…', + 'dev.remoteApiUsing': 'Using remote API (nginx /api → {url})', + 'dev.adminApiRemote': 'Admin API → {url}', + 'dev.clientApiRemote': 'Client API (nginx /api) → {url}', + 'dev.nginxReadyRemote': 'nginx ready at {url} (client API → {api})', + 'dev.checkNodeOk': 'Node.js {version}', + 'dev.checkDockerOk': 'Docker is running', + 'dev.prerequisitesOk': '✓ Node {version} · ✓ Docker', + 'dev.prerequisitesOkDesktop': '✓ Node {version} · SQLite (no Docker)', + 'dev.apiKept': 'Keeping healthy API on port {port}', + 'dev.timingReady': 'Ready in {summary}', + 'dev.timingInfra': 'infra', + 'dev.timingServices': 'services', + 'dev.timingApiReused': 'API reused', + 'dev.waitingApiQuiet': 'Waiting for API…', + 'dev.mysqlReadyQuiet': 'MySQL ready', + 'dev.themeStarting': 'Theme "{id}" → :{port}', + 'dev.themeCacheClearedForRemote': 'Cleared theme .next (remote API — stale SERVER_API_URL)', + 'dev.themeReadyQuiet': 'Visitor site ready → {url}', + 'dev.phaseAdmin': 'Starting admin SPA (internal :3000/admin/)…', + 'dev.phaseTheme': 'Starting active theme site (internal :3001)…', + 'dev.phaseClient': 'Starting legacy client…', + 'dev.phaseNginx': 'Starting nginx unified entry (:80)…', + 'dev.phaseNginxWait': 'Waiting for admin & theme, then nginx…', + 'dev.waitingProxies': 'Waiting for admin & theme…', + 'dev.startingAdmin': 'Admin dev server → {url}', + 'dev.adminNginxSlow': '[reactpress] Admin via nginx not ready: {url} — ensure Vite base is /admin/ (restart pnpm dev)', + 'dev.nginxReady': 'Nginx → {url}', + 'dev.proxiesReady': 'Admin & theme dev servers listening', + 'dev.portApiBusy': '[reactpress] Port {port} is already in use (API not healthy). Stop the other process or run: reactpress doctor', + 'dev.portApiBusyHint': '[reactpress] Tip: lsof -i :3002 · avoid running pnpm dev in two terminals', + 'dev.startingApi': 'Starting API (port 3002)…', + 'dev.waitingApi': 'Waiting for API: {url}', + 'dev.waitingApiCompile': 'compiling (port {port} not listening yet)', + 'dev.waitingApiStarting': 'port open, waiting for health', + 'dev.healthDbDown': 'database down', + 'dev.healthDegraded': 'API degraded', + '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', + 'dev.apiReusing': 'API on :{port} (healthy, skipped restart)', + 'dev.apiReady': 'API ready', + 'dev.toolkitUpToDate': 'Toolkit build skipped (dist is up to date; set REACTPRESS_FORCE_TOOLKIT_BUILD=1 to rebuild)', + 'dev.themeSiteSkipped': 'visitor site (:3001) will not start until the theme package exists', + 'dev.themeBackground': 'Visitor site (:3001) compiling in background — banner shows when API & admin are ready', + 'dev.themeBackgroundReady': 'Visitor site ready: {url}', + 'themeDev.starting': '[reactpress] Active theme "{id}" → {url} (port {port}, {dir})', + 'themeDev.startingShort': 'Theme "{id}" on :{port} ({dir})', + 'themeDev.cacheCleared': 'Cleared theme .next (REACTPRESS_CLEAR_THEME_CACHE=1)', + 'themeDev.cacheStaleCleared': 'Cleared theme .next (missing {marker})', + 'themeDev.apiSplit': '[reactpress] Theme API — SSR: {ssr} · browser: {browser}', + 'themeDev.ready': '[reactpress] Public site ready: {url} (theme: {id})', + 'themeDev.slow': '[reactpress] Theme site slow to start: {url}', + 'themeDev.notFound': 'Theme package "{id}" not found', + 'themeDev.invalidManifest': + 'Ignored invalid active-theme.json (only themes/ or .reactpress/runtime/ packages apply)', + 'themeDev.unavailable': 'will not listen', + 'themeDev.restart': 'active-theme.json changed — restarting theme on :3001…', + 'themeDev.restartFailed': 'theme restart failed: {message}', + 'themeDev.portBusy': 'Port {port} still in use after stopping the previous theme — skip restart (retry activate or restart pnpm dev)', + 'themeDev.portBusyHint': 'Or free the port manually: {cmd}', + 'dev.apiReadyAdmin': 'API ready · starting admin', + 'dev.clientSlow': '[reactpress] Frontend not responding within {seconds}s; it may still be compiling. Visit {url} later', + 'dev.adminSlow': '[reactpress] Admin SPA not responding within {seconds}s; it may still be compiling. Visit {url} later', + 'dev.noWeb': '[reactpress] web/ directory not found; cannot start admin dev stack.', + 'dev.envFailed': '[reactpress] Environment setup failed:', + 'dev.toolkitFailed': 'toolkit build failed with exit code: {code}', + 'dev.nextSteps': 'Suggested next steps:', + 'dev.nextDoctor': ' → reactpress doctor Diagnostics', + 'dev.nextDocker': ' → reactpress docker up Start embedded MySQL', + 'dev.nextEnv': ' → Check DB_* and SERVER_SITE_URL in .env', + 'dev.standaloneHint': '[reactpress] Standalone project: only API is started here; build your own frontend separately.', + 'dev.desktopStarting': 'Starting Electron desktop → {url}', + 'dev.desktopMissing': 'desktop/ package not found — run from monorepo root', + 'dev.desktopIntro': 'Desktop dev — local-first mode (SQLite embedded, no Docker/MySQL)', + 'dev.localWebIntro': 'Local web dev — SQLite API + admin SPA in browser (no Docker/Electron)', + 'dev.localFullIntro': 'Local full-stack dev — SQLite API + admin + theme (no Docker/MySQL)', + 'dev.autoLocalNoDocker': + 'Docker unavailable — auto-switching to local SQLite mode (no MySQL required)', + 'dev.autoLocalNoMysql': 'MySQL unreachable — auto-switching to local SQLite mode', + 'dev.desktopLocalApiStarting': 'Starting embedded SQLite API…', + 'dev.desktopLocalApiReady': '✓ Local API ({db}) → {url}', + 'dev.desktopLocalApi': 'Local SQLite API → {url}', + 'dev.dbTypeSqlite': 'SQLite', + 'devBanner.ready': 'ReactPress dev environment is ready', + 'devBanner.readyWeb': 'ReactPress admin dev environment is ready', + 'devBanner.readyLocalWeb': 'ReactPress local web dev environment is ready', + 'devBanner.readyDesktop': 'ReactPress desktop dev environment is ready', + 'devBanner.readyApi': 'ReactPress API is ready', + 'devBanner.site': 'Site', + 'devBanner.admin': 'Admin', + 'devBanner.api': 'API', + 'devBanner.database': 'Database', + 'devBanner.sqliteEmbedded': 'SQLite (embedded, no Docker)', + 'devBanner.mysqlDocker': 'MySQL (Docker)', + 'devBanner.swagger': 'Swagger', + 'devBanner.health': 'Health', + 'devBanner.desktopLocalHint': 'Default login admin/admin · switch to remote API in Settings', + 'devBanner.localWebHint': 'Open the admin URL in your browser · default login admin/admin', + 'devBanner.localModeGo': 'LOCAL MODE READY', + 'devBanner.hint': 'Diagnostics: reactpress doctor · Status: reactpress status', + 'devBanner.nginxHint': 'Internal ports 3001 (site) / 3002 (API) / 3003 (preview) / 3000 (admin) — use URLs above only', + 'devBanner.nginxRemoteHint': 'Client /api proxied to {url}', + 'devBanner.adminRemoteHint': 'Admin /api proxied to {url}', + 'devBanner.shortcuts': 'Ctrl+C stop', + 'devBanner.allSystemsGo': 'ALL SYSTEMS GO', + 'devBanner.dbDegraded': 'DB OFFLINE — run reactpress docker up', + 'dev.nginxSkippedDocker': '[reactpress] Docker not running — skipping nginx (use direct ports or start Docker)', + 'dev.nginxStartFailed': '[reactpress] nginx failed to start: {message}', + 'dev.dbEnsureFailed': '[reactpress] Database not ready: {message}', + 'dev.mysqlUnreachable': + '[reactpress] MySQL is not reachable — theme/admin API calls will fail. Start Docker, then: reactpress docker up', + 'dev.nginxSlow': '[reactpress] nginx entry slow to respond: {url}', + 'doctor.nodeBad': 'Node.js {version} (requires ≥ 18)', + 'doctor.nodeFix': 'Install Node.js 18+: https://nodejs.org/', + 'doctor.dockerOk': 'Docker engine is available', + 'doctor.dockerBad': 'Docker is not running or unavailable', + 'doctor.dockerSkipped': 'Docker not required (SQLite local mode)', + 'doctor.dockerFix': 'Install and start Docker: https://docs.docker.com/get-docker/ , then run reactpress docker up; or use external MySQL in config.json', + 'doctor.portApiBusy': 'API port {port} is in use', + 'doctor.portClientBusy': 'Frontend port {port} is in use', + 'doctor.portFix': 'Change SERVER_PORT / CLIENT_PORT in .env, or stop the blocking process', + 'doctor.portOk': 'Ports {apiPort} (API) and {clientPort} (frontend) are available', + 'doctor.dbNoMysql2': 'mysql2 not installed; cannot check database', + 'doctor.dbMysql2Fix': 'Run pnpm install at monorepo root', + 'doctor.dbOk': 'MySQL {host}:{port}/{database} connected', + 'doctor.dbBad': 'Database connection failed: {error}', + 'doctor.dbFix': 'Run reactpress docker up or check DB_* in .env', + 'doctor.dbSqliteOk': 'SQLite ready ({detail})', + 'doctor.dbSqliteBad': 'SQLite check failed: {error}', + 'doctor.dbSqliteFix': 'Run reactpress init --local or check DB_DATABASE in .env', + 'doctor.apiOk': 'API health check passed ({url})', + 'doctor.apiBad': 'API health check failed ({url})', + 'doctor.apiFix': 'Run reactpress server start or reactpress dev', + 'doctor.apiFixInit': 'Run reactpress init to start the API', + 'doctor.pnpmBad': 'pnpm not found', + 'doctor.pnpmFix': 'npm i -g pnpm, or enable corepack at monorepo root', + 'doctor.pnpmSkipped': 'pnpm optional (standalone / bundled runtime)', + 'doctor.check.config': 'Config file', + 'doctor.check.env': 'Environment', + 'doctor.check.ports': 'Ports', + 'doctor.check.database': 'Database', + 'doctor.check.api': 'API health', + 'doctor.check.site': 'Public site', + 'doctor.check.admin': 'Admin console', + 'doctor.siteOk': 'Site reachable at {url}', + 'doctor.siteBad': 'Site not reachable at {url}', + 'doctor.siteFix': 'Run reactpress init to start the theme frontend', + 'doctor.adminOk': 'Admin reachable at {url}', + 'doctor.adminBad': 'Admin not reachable at {url}', + 'doctor.adminFix': 'Run reactpress init, then open {url}', + 'doctor.configOk': '.reactpress/config.json exists', + 'doctor.configBad': 'Missing .reactpress/config.json', + 'doctor.configFix': 'Run reactpress init', + '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.', + 'doctor.title': 'ReactPress Doctor', + 'doctor.subtitle': 'environment diagnostics', + 'doctor.checking': 'Checking {name}…', + 'doctor.summary': '{passed} passed · {failed} failed · {total} total', + 'doctor.fixesHeader': 'Suggested fixes', + 'doctor.logsHint': 'Tip: run reactpress logs --tail 100 to inspect API error logs', + 'logs.title': 'ReactPress Logs', + 'logs.subtitle': 'API log viewer', + 'doctor.logs.title': 'ReactPress Logs', + 'doctor.logs.subtitle': 'API log viewer', + 'doctor.logs.dir': 'Log dir {path}', + 'doctor.logs.empty': 'No API log files found yet.', + 'doctor.logs.emptyHint': + 'Start the API with reactpress init — logs are written to .reactpress/logs/server/ (PM2 output: pm2-out.log / pm2-error.log)', + 'doctor.logs.listHeader': 'Available log files', + 'doctor.logs.fileHeader': '{file} (showing {count} line(s))', + 'doctor.logs.noMatchingLines': 'No matching log lines.', + 'doctor.logs.badPattern': 'Invalid grep pattern: {pattern}', + 'doctor.logs.noPid': 'No API pid file at {path}', + 'doctor.logs.activePid': 'API listening on port {port} (pid {pid})', + 'doctor.logs.pm2Supervised': 'API supervised by PM2 (pid {pid})', + 'doctor.logs.pm2Unavailable': 'PM2 is not available for live log streaming.', + 'doctor.logs.followWindows': 'Live follow is not supported here. Open: {path}', + 'doctor.logs.pidRunning': 'API pid {pid} (recorded in {path})', + 'doctor.logs.pidStale': 'Stale pid {pid} in {path} — process not running', + 'doctor.logs.moreHint': + 'Use --tail, --source, --grep, --follow, or --list for more control (see reactpress logs --help)', + 'status.title': 'ReactPress project status', + 'status.dir': 'Project {path}', + 'status.apiSource': 'API source {source}', + 'status.apiSource.monorepo': 'monorepo server/', + 'status.apiSource.bundle': '@fecommunity/reactpress', + 'status.configOk': '.reactpress/config.json', + 'status.configBad': 'not initialized', + 'status.envOk': '.env', + 'status.envBad': 'missing .env', + 'status.apiOnline': 'online', + 'status.apiOffline': 'offline', + 'status.apiUnreachable': '{url} (offline or not started)', + 'status.dbUp': 'connected', + 'status.dbDown': 'unavailable', + 'status.pidRunning': '(running)', + '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).', + 'bootstrap.initFailed': 'Initialization failed', + 'bootstrap.cliInitFailed': 'reactpress-cli init failed', + 'bootstrap.dbPendingShort': 'Database not ready', + 'bootstrap.dbNotReady': '{message}. Tip: start Docker and run reactpress docker up, or run reactpress doctor', + 'bootstrap.dbReady': 'Database is ready', + 'db.backup.to': 'Backing up database to {path}', + 'db.backup.done': 'Backup complete', + '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', + 'common.none': '(none)', + 'common.unknownError': 'Unknown error', + '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.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.apiExitedEarly': '[reactpress] API process exited early (pid {pid})', + 'lifecycle.apiReady': '[reactpress] API ready: {url}', + 'lifecycle.clientStopped': '[reactpress] Site process stopped (pid {pid})', + 'lifecycle.stopClientPidFailed': '[reactpress] Failed to stop site pid {pid}:', + 'lifecycle.clientAlreadyRunning': '[reactpress] Site already running (pid {pid})', + 'lifecycle.clientPidMissing': '[reactpress] Site failed to start (no pid recorded)', + 'lifecycle.waitingClient': 'Waiting for site at {url}…', + 'lifecycle.clientTimeout': '[reactpress] Site not ready within 180s: {url}', + 'lifecycle.clientExitedEarly': '[reactpress] Site process exited early (pid {pid})', + 'lifecycle.clientReady': '[reactpress] Site ready: {url} (pid {pid})', + 'lifecycle.apiStatusTitle': '[reactpress] API status', + 'lifecycle.source': ' Source: {source}', + 'lifecycle.source.monorepo': 'monorepo server/', + 'lifecycle.source.bundle': 'bundled API (@fecommunity/reactpress)', + 'lifecycle.pidFile': ' PID file: {path}', + 'lifecycle.recordedPid': ' Recorded PID: {pid}', + 'lifecycle.processAlive': ' Process alive: {alive}', + 'lifecycle.httpStatus': ' HTTP ({url}): {status}', + 'lifecycle.httpReachable': 'reachable', + 'lifecycle.httpUnreachable': 'unreachable', + 'lifecycle.unknownCommand': 'Unknown lifecycle command: {command}', + 'lifecycle.processSupervisorFallback': + '[reactpress] Process supervisor unavailable — falling back to background node start', + 'docker.stopping': '[reactpress] Stopping Docker services…', + 'docker.stopped': '[reactpress] Docker services stopped.', + 'docker.stopFailed': '[reactpress] Failed to stop Docker:', + 'docker.starting': '[reactpress] Starting Docker services…', + 'docker.notRunning': 'Docker is not running. Please start Docker Desktop first.', + 'docker.devStartBlocked': + 'MySQL is not reachable on 127.0.0.1:{port} and Docker is not running. Start Docker Desktop, then run: reactpress docker up — or point DB_* in .env to an external MySQL instance.', + 'docker.started': '[reactpress] Docker services started.', + 'docker.waitingMysql': '[reactpress] Waiting for MySQL…', + 'docker.mysqlReady': '[reactpress] MySQL is ready.', + 'docker.mysqlExternalReady': '[reactpress] Using existing MySQL on port {port}.', + 'docker.dbPortInUse': + '[reactpress] Port {port} is already in use — skipping reactpress_db, using existing MySQL on that port.', + 'docker.dbReuseExisting': + '[reactpress] MySQL already reachable on port {port} — keeping Docker DB container', + 'docker.dbPortInUseRecycle': + '[reactpress] Port {port} is in use but MySQL is not reachable — recreating reactpress_db container…', + 'docker.dbPortConflict': + '[reactpress] MySQL on port {port} is unreachable. Run: docker start reactpress_cli_db reactpress_db OR docker compose -f docker-compose.dev.yml up -d db', + 'docker.ensureDevDb': '[reactpress] MySQL not reachable — starting Docker database…', + 'docker.waitingMysqlProgress': '[reactpress] Waiting for MySQL… ({attempts}/{max})', + '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 (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.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': '[{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.stepSkippedFresh': 'skipped {label} (dist up to date)', + 'build.stepSkippedReuse': 'skipped {label} — reusing build for "{id}"', + 'build.done': 'Build finished in {seconds}s', + 'build.label.toolkit': 'Toolkit', + 'build.label.plugins': 'Plugins', + 'build.label.server': 'API (server)', + 'build.label.web': 'Admin (web)', + 'build.label.theme': 'Visitor theme', + 'build.label.docs': 'Documentation (docs)', + 'pm2.startFailed': '[reactpress] PM2 failed to start API:', + 'pm2.exitCode': 'PM2 exited with code {code}', + 'spawn.commandFailed': 'Command failed ({command}): exit code {code}', + 'spawn.exitCode': 'Exit code {code}', + 'shim.deprecated': '\n[deprecated] reactpress-cli will be removed in 3.1. Use instead:\n npm i -g @fecommunity/reactpress\n reactpress init · reactpress dev · reactpress doctor\n', + 'server.help.invokedBy': ' (usually invoked by reactpress server start)', + 'publish.pkg.main': 'ReactPress 3.0 main package — single entry (init / dev / doctor / publish)', + 'publish.pkg.server': 'NestJS backend API (deprecated — use bundled API in reactpress-cli)', + 'bundle.cli.description': 'Zero-config init and manage ReactPress CMS & blog server', + 'bundle.cli.cwd': 'ReactPress project directory (default: current working directory)', + 'bundle.cli.init.description': 'One-click init ReactPress CMS & blog (zero config)', + 'bundle.cli.init.directory': 'Project directory', + 'bundle.cli.init.force': 'Overwrite existing config', + 'bundle.cli.start.description': 'Start server (prepares database automatically)', + 'bundle.cli.stop.description': 'Stop server', + 'bundle.cli.stop.database': 'Also stop embedded database container', + 'bundle.cli.restart.description': 'Restart server', + 'bundle.cli.status.description': 'Server and database status', + 'bundle.cli.config.description': 'View or update config (--apply to restart)', + 'bundle.cli.config.key': 'Config key, e.g. server.port', + 'bundle.cli.config.value': 'New value', + 'bundle.cli.config.list': 'List all config keys', + 'bundle.cli.config.apply': 'Restart automatically after update', + 'bundle.cli.unknownCommand': 'Unknown command: {command}', + 'bundle.cmd.init.spinner': 'Initializing ReactPress project…', + 'bundle.cmd.init.succeed': 'Initialization complete', + 'bundle.cmd.init.fail': 'Initialization failed', + 'bundle.cmd.init.projectDir': 'Project directory: {path}', + 'bundle.cmd.init.nextStep': 'Next: reactpress-cli start', + 'bundle.cmd.notProject': 'This directory is not a ReactPress project.', + 'bundle.cmd.notProjectInit': 'This directory is not a ReactPress project. Run reactpress-cli init first.', + 'bundle.cmd.start.spinner': 'Preparing database and server…', + 'bundle.cmd.start.succeed': 'Server started', + 'bundle.cmd.start.fail': 'Start failed', + 'bundle.cmd.stop.spinner': 'Stopping server…', + 'bundle.cmd.stop.succeed': 'Stopped', + 'bundle.cmd.stop.fail': 'Stop failed', + 'bundle.cmd.restart.spinner': 'Restarting server…', + 'bundle.cmd.restart.succeed': 'Restart complete', + 'bundle.cmd.restart.fail': 'Restart failed', + 'bundle.cmd.status.title': 'Service status', + 'bundle.cmd.status.project': 'Project: {path}', + 'bundle.cmd.status.service': 'Service: {status}{pid}', + 'bundle.cmd.status.running': 'running', + 'bundle.cmd.status.stopped': 'stopped', + 'bundle.cmd.status.url': 'URL: {url}', + 'bundle.cmd.status.database': 'Database: {status} ({mode})', + 'bundle.cmd.status.dbReady': 'ready', + 'bundle.cmd.status.dbNotReady': 'not ready', + 'bundle.cmd.config.title': 'Configuration', + 'bundle.cmd.config.keyRequired': 'Specify a config key, e.g.: reactpress-cli config server.port 3003', + 'bundle.cmd.config.listHint': 'Use --list to show all keys', + 'bundle.cmd.config.updated': 'Updated {key} = {value}', + 'bundle.cmd.config.restartSpinner': 'Restarting to apply config…', + 'bundle.cmd.config.applied': 'Config applied', + 'bundle.cmd.config.restartFail': 'Restart failed', + 'bundle.cmd.config.restartManual': 'Run reactpress-cli restart manually', + 'bundle.cmd.config.applyHint': 'Run reactpress-cli restart or config --apply to apply changes', + 'bundle.service.init.alreadyProject': 'Directory is already a ReactPress project. Use --force to overwrite config.', + 'bundle.service.init.dbPending': 'Project created, but database is not ready: {message}. Run reactpress-cli start later.', + 'bundle.service.init.complete': 'ReactPress project initialized. Run reactpress-cli start to launch the server.', + 'bundle.service.init.templateMissing': 'Template file missing: {path}', + 'bundle.service.config.notFound': 'ReactPress project not found. Run reactpress-cli init first.', + 'bundle.service.config.keyMissing': 'Config key does not exist: {key}', + 'bundle.service.server.alreadyRunning': 'Server already running (PID {pid}), visit {url}', + 'bundle.service.server.portBusy': 'Port {port} is in use; ReactPress cannot bind. If you started via Docker Compose, run: docker stop reactpress_server. Or change server.port in .reactpress/config.json.', + 'bundle.service.server.cannotStart': 'Could not start ReactPress server process.', + 'bundle.service.server.noHttp': 'Server process started (PID {pid}) but no HTTP response at {url}. Check port conflicts or DB_* in .env.', + 'bundle.service.server.started': 'ReactPress server started at {url}', + 'bundle.service.server.stopped': 'ReactPress server stopped.', + 'bundle.service.server.cannotStopPid': 'Could not stop process PID {pid}', + 'bundle.service.database.dockerMissing': 'Docker not detected. Install and start Docker, or set database.mode to external with an existing MySQL.', + 'bundle.service.database.portSwitched': 'Host port {previous} was in use; switched to {port} (.env updated)', + 'bundle.service.database.portBindRetry': 'Port {port} bind failed, trying another port…', + 'bundle.service.database.containerStartFailed': 'Failed to start database container: {detail}', + 'bundle.service.database.credsMismatch': 'Database container is on port {port} but user "{user}" cannot connect (volume credentials differ from .env). Run: cd .reactpress && docker compose down -v && cd .. && reactpress-cli start', + 'bundle.service.database.connectionTimeout': 'Database container started but connection timed out. Run: docker logs {container}', + 'bundle.service.database.cannotConnect': 'Cannot connect to database {host}:{port}. Check DB_* in .env.', + 'bundle.serverBundle.missing': 'Bundled server is missing. Reinstall reactpress-cli.', + 'bundle.serverBundle.installing': 'Installing bundled server runtime dependencies…', + 'bundle.serverBundle.installFailed': 'Bundled server dependency install failed. In the reactpress-cli install dir run: cd server && npm install --omit=dev --no-bin-links', + 'bundle.serverBundle.notBuilt': 'Bundled server is not built or dependencies are incomplete. Run npm run build:server (dev) or reinstall reactpress-cli.', + 'bundle.serverBundle.toolkitMissing': 'Bundled toolkit is missing. Reinstall @fecommunity/reactpress.', + 'bundle.port.notFound': 'No available port in range {start}-{end}', + }, + zh: { + 'cli.description': 'ReactPress 4 — 零依赖发布平台。运行 reactpress init 或 reactpress doctor。', + 'cli.init.description': '初始化并启动 ReactPress(SQLite API + 管理后台 + 主题)', + 'cli.init.directory': '项目目录', + 'cli.init.force': '覆盖已有配置', + 'cli.init.local': '使用嵌入式 SQLite 初始化(无需 Docker)', + 'cli.dev.description': '零配置开发: 环境检查 + toolkit 构建 + API + 前端', + 'cli.dev.apiOnly': '仅启动 API (watch)', + 'cli.dev.local': '本地 SQLite 模式(无需 Docker/nginx)', + 'cli.dev.clientOnly': '仅启动前端', + 'cli.dev.webOnly': '管理后台 + API (web/)', + 'cli.dev.remoteOrigin': '默认远程 API;未指定 admin/client 时两者均走远程', + 'cli.dev.adminOrigin': '管理后台 API:local | remote | URL(remote 用 --remote-origin 默认值)', + 'cli.dev.clientOrigin': '访客站 API(nginx /api):local | remote | URL', + 'cli.dev.remoteOriginRequired': '--remote-origin 需要填写地址(如 api.gaoredu.com)', + 'cli.dev.remoteDefaultRequired': 'remote 需配合 URL:使用 --remote-origin 或为 admin/client-origin 填写地址', + 'cli.dev.invalidOrigin': '无效的 origin;请使用 local、remote 或主机/URL', + 'cli.dev.remoteOriginIncompatibleApiOnly': '远程 API 参数不能与 --api-only 同时使用', + 'cli.desktopDev.description': '桌面开发:内嵌 SQLite API + 管理后台 + Electron(无需 Docker/MySQL)', + 'cli.server.description': '管理 API 服务', + 'cli.server.start.description': '启动 API(等待 HTTP 就绪)', + 'cli.server.start.pm2': '使用 PM2 启动(生产)', + 'cli.server.start.bg': '后台启动,不等待 HTTP', + 'cli.server.stop': '停止 API', + 'cli.server.restart': '重启 API', + 'cli.server.status': '查看 API 状态', + 'cli.client.description': '管理前端', + 'cli.client.start': '启动 Next.js 客户端', + 'cli.client.start.pm2': '使用 PM2 启动', + 'cli.client.restart': '重新构建当前主题并重启访客端', + 'themeProd.building': '正在构建当前主题「{id}」…', + 'themeProd.installingDeps': '正在为主题「{id}」安装依赖…', + 'themeProd.reusingBuild': '复用主题「{id}」已有构建,跳过重新构建', + 'themeProd.restarting': '正在为主题「{id}」重启访客端…', + 'themeProd.restarted': '访客端已切换为主题「{id}」。', + 'themePreview.backgroundBuildScheduled': '正在后台构建 {count} 个预览主题(生产模式)…', + 'themePreview.warmingAll': '正在预构建 {count} 个主题预览(加速切换)…', + 'themePreview.warmingAllSkipped': '{count} 个预览构建已是最新,已跳过', + 'themePreview.installingDeps': '正在为预览主题「{id}」安装依赖…', + 'themePreview.building': '正在构建预览主题「{id}」…', + 'themePreview.reusingBuild': '复用预览主题「{id}」已有构建,跳过重新构建', + 'themePreview.buildDone': '预览主题「{id}」构建完成。', + 'themePreview.buildFailed': '预览主题「{id}」构建失败:{message}', + 'themePreview.starting': '预览主题「{id}」→ {url}(端口 {port},{dir},{mode})', + 'themePreview.ready': '预览已就绪:{url}(主题:{id})', + 'cli.build.description': '构建生产产物', + 'cli.docker.description': 'Docker 开发环境 (MySQL + nginx)', + 'cli.docker.up': '仅启动 Docker 服务并等待 MySQL', + 'cli.docker.down': '停止 Docker 服务', + 'cli.docker.start': '启动 Docker + 全栈开发 (API + 前端)', + '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.doctor.showLogs': '诊断结束后显示最近的 API 错误日志', + 'cli.doctor.logs.description': '查看 API 日志(error / request / response)', + 'cli.doctor.logs.tailOption': '每个日志文件显示的行数', + 'cli.doctor.logs.sourceOption': '日志类型:error、request、response 或 all', + 'cli.doctor.logs.grepOption': '用正则过滤日志行', + 'cli.doctor.logs.listOption': '仅列出可用日志文件', + 'cli.logs.description': '查看 API 日志(PM2 进程输出或结构化 error/request/response)', + 'cli.logs.descriptionAlias': '查看 API 日志(同 reactpress logs)', + 'cli.logs.tailOption': '每个日志文件显示的行数', + 'cli.logs.sourceOption': '日志类型:process(PM2)、error、request、response 或 all', + 'cli.logs.grepOption': '用正则过滤日志行', + 'cli.logs.listOption': '仅列出可用日志文件', + 'cli.logs.followOption': '持续输出日志(PM2 托管时用 pm2 logs,否则 tail -f)', + 'cli.stop.description': '停止 init 启动的 API 与站点服务', + 'cli.db.description': '数据库运维', + 'cli.db.backup': '使用 mysqldump 备份当前项目数据库', + 'cli.db.backup.output': '输出 SQL 文件路径', + 'cli.publish.description': '构建并发布 npm 包', + 'cli.publish.build': '仅构建发布产物', + 'cli.publish.publish': '发布核心 npm 包', + 'cli.start.description': '启动 ReactPress(SQLite API + 管理后台 + 主题)', + 'cli.help.examples': '用法:', + 'cli.help.init': ' reactpress init [dir] 初始化项目', + 'cli.help.doctor': ' reactpress doctor 诊断环境问题', + 'cli.help.logs': ' reactpress logs 查看 API 日志,快速定位问题', + 'cli.help.stop': ' reactpress stop 停止后台 API', + 'cli.help.doctorLogs': ' reactpress doctor logs reactpress logs 的别名', + 'cli.help.zeroDep': ' 零依赖:仅需 Node.js 18+,无需 Docker、nginx 或 MySQL', + 'cli.plugin.description': '管理 ReactPress 插件', + 'cli.plugin.install.description': '安装本地插件到 .reactpress/plugins', + 'cli.plugin.install.id': 'plugins/ 注册表中的插件 id', + 'cli.plugin.list.description': '列出已注册插件', + 'cli.theme.description': '安装与管理主题', + 'cli.theme.add.description': '从 npm 包 spec 或 .tgz 文件安装主题', + 'cli.theme.add.spec': 'npm 包 spec(如 @fecommunity/reactpress-theme-starter@1.0.0-beta.0)', + 'cli.theme.add.catalog': '按 themes/{dir}/package.json 中的主题 id 安装', + 'cli.theme.add.skipDeps': '跳过主题目录内的 pnpm/npm install', + 'cli.theme.list.description': '列出可用主题包', + 'themeInstall.specRequired': '请提供 npm 主题包 spec', + 'themeInstall.installing': '正在从 npm 安装主题: {spec}', + 'themeInstall.success': '主题「{name}」已安装为「{id}」→ {dir}', + 'themeInstall.nextActivate': '请在管理后台「外观 → 主题」中启用,或调用 POST /extension/themes/{id}/activate', + 'themeInstall.listHeading': '可用主题:', + 'themeInstall.listEmpty': '在 themes/ 或 .reactpress/runtime/ 下未找到主题包', + 'cli.build.target': '构建目标: toolkit | plugins | server | web | theme | docs | all', + 'cli.build.lowMem': '低内存模式:限制构建堆内存并跳过未变化步骤(2G 小机)', + 'init.readyTitle': 'ReactPress 已启动', + 'init.label.path': '路径', + 'init.label.db': '数据库', + 'init.label.site': '站点', + 'init.label.admin': '后台', + 'init.label.api': 'API', + 'init.hint': '账号 admin / admin · 服务已在后台运行 · 停止:reactpress stop · 日志:reactpress logs', + 'init.alreadyInitialized': '项目已初始化 — 正在确保服务运行', + 'init.apiOnlyHint': 'API 已运行。请在管理后台「外观 → 主题」安装主题以启用访客站。', + 'init.pluginsSeeded': '已写入内置插件(hello-world、seo、image-optimizer)。', + 'init.unknownCommand': '未知命令:{cmd}', + 'init.useInitOnly': 'ReactPress 4 支持:reactpress init · doctor · logs · stop', + 'banner.subtitle': '· 发布平台 · v4.0', + 'banner.tagline': '零依赖 · SQLite · 约 60 秒上线', + /** 左侧装饰性进度条标签(不是网址;仓库地址放在卡片顶部 Title 正下方)。 */ + 'banner.pulseLabel': '准备', + 'banner.pulseReady': '就绪', + 'banner.pulsePending': '初始化', + 'banner.label.mode': '模式', + 'banner.label.path': '路径', + 'banner.service.sqlite': 'SQLite', + 'banner.service.mysql': 'MySQL', + 'banner.service.server': 'Server', + 'banner.service.docker': 'Docker', + 'banner.service.nginx': 'Nginx', + 'banner.service.web': 'Web', + 'banner.nginxRunning': '运行中', + 'banner.nginxStopped': '未运行', + 'banner.feat.sqlite': 'SQLite', + 'banner.feat.plugins': '插件', + 'banner.feat.desktop': 'Desktop', + 'banner.feat.themes': '主题', + 'banner.mode.standalone': '独立项目', + 'banner.mode.monorepo': 'MONOREPO', + 'banner.mode.uninitialized': '未初始化', + 'banner.systemLabel': '系统', + 'banner.systemOnline': '在线', + 'banner.systemPartial': '部分就绪', + 'banner.systemError': '异常', + 'banner.systemPending': '准备中', + 'menu.dev': '全栈开发', + 'menu.init': '初始化项目', + 'menu.status': '查看项目状态', + 'menu.doctor': '环境检查', + 'menu.devApi': '仅 API (watch)', + 'menu.devClient': '仅前端', + 'menu.serverStart': '启动 API (生产)', + 'menu.serverStop': '停止 API', + 'menu.serverRestart': '重启 API', + 'menu.build': '生产构建', + 'menu.buildTarget': '选择要构建的目标', + 'menu.buildAll': '全部 (toolkit → server → client)', + 'menu.dockerStart': 'Docker 开发环境 (DB + nginx + 全栈)', + 'menu.dockerUp': 'Docker 仅启动数据库', + 'menu.dockerStop': '停止 Docker 服务', + 'menu.openAdmin': '在浏览器打开管理后台', + 'menu.publish': '发布 npm 包 (交互式)', + 'menu.exit': '退出', + 'menu.prompt': '选择操作', + 'menu.subPrompt': '{section}', + 'menu.backToMain': '返回主菜单', + 'menu.back': '返回主菜单?', + 'menu.retry': '返回主菜单重试?', + 'menu.startingDev': '启动全栈开发…', + 'menu.initProject': '初始化项目…', + 'menu.done': '完成', + 'menu.opening': '打开 {url}', + 'menu.goodbye': ' 再见。', + 'menu.section.run': '运行', + 'menu.section.quick': '快速开始', + 'menu.section.explore': '更多', + 'menu.section.extend': '开发替代方案', + 'menu.section.lifecycle': '服务控制', + 'menu.section.build': '构建与部署', + 'menu.section.tools': '工具', + 'menu.group.extend': '开发替代方案', + 'menu.group.lifecycle': '服务控制', + 'menu.group.build': '构建与部署', + 'menu.group.tools': '工具与实用功能', + 'menu.groupHint.extend': '桌面 · Web · SQLite · 主题', + 'menu.groupHint.lifecycle': 'API · 前端 · 启停', + 'menu.groupHint.build': '构建 · Docker · 发布', + 'menu.groupHint.tools': 'nginx · 备份 · 后台', + 'menu.devDesktop': '桌面开发', + 'menu.hint.devDesktop': 'SQLite + Electron', + 'menu.devWeb': '管理后台 + API', + 'menu.hint.devWeb': 'web/ 开发', + 'menu.devLocalWeb': '本地 Web', + 'menu.hint.devLocalWeb': '浏览器 + SQLite', + 'menu.initLocal': 'SQLite 初始化', + 'menu.hint.initLocal': '无需 Docker', + 'menu.themeList': '列出主题', + 'menu.hint.themeList': 'theme list', + 'menu.pluginList': '列出插件', + 'menu.hint.pluginList': 'plugin list', + 'menu.dbBackup': '备份数据库', + 'menu.hint.dbBackup': 'mysqldump', + 'menu.tip': '提示:上下方向键选择,回车确认,Ctrl+C 退出。', + 'menu.shortcuts': '↑/↓ 选择 · 1-9 快捷 · 回车确认 · 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.phaseApi': 'API → :3002', + 'dev.phasePrerequisites': '检查 Node.js 与 Docker…', + 'dev.phaseInfra': '启动 MySQL 与 nginx…', + 'dev.phaseServices': '启动 API、管理后台与主题…', + 'dev.phasePrerequisitesDesktop': '检查 Node.js…', + 'dev.phaseInfraDesktop': '构建 toolkit 与本地工作区…', + 'dev.phaseServicesDesktop': '启动管理后台与 Electron…', + 'dev.phaseServicesLocalWeb': '启动管理后台(浏览器预览)…', + 'dev.previewPrewarmStarting': '预构建主题预览以加速切换…', + 'dev.remoteApiUsing': '使用远程 API(nginx /api → {url})', + 'dev.adminApiRemote': '管理后台 API → {url}', + 'dev.clientApiRemote': '访客站 API(nginx /api)→ {url}', + 'dev.nginxReadyRemote': 'nginx 已就绪:{url}(访客站 API → {api})', + 'dev.checkNodeOk': 'Node.js {version}', + 'dev.checkDockerOk': 'Docker 已运行', + 'dev.prerequisitesOk': '✓ Node {version} · ✓ Docker', + 'dev.prerequisitesOkDesktop': '✓ Node {version} · SQLite(无需 Docker)', + 'dev.apiKept': '复用端口 {port} 上已健康的 API', + 'dev.timingReady': '启动完成 · {summary}', + 'dev.timingInfra': '基础设施', + 'dev.timingServices': '服务', + 'dev.timingApiReused': 'API 已复用', + 'dev.waitingApiQuiet': '等待 API…', + 'dev.mysqlReadyQuiet': 'MySQL 已就绪', + 'dev.themeStarting': '主题「{id}」→ :{port}', + 'dev.themeCacheClearedForRemote': '已清除主题 .next(远程 API,避免陈旧 SERVER_API_URL)', + 'dev.themeReadyQuiet': '访客站已就绪 → {url}', + 'dev.phaseAdmin': '启动管理后台(内部 :3000/admin/)…', + 'dev.phaseTheme': '启动当前启用主题(内部 :3001)…', + 'dev.phaseClient': '启动旧版 client 前端…', + 'dev.phaseNginx': '启动 nginx 统一入口(:80)…', + 'dev.phaseNginxWait': '等待管理端与主题就绪后启动 nginx…', + 'dev.waitingProxies': '等待管理后台与主题…', + 'dev.startingAdmin': '管理后台 → {url}', + 'dev.adminNginxSlow': '[reactpress] 经 nginx 访问管理端未就绪: {url} — 请确认 Vite base 为 /admin/(重新执行 pnpm dev)', + 'dev.nginxReady': 'Nginx → {url}', + 'dev.proxiesReady': '管理后台与主题开发服务已监听', + 'dev.portApiBusy': '[reactpress] 端口 {port} 已被占用(且 API 健康检查未通过)。请结束占用进程或运行: reactpress doctor', + 'dev.portApiBusyHint': '[reactpress] 提示: lsof -i :3002 · 请勿在两个终端同时运行 pnpm dev', + 'dev.startingApi': '启动 API(端口 3002)…', + 'dev.waitingApi': '等待 API: {url}', + 'dev.waitingApiCompile': '编译中(端口 {port} 尚未监听)', + 'dev.waitingApiStarting': '端口已开,等待健康检查', + 'dev.healthDbDown': '数据库未连接', + 'dev.healthDegraded': 'API 降级', + 'dev.apiTimeout': '[reactpress] API 在 {seconds}s 内未就绪。\n → 运行 reactpress doctor 查看详情\n → 嵌入式 MySQL:reactpress docker up\n → 检查 .env 中 DB_* 与 SERVER_SITE_URL', + 'dev.apiReusing': 'API :{port} 已健康,跳过重启', + 'dev.apiReady': 'API 已就绪', + 'dev.toolkitUpToDate': '已跳过 toolkit 构建(dist 为最新;设置 REACTPRESS_FORCE_TOOLKIT_BUILD=1 可强制重建)', + 'dev.themeSiteSkipped': '访客站(:3001)需在主题包就绪后才会启动', + 'dev.themeBackground': '访客站(:3001)后台编译中 — API 与管理端就绪后将显示启动横幅', + 'dev.themeBackgroundReady': '访客站已就绪: {url}', + 'themeDev.starting': '[reactpress] 已启用主题「{id}」→ {url}(端口 {port},{dir})', + 'themeDev.startingShort': '主题「{id}」→ :{port}({dir})', + 'themeDev.cacheCleared': '已清理主题 .next(REACTPRESS_CLEAR_THEME_CACHE=1)', + 'themeDev.cacheStaleCleared': '已清理主题 .next(缺少 {marker})', + 'themeDev.apiSplit': '[reactpress] 主题 API — 服务端渲染: {ssr} · 浏览器: {browser}', + 'themeDev.ready': '[reactpress] 访客站已就绪: {url}(主题: {id})', + 'themeDev.slow': '[reactpress] 主题站点启动较慢: {url}', + 'themeDev.notFound': '未找到主题包「{id}」', + 'themeDev.invalidManifest': + '已忽略无效的 active-theme.json(仅 themes/ 或 .reactpress/runtime/ 下的主题包会生效)', + 'themeDev.unavailable': '无法监听', + 'themeDev.restart': 'active-theme.json 已更新,正在重启 :3001 主题进程…', + 'themeDev.restartFailed': '主题重启失败: {message}', + 'themeDev.portBusy': '端口 {port} 仍被占用,已跳过本次主题重启(请再次启用主题或重启 pnpm dev)', + 'themeDev.portBusyHint': '也可手动释放端口: {cmd}', + 'dev.apiReadyAdmin': 'API 已就绪 · 启动管理后台', + 'dev.clientSlow': '[reactpress] 前端在 {seconds}s 内未响应,可能仍在编译。稍后访问 {url}', + 'dev.adminSlow': '[reactpress] 管理后台在 {seconds}s 内未响应,可能仍在编译。稍后访问 {url}', + 'dev.noWeb': '[reactpress] 未找到 web/ 目录,无法启动管理后台开发栈。', + 'dev.envFailed': '[reactpress] 环境准备失败:', + 'dev.toolkitFailed': 'toolkit 构建失败,退出码: {code}', + 'dev.nextSteps': '下一步建议:', + 'dev.nextDoctor': ' → reactpress doctor 环境诊断', + 'dev.nextDocker': ' → reactpress docker up 启动嵌入式 MySQL', + 'dev.nextEnv': ' → 检查 .env 中 DB_* 与 SERVER_SITE_URL', + 'dev.standaloneHint': '[reactpress] 独立项目:当前目录仅启动 API,前端请单独构建。', + 'dev.desktopStarting': '正在启动 Electron 桌面客户端 → {url}', + 'dev.desktopMissing': '未找到 desktop/ 包 — 请在 monorepo 根目录运行', + 'dev.desktopIntro': '桌面开发 — 本地优先模式(内嵌 SQLite,无需 Docker/MySQL)', + 'dev.localWebIntro': '本地 Web 开发 — SQLite API + 浏览器管理后台(无需 Docker/Electron)', + 'dev.localFullIntro': '本地全栈开发 — SQLite API + 管理后台 + 访客主题(无需 Docker/MySQL)', + 'dev.autoLocalNoDocker': 'Docker 不可用,已自动切换为本地 SQLite 模式(无需 MySQL)', + 'dev.autoLocalNoMysql': 'MySQL 未就绪,已自动切换为本地 SQLite 模式', + 'dev.desktopLocalApiStarting': '正在启动内嵌 SQLite API…', + 'dev.desktopLocalApiReady': '✓ 本地 API({db})→ {url}', + 'dev.desktopLocalApi': '本地 SQLite API → {url}', + 'dev.dbTypeSqlite': 'SQLite', + 'devBanner.ready': 'ReactPress 开发环境已就绪', + 'devBanner.readyWeb': 'ReactPress 管理后台开发环境已就绪', + 'devBanner.readyLocalWeb': 'ReactPress 本地 Web 开发环境已就绪', + 'devBanner.readyDesktop': 'ReactPress 桌面开发环境已就绪', + 'devBanner.readyApi': 'ReactPress API 已就绪', + 'devBanner.site': '前台', + 'devBanner.admin': '管理端', + 'devBanner.api': 'API', + 'devBanner.database': '数据库', + 'devBanner.sqliteEmbedded': 'SQLite(内嵌,无需 Docker)', + 'devBanner.mysqlDocker': 'MySQL(Docker)', + 'devBanner.swagger': 'Swagger', + 'devBanner.health': '健康检查', + 'devBanner.desktopLocalHint': '默认账号 admin/admin · 可在设置中切换远程 API 或同步', + 'devBanner.localWebHint': '在浏览器打开管理端地址 · 默认账号 admin/admin', + 'devBanner.localModeGo': '本地模式就绪', + 'devBanner.hint': '诊断: reactpress doctor · 状态: reactpress status', + 'devBanner.nginxHint': '内部端口:3001 访客站 / 3002 API / 3003 主题预览 / 3000 管理后台 — 请只使用上方地址', + 'devBanner.nginxRemoteHint': '访客站 /api 已代理至 {url}', + 'devBanner.adminRemoteHint': '管理后台 /api 已代理至 {url}', + 'devBanner.shortcuts': 'Ctrl+C 停止', + 'devBanner.allSystemsGo': '一切就绪', + 'devBanner.dbDegraded': '数据库未连接 — 请执行 reactpress docker up', + 'dev.nginxSkippedDocker': '[reactpress] Docker 未运行,已跳过 nginx(请启动 Docker 或直连各端口)', + 'dev.nginxStartFailed': '[reactpress] nginx 启动失败: {message}', + 'dev.dbEnsureFailed': '[reactpress] 数据库未就绪: {message}', + 'dev.mysqlUnreachable': + '[reactpress] MySQL 不可达,主题/后台接口会报错。请先启动 Docker,再执行: reactpress docker up', + 'dev.nginxSlow': '[reactpress] nginx 入口响应较慢: {url}', + 'doctor.nodeBad': 'Node.js {version}(需要 ≥ 18)', + 'doctor.nodeFix': '请安装 Node.js 18+:https://nodejs.org/', + 'doctor.dockerOk': 'Docker 引擎可用', + 'doctor.dockerBad': 'Docker 未运行或不可用', + 'doctor.dockerSkipped': '无需 Docker(SQLite 本地模式)', + 'doctor.dockerFix': '安装并启动 Docker:https://docs.docker.com/get-docker/ ,然后运行 reactpress docker up;或改 config.json 使用外部 MySQL', + 'doctor.portApiBusy': 'API 端口 {port} 已被占用', + 'doctor.portClientBusy': '前端端口 {port} 已被占用', + 'doctor.portFix': '修改 .env 中 SERVER_PORT / CLIENT_PORT,或停止占用进程', + 'doctor.portOk': '端口 {apiPort}(API)、{clientPort}(前端)可用', + 'doctor.dbNoMysql2': '未安装 mysql2,无法检测数据库', + 'doctor.dbMysql2Fix': '在 monorepo 根目录执行 pnpm install', + 'doctor.dbOk': 'MySQL {host}:{port}/{database} 连通', + 'doctor.dbBad': '数据库连接失败: {error}', + 'doctor.dbFix': '运行 reactpress docker up 或检查 .env 中 DB_* 配置', + 'doctor.dbSqliteOk': 'SQLite 就绪 ({detail})', + 'doctor.dbSqliteBad': 'SQLite 检测失败: {error}', + 'doctor.dbSqliteFix': '运行 reactpress init --local 或检查 .env 中 DB_DATABASE', + 'doctor.apiOk': 'API 健康检查通过 ({url})', + 'doctor.apiBad': 'API 未响应健康检查 ({url})', + 'doctor.apiFix': '运行 reactpress server start 或 reactpress dev', + 'doctor.apiFixInit': '运行 reactpress init 启动 API', + 'doctor.pnpmBad': '未检测到 pnpm', + 'doctor.pnpmFix': 'npm i -g pnpm,或在 monorepo 根目录使用 corepack enable', + 'doctor.pnpmSkipped': 'pnpm 可选(独立项目 / 内置运行时)', + 'doctor.check.config': '配置文件', + 'doctor.check.env': '环境变量', + 'doctor.check.ports': '端口', + 'doctor.check.database': '数据库', + 'doctor.check.api': 'API 健康', + 'doctor.check.site': '访客站点', + 'doctor.check.admin': '管理后台', + 'doctor.siteOk': '站点可访问 {url}', + 'doctor.siteBad': '站点不可访问 {url}', + 'doctor.siteFix': '运行 reactpress init 启动主题前台', + 'doctor.adminOk': '后台可访问 {url}', + 'doctor.adminBad': '后台不可访问 {url}', + 'doctor.adminFix': '运行 reactpress init 后访问 {url}', + 'doctor.configOk': '.reactpress/config.json 存在', + 'doctor.configBad': '缺少 .reactpress/config.json', + 'doctor.configFix': '运行 reactpress init', + 'doctor.envOk': '.env 存在', + 'doctor.envBad': '缺少 .env', + 'doctor.envFix': '运行 reactpress init 或 reactpress config --apply', + 'doctor.project': '项目目录 {path}', + 'doctor.allPass': '全部检查通过,可以开始开发。', + 'doctor.failed': '{count} 项需要处理。', + 'doctor.title': 'ReactPress Doctor', + 'doctor.subtitle': '环境健康检查', + 'doctor.checking': '正在检查 {name}…', + 'doctor.summary': '通过 {passed} · 失败 {failed} · 共 {total} 项', + 'doctor.fixesHeader': '修复建议', + 'doctor.logsHint': '提示:运行 reactpress logs --tail 100 查看 API 错误日志', + 'logs.title': 'ReactPress 日志', + 'logs.subtitle': 'API 日志查看', + 'doctor.logs.title': 'ReactPress 日志', + 'doctor.logs.subtitle': 'API 日志查看', + 'doctor.logs.dir': '日志目录 {path}', + 'doctor.logs.empty': '尚未找到 API 日志文件。', + 'doctor.logs.emptyHint': + '运行 reactpress init 启动 API 后,日志写入 .reactpress/logs/server/(PM2 输出:pm2-out.log / pm2-error.log)', + 'doctor.logs.listHeader': '可用日志文件', + 'doctor.logs.fileHeader': '{file}(显示 {count} 行)', + 'doctor.logs.noMatchingLines': '没有匹配的日志行。', + 'doctor.logs.badPattern': 'grep 正则无效:{pattern}', + 'doctor.logs.noPid': '未找到 API pid 文件:{path}', + 'doctor.logs.activePid': 'API 正在监听端口 {port}(pid {pid})', + 'doctor.logs.pm2Supervised': 'API 由 PM2 托管(pid {pid})', + 'doctor.logs.pm2Unavailable': '无法使用 PM2 实时输出日志。', + 'doctor.logs.followWindows': '当前环境不支持持续跟踪,请直接打开:{path}', + 'doctor.logs.pidRunning': 'API 进程 pid {pid}(记录在 {path})', + 'doctor.logs.pidStale': 'pid 文件 {path} 中的 {pid} 已失效(进程未运行)', + 'doctor.logs.moreHint': + '可用 --tail、--source、--grep、--follow、--list 进一步筛选(见 reactpress logs --help)', + 'status.title': 'ReactPress 项目状态', + 'status.dir': '项目目录 {path}', + 'status.apiSource': 'API 来源 {source}', + 'status.apiSource.monorepo': 'monorepo server/', + 'status.apiSource.bundle': '@fecommunity/reactpress', + 'status.configOk': '.reactpress/config.json', + 'status.configBad': '未初始化', + 'status.envOk': '.env', + 'status.envBad': '缺少 .env', + 'status.apiOnline': '在线', + 'status.apiOffline': '离线', + 'status.apiUnreachable': '{url} (离线或未启动)', + 'status.dbUp': '连通', + 'status.dbDown': '不可用', + 'status.pidRunning': '(运行中)', + '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 开发环境已就绪(配置 + 数据库)。', + 'bootstrap.initFailed': '初始化失败', + 'bootstrap.cliInitFailed': 'reactpress-cli init 失败', + 'bootstrap.dbPendingShort': '数据库未就绪', + 'bootstrap.dbNotReady': '{message}。建议:启动 Docker 后运行 reactpress docker up,或执行 reactpress doctor', + 'bootstrap.dbReady': '数据库已就绪', + 'db.backup.to': '备份数据库到 {path}', + 'db.backup.done': '备份完成', + 'db.backup.viaDocker': '本机未找到 mysqldump,改用 Docker 内 db 容器的 mysqldump…', + 'db.backup.fail': + 'mysqldump 失败:请安装 MySQL 客户端(如 brew install mysql-client),或确保 Docker 数据库已运行以便自动在容器内备份', + 'common.done': '完成', + 'common.yes': '是', + 'common.no': '否', + 'common.none': '(无)', + 'common.unknownError': '未知错误', + 'lifecycle.apiStopped': '[reactpress] 已停止 API 进程 (pid {pid})', + 'lifecycle.stopPidFailed': '[reactpress] 停止 pid {pid} 失败:', + 'lifecycle.apiAlreadyRunning': '[reactpress] API 已在运行 (pid {pid})', + '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.apiExitedEarly': '[reactpress] API 进程已提前退出 (pid {pid})', + 'lifecycle.apiReady': '[reactpress] API 已就绪: {url}', + 'lifecycle.clientStopped': '[reactpress] 已停止站点进程 (pid {pid})', + 'lifecycle.stopClientPidFailed': '[reactpress] 停止站点 pid {pid} 失败:', + 'lifecycle.clientAlreadyRunning': '[reactpress] 站点已在运行 (pid {pid})', + 'lifecycle.clientPidMissing': '[reactpress] 站点启动失败(未记录 pid)', + 'lifecycle.waitingClient': '等待站点就绪 {url}…', + 'lifecycle.clientTimeout': '[reactpress] 站点在 180s 内未就绪: {url}', + 'lifecycle.clientExitedEarly': '[reactpress] 站点进程已提前退出 (pid {pid})', + 'lifecycle.clientReady': '[reactpress] 站点已就绪: {url} (pid {pid})', + 'lifecycle.apiStatusTitle': '[reactpress] API 状态', + 'lifecycle.source': ' 来源: {source}', + 'lifecycle.source.monorepo': 'monorepo server/', + 'lifecycle.source.bundle': '内置 API (@fecommunity/reactpress)', + 'lifecycle.pidFile': ' PID 文件: {path}', + 'lifecycle.recordedPid': ' 记录 PID: {pid}', + 'lifecycle.processAlive': ' 进程存活: {alive}', + 'lifecycle.httpStatus': ' HTTP ({url}): {status}', + 'lifecycle.httpReachable': '可访问', + 'lifecycle.httpUnreachable': '不可访问', + 'lifecycle.unknownCommand': '未知 lifecycle 命令: {command}', + 'lifecycle.processSupervisorFallback': + '[reactpress] 进程守护不可用 — 已回退到后台 Node 启动', + 'docker.stopping': '[reactpress] 正在停止 Docker 服务…', + 'docker.stopped': '[reactpress] Docker 服务已停止。', + 'docker.stopFailed': '[reactpress] 停止 Docker 失败:', + 'docker.starting': '[reactpress] 正在启动 Docker 服务…', + 'docker.notRunning': 'Docker 未运行,请先启动 Docker Desktop。', + 'docker.devStartBlocked': + '无法连接 127.0.0.1:{port} 上的 MySQL,且 Docker 未运行。请先启动 Docker Desktop,再执行:reactpress docker up — 或在 .env 中将 DB_* 指向已有 MySQL 实例。', + 'docker.started': '[reactpress] Docker 服务已启动。', + 'docker.waitingMysql': '[reactpress] 等待 MySQL 就绪…', + 'docker.mysqlReady': '[reactpress] MySQL 已就绪。', + 'docker.mysqlExternalReady': '[reactpress] 已使用端口 {port} 上的现有 MySQL。', + 'docker.dbPortInUse': + '[reactpress] 端口 {port} 已被占用 — 跳过 reactpress_db,改用该端口上的现有 MySQL。', + 'docker.dbReuseExisting': + '[reactpress] 端口 {port} 上 MySQL 已可用 — 保留 Docker 数据库容器', + 'docker.dbPortInUseRecycle': + '[reactpress] 端口 {port} 被占用且 MySQL 不可达 — 正在重建 reactpress_db 容器…', + 'docker.dbPortConflict': + '[reactpress] 端口 {port} 上的 MySQL 不可达。请执行:docker start reactpress_cli_db reactpress_db 或 docker compose -f docker-compose.dev.yml up -d db', + 'docker.ensureDevDb': '[reactpress] MySQL 不可达 — 正在启动 Docker 数据库…', + 'docker.waitingMysqlProgress': '[reactpress] 等待 MySQL… ({attempts}/{max})', + 'docker.mysqlTimeout': '[reactpress] MySQL 在超时时间内未就绪。', + 'docker.mysqlNotReady': 'MySQL 未就绪', + 'docker.startDevStack': '[reactpress] 启动 API + 前端 (Docker MySQL)…', + '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.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': '[{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.stepSkippedFresh': '已跳过 {label}(dist 已是最新)', + 'build.stepSkippedReuse': '已跳过 {label} — 复用主题「{id}」已有构建', + 'build.done': '构建完成,耗时 {seconds}s', + 'build.label.toolkit': 'Toolkit', + 'build.label.plugins': '插件 (plugins)', + 'build.label.server': 'API (server)', + 'build.label.web': '管理后台 (web)', + 'build.label.theme': '访客主题 (theme)', + 'build.label.docs': '文档 (docs)', + 'pm2.startFailed': '[reactpress] PM2 启动 API 失败:', + 'pm2.exitCode': 'PM2 退出码 {code}', + 'spawn.commandFailed': '命令失败 ({command}): 退出码 {code}', + 'spawn.exitCode': '退出码 {code}', + 'shim.deprecated': '\n[deprecated] reactpress-cli 将在 3.1 移除。请改用:\n npm i -g @fecommunity/reactpress\n reactpress init · reactpress dev · reactpress doctor\n', + 'server.help.invokedBy': ' (通常由 reactpress server start 调用)', + 'publish.pkg.main': 'ReactPress 3.0 主包 — 唯一入口 (init / dev / doctor / publish)', + 'publish.pkg.server': 'NestJS 后端 API (deprecated — 使用 reactpress-cli 内置 API)', + 'bundle.cli.description': '零配置初始化与管理 ReactPress CMS & 博客服务器', + 'bundle.cli.cwd': 'ReactPress 项目目录(默认:当前工作目录)', + 'bundle.cli.init.description': '一键初始化 ReactPress CMS & 博客服务器(零配置)', + 'bundle.cli.init.directory': '项目目录', + 'bundle.cli.init.force': '覆盖已有配置', + 'bundle.cli.start.description': '启动服务器(自动准备数据库)', + 'bundle.cli.stop.description': '停止服务器', + 'bundle.cli.stop.database': '同时停止嵌入式数据库容器', + 'bundle.cli.restart.description': '重启服务器', + 'bundle.cli.status.description': '查看服务与数据库状态', + 'bundle.cli.config.description': '查看或更新配置(更新后可用 --apply 重启生效)', + 'bundle.cli.config.key': '配置键,如 server.port', + 'bundle.cli.config.value': '新值', + 'bundle.cli.config.list': '列出所有配置', + 'bundle.cli.config.apply': '更新后自动重启服务', + 'bundle.cli.unknownCommand': '未知命令: {command}', + 'bundle.cmd.init.spinner': '正在初始化 ReactPress 项目…', + 'bundle.cmd.init.succeed': '初始化完成', + 'bundle.cmd.init.fail': '初始化失败', + 'bundle.cmd.init.projectDir': '项目目录: {path}', + 'bundle.cmd.init.nextStep': '下一步: reactpress-cli start', + 'bundle.cmd.notProject': '当前目录不是 ReactPress 项目。', + 'bundle.cmd.notProjectInit': '当前目录不是 ReactPress 项目。请先运行 reactpress-cli init。', + 'bundle.cmd.start.spinner': '正在准备数据库与服务…', + 'bundle.cmd.start.succeed': '服务已启动', + 'bundle.cmd.start.fail': '启动失败', + 'bundle.cmd.stop.spinner': '正在停止服务…', + 'bundle.cmd.stop.succeed': '已停止', + 'bundle.cmd.stop.fail': '停止失败', + 'bundle.cmd.restart.spinner': '正在重启服务…', + 'bundle.cmd.restart.succeed': '重启完成', + 'bundle.cmd.restart.fail': '重启失败', + 'bundle.cmd.status.title': '服务状态', + 'bundle.cmd.status.project': '项目: {path}', + 'bundle.cmd.status.service': '服务: {status}{pid}', + 'bundle.cmd.status.running': '运行中', + 'bundle.cmd.status.stopped': '已停止', + 'bundle.cmd.status.url': '地址: {url}', + 'bundle.cmd.status.database': '数据库: {status} ({mode})', + 'bundle.cmd.status.dbReady': '就绪', + 'bundle.cmd.status.dbNotReady': '未就绪', + 'bundle.cmd.config.title': '配置项', + 'bundle.cmd.config.keyRequired': '请指定配置键,例如: reactpress-cli config server.port 3003', + 'bundle.cmd.config.listHint': '使用 --list 查看所有配置项', + 'bundle.cmd.config.updated': '已更新 {key} = {value}', + 'bundle.cmd.config.restartSpinner': '正在重启以使配置生效…', + 'bundle.cmd.config.applied': '配置已应用', + 'bundle.cmd.config.restartFail': '重启失败', + 'bundle.cmd.config.restartManual': '请手动运行 reactpress-cli restart', + 'bundle.cmd.config.applyHint': '运行 reactpress-cli restart 或 config --apply 使配置生效', + 'bundle.service.init.alreadyProject': '目录已是 ReactPress 项目。使用 --force 覆盖配置。', + 'bundle.service.init.dbPending': '项目已创建,但数据库未就绪: {message}。可稍后运行 reactpress-cli start。', + 'bundle.service.init.complete': 'ReactPress 项目初始化完成。运行 reactpress-cli start 启动服务。', + 'bundle.service.init.templateMissing': '模板文件缺失: {path}', + 'bundle.service.config.notFound': '未找到 ReactPress 项目。请先运行 reactpress-cli init 初始化。', + 'bundle.service.config.keyMissing': '配置项不存在: {key}', + 'bundle.service.server.alreadyRunning': '服务已在运行 (PID {pid}),访问 {url}', + 'bundle.service.server.portBusy': '端口 {port} 已被占用,ReactPress 无法绑定。若曾用 Docker Compose 启动过 ReactPress,请执行: docker stop reactpress_server。也可在 .reactpress/config.json 中修改 server.port。', + 'bundle.service.server.cannotStart': '无法启动 ReactPress 服务进程。', + 'bundle.service.server.noHttp': '服务进程已启动 (PID {pid}),但 {url} 无 HTTP 响应。请检查端口占用或 .env 数据库配置。', + 'bundle.service.server.started': 'ReactPress 服务已启动,访问 {url}', + 'bundle.service.server.stopped': 'ReactPress 服务已停止。', + 'bundle.service.server.cannotStopPid': '无法停止进程 PID {pid}', + 'bundle.service.database.dockerMissing': '未检测到 Docker。请安装并启动 Docker,或将 database.mode 设为 external 并使用已有 MySQL。', + 'bundle.service.database.portSwitched': '宿主机端口 {previous} 已被占用,已改用 {port}(已更新 .env)', + 'bundle.service.database.portBindRetry': '端口 {port} 绑定失败,正在尝试其他端口…', + 'bundle.service.database.containerStartFailed': '启动数据库容器失败: {detail}', + 'bundle.service.database.credsMismatch': '数据库容器已在端口 {port} 运行,但账号「{user}」无法连接(数据卷中的凭证与 .env 不一致)。请在项目目录执行: cd .reactpress && docker compose down -v && cd .. && reactpress-cli start', + 'bundle.service.database.connectionTimeout': '数据库容器已启动,但连接超时。请执行 docker logs {container} 查看详情。', + 'bundle.service.database.cannotConnect': '无法连接数据库 {host}:{port},请检查 .env 中的 DB_* 配置。', + 'bundle.serverBundle.missing': '内置服务端缺失,请重新安装 reactpress-cli。', + 'bundle.serverBundle.installing': '正在安装内置服务端运行时依赖…', + 'bundle.serverBundle.installFailed': '内置服务端依赖安装失败。请在 reactpress-cli 安装目录下手动执行: cd server && npm install --omit=dev --no-bin-links', + 'bundle.serverBundle.notBuilt': '内置服务端未构建或依赖不完整。请运行 npm run build:server(开发)或重新安装 reactpress-cli。', + 'bundle.serverBundle.toolkitMissing': '内置 toolkit 缺失,请重新安装 @fecommunity/reactpress。', + 'bundle.port.notFound': '在 {start}-{end} 范围内未找到可用端口', + }, +}; + +module.exports = { STRINGS }; diff --git a/cli/src/lib/lifecycle.ts b/cli/src/lib/lifecycle.ts new file mode 100644 index 00000000..21b46b32 --- /dev/null +++ b/cli/src/lib/lifecycle.ts @@ -0,0 +1,303 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +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 { stopClient } = require('./client-lifecycle'); +const { ensureOriginalCwd } = require('./root'); +const { t } = require('./i18n'); +const { getPm2ServerMemoryRestart } = require('./prod-memory'); +const { ensureBundledServerDeps } = require('./server-bundle'); +const { + PM2_API_APP, + PM2_CLIENT_APP, + isPm2RuntimeAvailable, + isPm2AppOnline, + getPm2AppPid, + runPm2, + stopPm2Apps, +} = require('./pm2-runtime'); + +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); + return true; + } catch (err) { + console.error(t('dev.envFailed'), err.message || err); + return false; + } +} + +function stopApi(projectRoot) { + const pid = readPid(projectRoot); + if (pid && isProcessRunning(pid)) { + try { + process.kill(pid, 'SIGTERM'); + console.log(t('lifecycle.apiStopped', { pid })); + } catch (err) { + console.warn(t('lifecycle.stopPidFailed', { pid }), err.message); + } + } + clearPidFile(projectRoot); +} + +function stopAll(projectRoot) { + stopPm2Apps(projectRoot, [PM2_API_APP, PM2_CLIENT_APP]); + stopClient(projectRoot); + stopApi(projectRoot); +} + +async function startApiWithPm2(projectRoot, { wait = true } = {}) { + const serverBin = getServerBin(projectRoot); + const serverDir = getServerDir(projectRoot); + const serverLogDir = path.join(projectRoot, '.reactpress', 'logs', 'server'); + fs.mkdirSync(serverLogDir, { recursive: true }); + const pm2OutLog = path.join(serverLogDir, 'pm2-out.log'); + const pm2ErrLog = path.join(serverLogDir, 'pm2-error.log'); + + const started = runPm2( + projectRoot, + [ + 'start', + serverBin, + '--name', + PM2_API_APP, + '--cwd', + serverDir, + '--output', + pm2OutLog, + '--error', + pm2ErrLog, + '--max-memory-restart', + getPm2ServerMemoryRestart(), + '--update-env', + '-f', + ], + { + env: { + REACTPRESS_ORIGINAL_CWD: projectRoot, + REACTPRESS_SERVER_LOG_DIR: serverLogDir, + }, + stdio: 'ignore', + }, + ); + + if (!started.ok) { + console.warn(t('lifecycle.processSupervisorFallback')); + return startApiDetached(projectRoot, { wait }); + } + + const pid = getPm2AppPid(projectRoot, PM2_API_APP); + if (pid) { + writePid(projectRoot, pid); + } + console.log(t('lifecycle.apiStartedBg', { pid: pid ?? PM2_API_APP })); + + if (!wait) return 0; + return waitForApiReady(projectRoot, pid); +} + +async function waitForApiReady(projectRoot, pid) { + const serverUrl = loadServerSiteUrl(projectRoot); + const spinner = ora({ + text: t('dev.waitingApi', { url: serverUrl }), + color: 'magenta', + spinner: 'dots', + }).start(); + + const deadline = Date.now() + 120_000; + let ready = false; + while (Date.now() < deadline) { + if (pid && !isProcessRunning(pid) && !isPm2AppOnline(projectRoot, PM2_API_APP)) { + spinner.fail(t('lifecycle.apiExitedEarly', { pid })); + return 1; + } + if (await require('./http').isHttpResponding(serverUrl)) { + ready = true; + break; + } + await new Promise((r) => setTimeout(r, 500)); + } + + if (!ready) { + spinner.fail(t('lifecycle.apiTimeout120', { url: serverUrl })); + return 1; + } + spinner.succeed(t('lifecycle.apiReady', { url: serverUrl })); + return 0; +} + +async function startApiDetached(projectRoot, { wait = true } = {}) { + const serverLogDir = path.join(projectRoot, '.reactpress', 'logs', 'server'); + fs.mkdirSync(serverLogDir, { recursive: true }); + + const child = spawn(process.execPath, [getServerBin(projectRoot)], { + cwd: getServerDir(projectRoot), + detached: true, + stdio: 'ignore', + env: { + ...process.env, + REACTPRESS_ORIGINAL_CWD: projectRoot, + REACTPRESS_SERVER_LOG_DIR: serverLogDir, + }, + }); + + child.unref(); + writePid(projectRoot, child.pid); + console.log(t('lifecycle.apiStartedBg', { pid: child.pid })); + + if (!wait) return 0; + return waitForApiReady(projectRoot, child.pid); +} + +async function startApi(projectRoot, { wait = true } = {}) { + if (!(await ensureConfig(projectRoot))) { + return 1; + } + + if (isPm2AppOnline(projectRoot, PM2_API_APP)) { + const pid = getPm2AppPid(projectRoot, PM2_API_APP); + console.log(t('lifecycle.apiAlreadyRunning', { pid: pid ?? PM2_API_APP })); + return 0; + } + + const existing = readPid(projectRoot); + if (existing && isProcessRunning(existing)) { + console.log(t('lifecycle.apiAlreadyRunning', { pid: existing })); + return 0; + } + clearPidFile(projectRoot); + + if (!canStartLocalApi(projectRoot)) { + console.error(t('lifecycle.noServerAvailable')); + return 1; + } + + if (!isUsingMonorepoServer(projectRoot)) { + const bundled = await ensureBundledServerDeps(projectRoot); + if (!bundled.ok) { + console.error(bundled.message || t('bundle.serverBundle.notBuilt')); + return 1; + } + } + + if (isUsingMonorepoServer(projectRoot)) { + console.log(t('lifecycle.startingLocalApi')); + } else { + console.log(t('lifecycle.startingBundledApi')); + } + + if (isPm2RuntimeAvailable(projectRoot)) { + return startApiWithPm2(projectRoot, { wait }); + } + + return startApiDetached(projectRoot, { wait }); +} + +async function statusApi(projectRoot) { + const pid = readPid(projectRoot); + const serverUrl = loadServerSiteUrl(projectRoot); + const { isHttpResponding } = require('./http'); + const httpOk = await isHttpResponding(serverUrl); + + const source = isUsingMonorepoServer(projectRoot) + ? t('lifecycle.source.monorepo') + : t('lifecycle.source.bundle'); + + console.log(t('lifecycle.apiStatusTitle')); + console.log(t('lifecycle.source', { source })); + console.log(t('lifecycle.pidFile', { path: getPidFile(projectRoot) })); + console.log( + t('lifecycle.recordedPid', { + pid: pid ?? t('common.none'), + }) + ); + console.log( + t('lifecycle.processAlive', { + alive: pid + ? isProcessRunning(pid) + ? t('common.yes') + : t('common.no') + : '—', + }) + ); + console.log( + t('lifecycle.httpStatus', { + url: serverUrl, + status: httpOk ? t('lifecycle.httpReachable') : t('lifecycle.httpUnreachable'), + }) + ); +} + +async function runLifecycleCommand(command, projectRoot = ensureOriginalCwd()) { + switch (command) { + case 'start': + return startApi(projectRoot, { wait: true }); + case 'start:bg': + return startApi(projectRoot, { wait: false }); + case 'stop': + stopAll(projectRoot); + return 0; + case 'restart': + stopAll(projectRoot); + await waitForPortFree(parseServerPort(projectRoot)); + await new Promise((r) => setTimeout(r, 400)); + return startApi(projectRoot, { wait: true }); + case 'status': + await statusApi(projectRoot); + return 0; + default: + throw new Error(t('lifecycle.unknownCommand', { command })); + } +} + +module.exports = { + startApi, + stopApi, + stopAll, + statusApi, + runLifecycleCommand, +}; diff --git a/cli/src/lib/nginx.ts b/cli/src/lib/nginx.ts new file mode 100644 index 00000000..3c9cfdbb --- /dev/null +++ b/cli/src/lib/nginx.ts @@ -0,0 +1,661 @@ +// @ts-nocheck +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 { readDevClientApiOrigin } = require('./remote-dev'); + +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}`; +} + +function readDevNginxPorts(projectRoot) { + const { DEV_PORTS, readEnvPort, readVisitorPort } = require('./ports'); + return { + adminPort: readEnvPort(projectRoot, 'WEB_ADMIN_PORT', DEV_PORTS.ADMIN_WEB), + visitorPort: readVisitorPort(projectRoot), + apiPort: readEnvPort(projectRoot, 'SERVER_PORT', DEV_PORTS.API), + }; +} + +function resolveRemoteUpstreamHost(remoteApiOrigin) { + const raw = typeof remoteApiOrigin === 'string' ? remoteApiOrigin.trim() : ''; + if (!raw) return ''; + try { + return new URL(raw).host; + } catch { + return raw.replace(/^https?:\/\//i, '').split('/')[0]; + } +} + +function renderApiProxyBlock(remoteApiOrigin, apiPort) { + if (remoteApiOrigin) { + const upstreamHost = resolveRemoteUpstreamHost(remoteApiOrigin); + return ` # REST API (remote upstream) + location /api { + proxy_pass ${remoteApiOrigin}; + proxy_ssl_server_name on; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host ${upstreamHost}; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; + proxy_redirect off; + }`; + } + + return ` # REST API (Nest on host :${apiPort}, keep /api prefix) + location /api { + proxy_pass http://host.docker.internal:${apiPort}; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; + proxy_redirect off; + }`; +} + +function renderPublicUploadsProxyBlock(remoteApiOrigin, apiPort) { + const proxyTarget = remoteApiOrigin + ? remoteApiOrigin.replace(/\/api\/?$/, '') + : `http://host.docker.internal:${apiPort}`; + + return ` # Uploaded media (API static /public) + location /public/ { + proxy_pass ${proxyTarget}; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300; + proxy_connect_timeout 300; + expires 30d; + add_header Cache-Control "public, max-age=2592000"; + access_log off; + }`; +} + +function renderDevNginxConfig({ adminPort, visitorPort, apiPort, clientApiOrigin = null }) { + const apiBlock = renderApiProxyBlock(clientApiOrigin, apiPort); + const publicBlock = renderPublicUploadsProxyBlock(clientApiOrigin, apiPort); + return `server { + listen 80; + server_name localhost; + charset utf-8; + + # Visitor site (active theme Next.js on host :${visitorPort}) + location / { + proxy_pass http://host.docker.internal:${visitorPort}; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; + proxy_redirect off; + } + + # Admin SPA (Vite base /admin/, host :${adminPort}) + location /admin/ { + proxy_pass http://host.docker.internal:${adminPort}/admin/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 300; + proxy_connect_timeout 300; + } + + location = /admin { + return 301 /admin/; + } + +${publicBlock} + +${apiBlock} + + # Next.js dev/HMR rewrites chunks frequently — never cache /_next (prod nginx keeps long cache). + location /_next/ { + proxy_pass http://host.docker.internal:${visitorPort}; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 300; + proxy_connect_timeout 300; + add_header Cache-Control "no-store, no-cache, must-revalidate" always; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; + } + + location /health { + access_log off; + return 200 "healthy\\n"; + add_header Content-Type text/plain; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} +`; +} + +function isDevNginxConfigStale(projectRoot, configPath) { + const { adminPort, visitorPort, apiPort } = readDevNginxPorts(projectRoot); + const clientApiOrigin = readDevClientApiOrigin(projectRoot); + let content; + try { + content = fs.readFileSync(configPath, 'utf8'); + } catch { + return true; + } + if (content.includes(':5173')) return true; + if (!content.includes(`host.docker.internal:${adminPort}/admin/`)) return true; + if (!content.includes(`host.docker.internal:${visitorPort}`)) return true; + if (clientApiOrigin) { + if (!content.includes(`proxy_pass ${clientApiOrigin}`)) return true; + if (content.includes(`host.docker.internal:${apiPort}`)) return true; + } else if (!content.includes(`host.docker.internal:${apiPort}`)) { + return true; + } + // Dev must not long-cache Next chunks (breaks client-side nav after on-demand compile). + if (content.includes('expires 1y') && content.includes('/_next/')) return true; + if (!content.includes('location /public/')) return true; + return false; +} + +function isProdNginxConfigStale(projectRoot, configPath) { + const { visitorPort, apiPort } = readDevNginxPorts(projectRoot); + let content = ''; + try { + content = fs.readFileSync(configPath, 'utf8'); + } catch { + return true; + } + if (content.includes('host.docker.internal:13001') || content.includes('host.docker.internal:13002')) { + return true; + } + if (!content.includes(`host.docker.internal:${visitorPort}`)) return true; + if (!content.includes(`host.docker.internal:${apiPort}`)) return true; + if (!content.includes('location /public/')) return true; + return false; +} + +function renderProdNginxConfig(projectRoot) { + const templatePath = bundledTemplatePath('prod'); + const { visitorPort, apiPort } = readDevNginxPorts(projectRoot); + let content = fs.readFileSync(templatePath, 'utf8'); + content = content.replace(/host\.docker\.internal:3001/g, `host.docker.internal:${visitorPort}`); + content = content.replace(/host\.docker\.internal:3002/g, `host.docker.internal:${apiPort}`); + return content; +} + +function writeProdNginxConfig(projectRoot) { + const configPath = resolveNginxConfigPath(projectRoot, 'prod'); + const content = renderProdNginxConfig(projectRoot); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + const existed = fs.existsSync(configPath); + const previous = existed ? fs.readFileSync(configPath, 'utf8') : ''; + fs.writeFileSync(configPath, content, 'utf8'); + return { + configPath, + changed: content !== previous, + created: !existed, + mode: 'prod', + }; +} + +function writeDevNginxConfig(projectRoot) { + const configPath = resolveNginxConfigPath(projectRoot, 'dev'); + const ports = readDevNginxPorts(projectRoot); + const clientApiOrigin = readDevClientApiOrigin(projectRoot); + const content = renderDevNginxConfig({ ...ports, clientApiOrigin }); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + const existed = fs.existsSync(configPath); + const previous = existed ? fs.readFileSync(configPath, 'utf8') : ''; + fs.writeFileSync(configPath, content, 'utf8'); + return { + configPath, + changed: content !== previous, + created: !existed, + mode: 'dev', + }; +} + +/** + * Write default nginx config from CLI templates when missing (or when force). + * + * @returns {{ configPath: string, created: boolean, mode: 'dev' | 'prod', changed?: boolean }} + */ +function ensureNginxConfig(projectRoot, options = {}) { + const mode = resolveNginxMode(options); + const configPath = resolveNginxConfigPath(projectRoot, mode); + + if (mode === 'dev') { + if (options.force || !fs.existsSync(configPath) || isDevNginxConfigStale(projectRoot, configPath)) { + const result = writeDevNginxConfig(projectRoot); + return { configPath: result.configPath, created: result.created || result.changed, changed: result.changed, mode }; + } + return { configPath, created: false, changed: false, mode }; + } + + if (mode === 'prod') { + if (options.force || !fs.existsSync(configPath) || isProdNginxConfigStale(projectRoot, configPath)) { + const result = writeProdNginxConfig(projectRoot); + return { + configPath: result.configPath, + created: result.created || result.changed, + changed: result.changed, + mode, + }; + } + return { configPath, created: false, changed: false, 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 composeArgs = ['up', '-d', '--no-deps', '--remove-orphans', ctx.service]; + const result = runComposeOnContext(ctx, composeArgs, { + stdio: options.quiet ? 'ignore' : 'inherit', + }); + if (result.status !== 0) { + throw new Error(t('nginx.startFailed')); + } + } else { + startNginxContainer(configPath, port); + } + + if (!options.quiet) { + 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'), + }; +} + +/** + * Start dev reverse proxy (Docker). Returns false when skipped or failed (non-fatal). + * @returns {Promise} + */ +async function startDevNginx(projectRoot) { + if (process.env.REACTPRESS_SKIP_NGINX === '1') { + return false; + } + if (!isDockerRunning()) { + console.warn(t('dev.nginxSkippedDocker')); + return false; + } + try { + const { changed } = writeDevNginxConfig(projectRoot); + nginxUp(projectRoot, { quiet: true }); + if (changed && isNginxContainerRunning()) { + try { + nginxReload(); + } catch { + nginxRestart(projectRoot, { quiet: true }); + } + } + const probeMs = Math.max( + 1000, + parseInt(process.env.REACTPRESS_NGINX_PROBE_MS || '4000', 10) || 4000, + ); + const healthy = await probeNginxHealth(projectRoot, probeMs); + if (!healthy) { + console.warn(t('dev.nginxSlow', { url: nginxEntryUrl(projectRoot) })); + } + return true; + } catch (err) { + console.warn(t('dev.nginxStartFailed', { message: err.message || String(err) })); + return false; + } +} + +function stopDevNginx(projectRoot) { + try { + nginxDown(projectRoot); + } catch { + stopNginxContainer(); + } +} + +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, + renderDevNginxConfig, + renderProdNginxConfig, + nginxEntryUrl, + resolveNginxPort, + isNginxContainerRunning, + probeNginxHealth, + checkNginx, + runNginxCommand, + startDevNginx, + stopDevNginx, +}; diff --git a/cli/src/lib/paths.ts b/cli/src/lib/paths.ts new file mode 100644 index 00000000..d27bc4ac --- /dev/null +++ b/cli/src/lib/paths.ts @@ -0,0 +1,144 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const { ensureOriginalCwd, getMonorepoRoot } = require('./root'); + +function resolveProjectRoot(projectRoot) { + return path.resolve(projectRoot || ensureOriginalCwd()); +} + +function getMonorepoServerDir(projectRoot) { + return path.join(resolveProjectRoot(projectRoot), 'server'); +} + +function hasMonorepoServerSource(projectRoot) { + return fs.existsSync( + path.join(getMonorepoServerDir(projectRoot), 'src', 'main.ts') + ); +} + +function getCliPackageRoot() { + const ownRoot = path.join(__dirname, '..', '..'); + if (fs.existsSync(path.join(ownRoot, 'out', 'bin', 'reactpress.js'))) { + return ownRoot; + } + if (fs.existsSync(path.join(ownRoot, 'dist', 'index.js'))) { + return ownRoot; + } + try { + return path.dirname( + require.resolve('@fecommunity/reactpress-cli-core/package.json') + ); + } catch { + return path.dirname(require.resolve('@fecommunity/reactpress-cli/package.json')); + } +} + +function getCliVersion() { + try { + const pkgPath = path.join(getCliPackageRoot(), 'package.json'); + const pkg = require(pkgPath); + return typeof pkg.version === 'string' ? pkg.version : 'dev'; + } catch { + return 'dev'; + } +} + +function getBundledServerDir() { + return path.join(getCliPackageRoot(), 'server'); +} + +function hasBundledServerBuild() { + return fs.existsSync(path.join(getBundledServerDir(), 'dist', 'main.js')); +} + +function getServerDir(projectRoot) { + if (hasMonorepoServerSource(projectRoot)) { + return getMonorepoServerDir(projectRoot); + } + return getBundledServerDir(); +} + +function getServerBin(projectRoot) { + return path.join(getServerDir(projectRoot), 'bin', 'reactpress-server.js'); +} + +function getSwaggerPath(projectRoot) { + return path.join(getServerDir(projectRoot), 'public', 'swagger.json'); +} + +function getServerMain(projectRoot) { + return path.join(getServerDir(projectRoot), 'dist', 'main.js'); +} + +function isUsingMonorepoServer(projectRoot) { + return hasMonorepoServerSource(projectRoot); +} + +function canStartLocalApi(projectRoot) { + return ( + isUsingMonorepoServer(projectRoot) || + hasBundledServerBuild() + ); +} + +function getThemeBin(projectRoot) { + const root = resolveProjectRoot(projectRoot); + const { readActiveThemeManifest, resolveThemeDirectory } = require('./theme-runtime'); + const { activeTheme } = readActiveThemeManifest(root); + const themeDir = resolveThemeDirectory(root, activeTheme); + if (!themeDir) { + const err = new Error( + `Active theme not found: ${activeTheme}. Activate a theme in Admin → Appearance.` + ); + err.code = 'REACTPRESS_THEME_NOT_FOUND'; + throw err; + } + const binPath = path.join(themeDir, 'bin', 'reactpress-client.js'); + if (fs.existsSync(binPath)) { + return binPath; + } + const genericBin = path.join(getCliPackageRoot(), 'bin', 'reactpress-theme-client.js'); + if (fs.existsSync(genericBin)) { + return genericBin; + } + const err = new Error( + `Theme entry not found: ${binPath}. Run from a ReactPress project with an installed theme.` + ); + err.code = 'REACTPRESS_THEME_BIN_NOT_FOUND'; + throw err; +} + +/** @deprecated Use getThemeBin */ +function getClientBin(projectRoot) { + return getThemeBin(projectRoot); +} + +function getPidFile(projectRoot) { + return path.join(resolveProjectRoot(projectRoot), '.reactpress', 'server.pid'); +} + +function getClientPidFile(projectRoot) { + return path.join(resolveProjectRoot(projectRoot), '.reactpress', 'client.pid'); +} + +module.exports = { + getMonorepoRoot, + resolveProjectRoot, + getMonorepoServerDir, + hasMonorepoServerSource, + hasBundledServerBuild, + isUsingMonorepoServer, + canStartLocalApi, + getCliPackageRoot, + getCliVersion, + getBundledServerDir, + getServerDir, + getServerBin, + getSwaggerPath, + getServerMain, + getThemeBin, + getClientBin, + getPidFile, + getClientPidFile, +}; diff --git a/cli/src/lib/plugin-build.ts b/cli/src/lib/plugin-build.ts new file mode 100644 index 00000000..322b53a4 --- /dev/null +++ b/cli/src/lib/plugin-build.ts @@ -0,0 +1,97 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +function readLocalPluginIds(projectRoot) { + const pkgPath = path.join(projectRoot, 'plugins', 'package.json'); + if (!fs.existsSync(pkgPath)) return []; + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + const local = pkg?.reactpress?.local; + return Array.isArray(local) ? local.filter((id) => typeof id === 'string') : []; + } catch { + return []; + } +} + +function readPluginManifest(pluginDir) { + const manifestPath = path.join(pluginDir, 'plugin.json'); + if (!fs.existsSync(manifestPath)) return null; + try { + return JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + } catch { + return null; + } +} + +function newestMtime(root, relDir) { + const dir = path.join(root, relDir); + if (!fs.existsSync(dir)) return 0; + let max = 0; + const walk = (current) => { + for (const entry of fs.readdirSync(current, { withFileTypes: true })) { + const full = path.join(current, entry.name); + if (entry.isDirectory()) walk(full); + else if (entry.isFile()) max = Math.max(max, fs.statSync(full).mtimeMs); + } + }; + walk(dir); + return max; +} + +function shouldBuildPlugin(pluginDir) { + const pkgPath = path.join(pluginDir, 'package.json'); + if (!fs.existsSync(pkgPath)) return false; + let pkg; + try { + pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + } catch { + return false; + } + if (!pkg.scripts?.build) return false; + + const manifest = readPluginManifest(pluginDir); + const moduleRel = manifest?.server?.module; + if (!moduleRel || typeof moduleRel !== 'string') return false; + + const entry = path.join(pluginDir, moduleRel.replace(/^\.\//, '')); + if (!fs.existsSync(entry)) return true; + + const srcMtime = newestMtime(pluginDir, 'src'); + const entryMtime = fs.statSync(entry).mtimeMs; + return srcMtime > entryMtime; +} + +function buildPlugin(pluginDir, { quiet = false } = {}) { + const name = path.basename(pluginDir); + if (!quiet) { + console.log(`[reactpress] Building plugin "${name}"…`); + } + const result = spawnSync('pnpm', ['run', 'build'], { + cwd: pluginDir, + stdio: quiet ? 'pipe' : 'inherit', + env: process.env, + }); + if (result.status !== 0) { + const stderr = result.stderr?.toString?.() ?? ''; + throw new Error(`Plugin "${name}" build failed${stderr ? `: ${stderr.trim()}` : ''}`); + } +} + +function buildLocalPlugins(projectRoot, options = {}) { + const ids = readLocalPluginIds(projectRoot); + for (const id of ids) { + const pluginDir = path.join(projectRoot, 'plugins', id); + if (!fs.existsSync(pluginDir)) continue; + if (!shouldBuildPlugin(pluginDir)) continue; + buildPlugin(pluginDir, options); + } +} + +module.exports = { + readLocalPluginIds, + shouldBuildPlugin, + buildPlugin, + buildLocalPlugins, +}; diff --git a/cli/src/lib/plugin-cli.ts b/cli/src/lib/plugin-cli.ts new file mode 100644 index 00000000..9ed8d4d2 --- /dev/null +++ b/cli/src/lib/plugin-cli.ts @@ -0,0 +1,144 @@ +// @ts-nocheck +const chalk = require('chalk'); +const fs = require('fs'); +const path = require('path'); + +const PLUGIN_RUNTIME_REL = path.join('.reactpress', 'plugins'); +const PLUGIN_ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; +const COPY_SKIP_NAMES = new Set([ + 'node_modules', + '.git', + 'dist', + '.turbo', + 'coverage', + '.reactpress', + '.cache', + 'package-lock.json', +]); + +function isValidPluginId(id) { + return typeof id === 'string' && PLUGIN_ID_RE.test(id) && id.length <= 64; +} + +function readPluginsPackageMeta(projectRoot) { + const pkgPath = path.join(projectRoot, 'plugins', 'package.json'); + if (!fs.existsSync(pkgPath)) return { local: [] }; + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + const local = Array.isArray(pkg.reactpress?.local) + ? pkg.reactpress.local.filter((id) => typeof id === 'string') + : []; + return { local }; + } catch { + return { local: [] }; + } +} + +function readPluginManifest(pluginDir) { + const manifestPath = path.join(pluginDir, 'plugin.json'); + if (!fs.existsSync(manifestPath)) return null; + try { + return JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + } catch { + return null; + } +} + +function copyDir(src, dest) { + fs.mkdirSync(dest, { recursive: true }); + for (const entry of fs.readdirSync(src, { withFileTypes: true })) { + if (COPY_SKIP_NAMES.has(entry.name)) continue; + const from = path.join(src, entry.name); + const to = path.join(dest, entry.name); + if (entry.isSymbolicLink()) { + continue; + } else if (entry.isDirectory()) { + copyDir(from, to); + } else if (entry.isFile()) { + fs.copyFileSync(from, to); + } + } +} + +function removeDir(dir) { + if (!fs.existsSync(dir)) return; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) removeDir(full); + else fs.unlinkSync(full); + } + fs.rmdirSync(dir); +} + +function materializeRuntimePlugin(projectRoot, templatePath, targetDir) { + const forceCopy = + process.env.REACTPRESS_PLUGIN_RUNTIME_COPY === '1' || process.env.NODE_ENV === 'production'; + fs.mkdirSync(path.dirname(targetDir), { recursive: true }); + if (fs.existsSync(targetDir)) removeDir(targetDir); + if (!forceCopy) { + const linkTarget = path.relative(path.dirname(targetDir), templatePath); + fs.symlinkSync(linkTarget, targetDir, 'dir'); + return; + } + copyDir(templatePath, targetDir); +} + +function installLocalPlugin(projectRoot, id) { + if (!isValidPluginId(id)) { + throw new Error(`Invalid plugin id "${id}"`); + } + const templatePath = path.join(projectRoot, 'plugins', id); + if (!fs.existsSync(templatePath)) { + throw new Error(`Plugin template "${id}" not found under plugins/`); + } + const manifest = readPluginManifest(templatePath); + if (!manifest?.id) { + throw new Error(`Plugin "${id}" has invalid plugin.json`); + } + const targetDir = path.join(projectRoot, PLUGIN_RUNTIME_REL, id); + materializeRuntimePlugin(projectRoot, templatePath, targetDir); + return { pluginId: manifest.id, name: manifest.name, pluginDirRel: PLUGIN_RUNTIME_REL }; +} + +function listAvailablePluginIds(projectRoot) { + const { local } = readPluginsPackageMeta(projectRoot); + const runtimeDir = path.join(projectRoot, PLUGIN_RUNTIME_REL); + const installed = fs.existsSync(runtimeDir) + ? fs + .readdirSync(runtimeDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + : []; + return [...new Set([...local, ...installed])]; +} + +function runPluginInstall(projectRoot, id) { + const result = installLocalPlugin(projectRoot, id); + console.log( + chalk.green('[reactpress]'), + `Installed plugin "${result.name}" (${result.pluginId}) → ${result.pluginDirRel}/${result.pluginId}/`, + ); + console.log(chalk.gray(`Activate via admin /plugins or: reactpress plugin activate ${result.pluginId}`)); + return result; +} + +function runPluginList(projectRoot) { + const ids = listAvailablePluginIds(projectRoot); + if (!ids.length) { + console.log('No plugins registered.'); + return; + } + console.log('Available plugins:'); + for (const id of ids.sort()) { + const runtime = path.join(projectRoot, PLUGIN_RUNTIME_REL, id); + const installed = fs.existsSync(runtime); + console.log(` - ${id}${installed ? ' (installed)' : ''}`); + } +} + +module.exports = { + installLocalPlugin, + listAvailablePluginIds, + runPluginInstall, + runPluginList, +}; diff --git a/cli/src/lib/pm2-runtime.ts b/cli/src/lib/pm2-runtime.ts new file mode 100644 index 00000000..9e607466 --- /dev/null +++ b/cli/src/lib/pm2-runtime.ts @@ -0,0 +1,120 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); +const { getBundledServerDir, getServerDir } = require('./paths'); + +/** Internal PM2 app names — not exposed in user-facing CLI copy. */ +const PM2_API_APP = 'reactpress-api'; +const PM2_CLIENT_APP = 'reactpress-client'; + +function isPm2Disabled() { + return process.env.REACTPRESS_DISABLE_PM2 === '1'; +} + +function resolvePm2Bin(projectRoot) { + if (isPm2Disabled()) return null; + + const candidates = [ + path.join(getServerDir(projectRoot), 'node_modules', '.bin', 'pm2'), + path.join(getBundledServerDir(), 'node_modules', '.bin', 'pm2'), + ]; + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) return candidate; + } + + const probe = spawnSync('pm2', ['--version'], { stdio: 'ignore', shell: process.platform === 'win32' }); + return probe.status === 0 ? 'pm2' : null; +} + +function isPm2RuntimeAvailable(projectRoot) { + return Boolean(resolvePm2Bin(projectRoot)); +} + +function runPm2(projectRoot, args, options = {}) { + const bin = resolvePm2Bin(projectRoot); + if (!bin) { + return { ok: false, error: 'PM2_UNAVAILABLE' }; + } + + const result = spawnSync(bin, args, { + cwd: options.cwd, + env: { ...process.env, ...options.env }, + encoding: 'utf8', + stdio: options.stdio ?? 'pipe', + shell: options.shell ?? false, + }); + + if (result.status !== 0) { + const message = [result.stderr, result.stdout].filter(Boolean).join('\n').trim(); + return { ok: false, error: message || `pm2 exited ${result.status}` }; + } + + return { ok: true, stdout: result.stdout ?? '' }; +} + +function readPm2Apps(projectRoot) { + const result = runPm2(projectRoot, ['jlist'], { stdio: 'pipe' }); + if (!result.ok) return []; + try { + const apps = JSON.parse(result.stdout); + return Array.isArray(apps) ? apps : []; + } catch { + return []; + } +} + +function getPm2App(projectRoot, name) { + return readPm2Apps(projectRoot).find((app) => app?.name === name) ?? null; +} + +function isPm2AppOnline(projectRoot, name) { + const app = getPm2App(projectRoot, name); + return app?.pm2_env?.status === 'online'; +} + +function getPm2AppPid(projectRoot, name) { + const app = getPm2App(projectRoot, name); + return typeof app?.pid === 'number' ? app.pid : null; +} + +function getPm2AppLogPaths(projectRoot, name = PM2_API_APP) { + const serverLogDir = path.join(projectRoot, '.reactpress', 'logs', 'server'); + const defaults = { + out: path.join(serverLogDir, 'pm2-out.log'), + err: path.join(serverLogDir, 'pm2-error.log'), + }; + const app = getPm2App(projectRoot, name); + if (!app) return defaults; + return { + out: app.pm2_env?.pm_out_log_path || defaults.out, + err: app.pm2_env?.pm_err_log_path || defaults.err, + }; +} + +function stopPm2Apps(projectRoot, names) { + const bin = resolvePm2Bin(projectRoot); + if (!bin) return false; + + const targets = names.filter((name) => getPm2App(projectRoot, name)); + if (!targets.length) return false; + + runPm2(projectRoot, ['delete', ...targets], { stdio: 'ignore' }); + return true; +} + +module.exports = { + PM2_API_APP, + PM2_CLIENT_APP, + isPm2Disabled, + resolvePm2Bin, + isPm2RuntimeAvailable, + runPm2, + readPm2Apps, + getPm2App, + isPm2AppOnline, + getPm2AppPid, + getPm2AppLogPaths, + stopPm2Apps, +}; diff --git a/cli/src/lib/pm2.ts b/cli/src/lib/pm2.ts new file mode 100644 index 00000000..2cbf4863 --- /dev/null +++ b/cli/src/lib/pm2.ts @@ -0,0 +1,41 @@ +// @ts-nocheck +const { spawn } = require('child_process'); +const { getServerBin, getServerDir, isUsingMonorepoServer } = require('./paths'); +const { ensureOriginalCwd } = require('./root'); +const { t } = require('./i18n'); +const { ensureBundledServerDeps } = require('./server-bundle'); + +async function startApiWithPm2(projectRoot = ensureOriginalCwd()) { + if (!isUsingMonorepoServer(projectRoot)) { + const bundled = await ensureBundledServerDeps(projectRoot); + if (!bundled.ok) { + throw new Error(bundled.message || t('bundle.serverBundle.notBuilt')); + } + } + + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, [getServerBin(projectRoot), '--pm2'], { + stdio: 'inherit', + cwd: getServerDir(projectRoot), + env: { + ...process.env, + REACTPRESS_ORIGINAL_CWD: projectRoot, + }, + }); + + child.on('error', (error) => { + console.error(t('pm2.startFailed'), error); + reject(error); + }); + + child.on('close', (code) => { + if (code !== 0) { + reject(Object.assign(new Error(t('pm2.exitCode', { code })), { exitCode: code })); + return; + } + resolve(); + }); + }); +} + +module.exports = { startApiWithPm2 }; diff --git a/cli/src/lib/ports.ts b/cli/src/lib/ports.ts new file mode 100644 index 00000000..23d9fe42 --- /dev/null +++ b/cli/src/lib/ports.ts @@ -0,0 +1,561 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const net = require('net'); +const { spawnSync } = require('child_process'); + +/** + * Local dev port map (keep in sync with `.env`, README, and `theme.service.ts` defaults). + * + * | Port | Service | + * |------|---------| + * | 3000 | Admin SPA — Vite (`web/`, `WEB_ADMIN_URL`) | + * | 3001 | Visitor site — active theme Next.js (`CLIENT_SITE_URL`) | + * | 3002 | API server (`SERVER_PORT`) | + * | 3003 | Admin theme preview only (`REACTPRESS_PREVIEW_PORT`, `preview-theme.json`) | + * | 3306 | MySQL (`DB_PORT`) | + */ +const DEV_PORTS = { + ADMIN_WEB: 3000, + VISITOR: 3001, + API: 3002, + THEME_PREVIEW: 3003, + MYSQL: 3306, +}; + +/** Offset applied per `REACTPRESS_INSTANCE` (instance 1 → admin :3010, api :3012, …). */ +const INSTANCE_PORT_STEP = 10; + +function readProcessEnvPort(key) { + const raw = process.env[key]?.trim(); + if (!raw) return null; + const n = parseInt(raw.replace(/^['"]|['"]$/g, ''), 10); + return Number.isInteger(n) && n > 0 ? n : null; +} + +function readInstanceIndex() { + const raw = process.env.REACTPRESS_INSTANCE ?? process.env.REACTPRESS_DEV_INSTANCE ?? '0'; + const n = parseInt(String(raw).trim(), 10); + if (!Number.isInteger(n) || n < 0 || n > 99) return 0; + return n; +} + +function instancePortOffset() { + return readInstanceIndex() * INSTANCE_PORT_STEP; +} + +/** + * Resolve a dev-stack port: process.env override → `.env` (instance 0 only) → default + instance offset. + */ +function resolveStackPort(projectRoot, envKey, defaultBase) { + const fromProcess = readProcessEnvPort(envKey); + if (fromProcess) return fromProcess; + + const instance = readInstanceIndex(); + const shiftedDefault = defaultBase + instance * INSTANCE_PORT_STEP; + + if (instance === 0) { + try { + const content = fs.readFileSync(path.join(projectRoot, '.env'), 'utf8'); + const match = content.match(new RegExp(`^${envKey}=(.+)$`, 'm')); + if (match) { + const n = parseInt(match[1].trim().replace(/^['"]|['"]$/g, ''), 10); + if (Number.isInteger(n) && n > 0) return n; + } + } catch { + // ignore + } + } + + return shiftedDefault; +} + +function resolveDevStackPorts(projectRoot) { + const offset = instancePortOffset(); + const visitorFromProcess = readProcessEnvPort('CLIENT_PORT'); + let visitor = DEV_PORTS.VISITOR + offset; + if (visitorFromProcess) { + visitor = visitorFromProcess; + } else if (readInstanceIndex() === 0) { + visitor = readVisitorPort(projectRoot); + } + + return { + admin: resolveStackPort(projectRoot, 'WEB_ADMIN_PORT', DEV_PORTS.ADMIN_WEB), + visitor, + api: resolveStackPort(projectRoot, 'SERVER_PORT', DEV_PORTS.API), + preview: resolveStackPort(projectRoot, 'REACTPRESS_PREVIEW_PORT', DEV_PORTS.THEME_PREVIEW), + }; +} + +/** Push resolved ports into `process.env` so child processes share one port map. */ +function applyDevStackPortsToEnv(ports) { + process.env.WEB_ADMIN_PORT = String(ports.admin); + process.env.SERVER_PORT = String(ports.api); + process.env.REACTPRESS_LOCAL_API_PORT = String(ports.api); + process.env.CLIENT_PORT = String(ports.visitor); + process.env.REACTPRESS_PREVIEW_PORT = String(ports.preview); + process.env.CLIENT_SITE_URL = `http://localhost:${ports.visitor}`; + process.env.SERVER_SITE_URL = `http://127.0.0.1:${ports.api}`; + if (!process.env.VITE_DEV_API_PROXY_TARGET?.trim()) { + process.env.VITE_DEV_API_PROXY_TARGET = `http://127.0.0.1:${ports.api}`; + } +} + +function devSessionSuffix() { + const instance = readInstanceIndex(); + return instance > 0 ? `-instance-${instance}` : ''; +} + +/** Ports theme `next dev` must not bind to (reserved for other services). 3003 is allowed — preview theme. */ +/** Never kill listeners on DB / infra ports during dev port cleanup. */ +const PROTECTED_KILL_PORTS = new Set([3306, 3307, 5432, 6379]); + +const BLOCKED_THEME_DEV_PORTS = new Set([ + 22, + 80, + 443, + 3000, + 3002, + 5173, + 5432, + 6379, + 8080, + 8443, + 3306, + 3307, +]); + +function readEnvPort(projectRoot, key, fallback) { + const fromProcess = readProcessEnvPort(key); + if (fromProcess) return fromProcess; + try { + const content = fs.readFileSync(path.join(projectRoot, '.env'), 'utf8'); + const match = content.match(new RegExp(`^${key}=(.+)$`, 'm')); + if (match) { + const n = parseInt(match[1].trim().replace(/^['"]|['"]$/g, ''), 10); + if (Number.isInteger(n) && n > 0) return n; + } + } catch { + // ignore + } + return fallback; +} + +function readVisitorPort(projectRoot) { + const fromEnv = readEnvPort(projectRoot, 'CLIENT_PORT', null); + if (fromEnv) return fromEnv; + try { + const content = fs.readFileSync(path.join(projectRoot, '.env'), 'utf8'); + const match = content.match(/^CLIENT_SITE_URL=(.+)$/m); + if (match) { + const url = new URL(match[1].trim().replace(/^['"]|['"]$/g, '')); + const n = parseInt(url.port || String(DEV_PORTS.VISITOR), 10); + if (Number.isInteger(n) && n > 0) return n; + } + } catch { + // ignore + } + return DEV_PORTS.VISITOR; +} + +function isPortListening(port) { + const n = parseInt(port, 10); + if (!Number.isInteger(n) || n < 1) return false; + const result = spawnSync('lsof', [`-tiTCP:${n}`, '-sTCP:LISTEN'], { encoding: 'utf8' }); + return result.status === 0 && Boolean(result.stdout?.trim()); +} + +/** Kill processes listening on `port`. Returns PIDs signalled. */ +function killPortListeners(port, signal = 'KILL') { + const n = parseInt(port, 10); + if (!Number.isInteger(n) || n < 1) return []; + + const flag = signal === 'TERM' ? '-TERM' : '-9'; + const result = spawnSync('lsof', [`-tiTCP:${n}`, '-sTCP:LISTEN'], { encoding: 'utf8' }); + if (result.status !== 0 || !result.stdout?.trim()) return []; + + const pids = []; + for (const pid of result.stdout.trim().split(/\s+/)) { + if (!pid) continue; + spawnSync('kill', [flag, pid], { stdio: 'ignore' }); + pids.push(pid); + } + return pids; +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function getListenerPids(port) { + const n = parseInt(port, 10); + if (!Number.isInteger(n) || n < 1) return []; + const result = spawnSync('lsof', [`-tiTCP:${n}`, '-sTCP:LISTEN'], { encoding: 'utf8' }); + if (result.status !== 0 || !result.stdout?.trim()) return []; + return result.stdout.trim().split(/\s+/).filter(Boolean); +} + +function collectDescendantPids(rootPid) { + const root = parseInt(rootPid, 10); + if (!Number.isFinite(root) || root <= 0) return []; + + const out = []; + const queue = [String(root)]; + const seen = new Set(); + + while (queue.length) { + const pid = queue.shift(); + if (!pid || seen.has(pid)) continue; + seen.add(pid); + + const children = spawnSync('pgrep', ['-P', pid], { encoding: 'utf8' }); + if (children.status !== 0 || !children.stdout?.trim()) continue; + + for (const child of children.stdout.trim().split(/\s+/)) { + if (!child || seen.has(child)) continue; + out.push(child); + queue.push(child); + } + } + return out; +} + +function collectAncestorPids(pid, maxDepth = 10) { + const out = []; + let current = parseInt(pid, 10); + for (let i = 0; i < maxDepth; i += 1) { + if (!Number.isFinite(current) || current <= 1) break; + const ppidRes = spawnSync('ps', ['-o', 'ppid=', '-p', String(current)], { encoding: 'utf8' }); + const parent = parseInt(ppidRes.stdout?.trim(), 10); + if (!Number.isFinite(parent) || parent <= 1) break; + out.push(String(parent)); + current = parent; + } + return out; +} + +function getProcessCommand(pid) { + const res = spawnSync('ps', ['-o', 'args=', '-p', String(pid)], { encoding: 'utf8' }); + return (res.stdout || '').trim(); +} + +function isDockerInfrastructureProcess(pid) { + const cmd = getProcessCommand(pid).toLowerCase(); + if (!cmd) return false; + return ( + cmd.includes('com.docker') || + cmd.includes('docker desktop') || + cmd.includes('dockerd') || + cmd.includes('vpnkit') || + cmd.includes('containerd') || + (cmd.includes('docker') && cmd.includes('proxy')) + ); +} + +/** Nest / pnpm API dev parent — killing only the listener leaves watch respawning children. */ +function isReactPressApiProcess(pid, projectRoot) { + if (isDockerInfrastructureProcess(pid)) return false; + const cmd = getProcessCommand(pid); + if (!cmd) return false; + if ( + /nest start|api-dev-runner|server\/dist\/starter|@nestjs\/cli|reactpress\.js/.test(cmd) + ) { + return true; + } + const serverDir = path.join(path.resolve(projectRoot), 'server'); + const res = spawnSync('lsof', ['-p', String(pid)], { encoding: 'utf8' }); + if (res.status !== 0) return false; + return res.stdout.split('\n').some((line) => { + if (!line.includes(' cwd ')) return false; + const parts = line.trim().split(/\s+/); + const cwd = parts[parts.length - 1]; + return cwd === serverDir || cwd.startsWith(`${serverDir}${path.sep}`); + }); +} + +function signalPidSet(pids, signal) { + const flag = signal === 'TERM' ? '-TERM' : '-9'; + for (const pid of pids) { + if (!pid || pid === String(process.pid)) continue; + spawnSync('kill', [flag, pid], { stdio: 'ignore' }); + } +} + +function collectApiPortProcessTree(projectRoot, port) { + const toSignal = new Set(); + for (const pid of getListenerPids(port)) { + if (isDockerInfrastructureProcess(pid)) continue; + toSignal.add(pid); + for (const child of collectDescendantPids(pid)) { + if (!isDockerInfrastructureProcess(child)) toSignal.add(child); + } + for (const ancestor of collectAncestorPids(pid)) { + if (isReactPressApiProcess(ancestor, projectRoot)) toSignal.add(ancestor); + } + } + return toSignal; +} + +/** Stop Nest / api-dev listeners on `port` (TERM then KILL). */ +async function stopApiPortListeners(projectRoot, port, { label = 'API' } = {}) { + const n = parseInt(port, 10); + if (!Number.isInteger(n) || n < 1 || !isPortListening(n)) return true; + + console.warn( + `[reactpress] Port ${n} (${label}) is busy — stopping existing API processes…`, + ); + + const toSignal = collectApiPortProcessTree(projectRoot, n); + signalPidSet(toSignal, 'TERM'); + await sleep(600); + + const deadline = Date.now() + 10_000; + while (Date.now() < deadline) { + if (!isPortListening(n)) return true; + for (const pid of getListenerPids(n)) { + toSignal.add(pid); + for (const child of collectDescendantPids(pid)) toSignal.add(child); + } + signalPidSet(toSignal, 'KILL'); + await sleep(500); + } + + if (isPortListening(n)) { + console.warn( + `[reactpress] Port ${n} (${label}) still in use — try: lsof -tiTCP:${n} -sTCP:LISTEN | xargs kill -9`, + ); + return false; + } + return true; +} + +/** + * Free API port: skip when health check passes; otherwise stop listener + nest watch tree. + * @returns {{ reused: boolean, port: number }} + */ +async function ensureApiPortFree(projectRoot, { allowReuse = true } = {}) { + const port = readEnvPort(projectRoot, 'SERVER_PORT', DEV_PORTS.API); + const { getHealthUrl, checkHealth } = require('./http'); + if (allowReuse) { + const health = await checkHealth(getHealthUrl(projectRoot), 1500); + if (health.ok) { + return { reused: true, port }; + } + } + + if (!isPortListening(port)) { + return { reused: false, port }; + } + + await stopApiPortListeners(projectRoot, port); + return { reused: false, port }; +} + +/** Always stop API listeners — used when replacing a prior `reactpress dev` session. */ +async function forceReleaseApiPort(projectRoot) { + const port = readEnvPort(projectRoot, 'SERVER_PORT', DEV_PORTS.API); + if (!isPortListening(port)) return port; + console.warn(`[reactpress] Releasing API port ${port} for new dev session…`); + await stopApiPortListeners(projectRoot, port); + return port; +} + +/** Stop orphaned dev-stack listeners after session takeover or crash. */ +async function forceReleaseDevStackPorts(projectRoot) { + await forceReleaseApiPort(projectRoot); + + const previewPort = readEnvPort( + projectRoot, + 'REACTPRESS_PREVIEW_PORT', + DEV_PORTS.THEME_PREVIEW, + ); + const adminPort = readEnvPort(projectRoot, 'WEB_ADMIN_PORT', DEV_PORTS.ADMIN_WEB); + const visitorPort = readVisitorPort(projectRoot); + + for (const [port, label] of [ + [adminPort, 'admin'], + [visitorPort, 'visitor site'], + [previewPort, 'theme preview'], + ]) { + await ensurePortFree(port, { label }); + } +} + +/** + * Free only unhealthy or non-API listeners — keeps a healthy API to skip Nest cold compile. + */ +async function releaseStaleDevStackPorts(projectRoot) { + if (process.env.REACTPRESS_FORCE_PORT_RESET === '1') { + await forceReleaseDevStackPorts(projectRoot); + return; + } + + const { getHealthUrl, checkHealth } = require('./http'); + const apiPort = resolveStackPort(projectRoot, 'SERVER_PORT', DEV_PORTS.API); + + if (isPortListening(apiPort)) { + const healthUrl = `http://127.0.0.1:${apiPort}/api/health`; + const health = await checkHealth(healthUrl, 2000); + if (health.ok) { + const { logDevDetail } = require('./dev-log'); + logDevDetail('dev.apiKept', { port: apiPort }); + } else { + await stopApiPortListeners(projectRoot, apiPort); + } + } + + const previewPort = readEnvPort( + projectRoot, + 'REACTPRESS_PREVIEW_PORT', + DEV_PORTS.THEME_PREVIEW, + ); + const adminPort = readEnvPort(projectRoot, 'WEB_ADMIN_PORT', DEV_PORTS.ADMIN_WEB); + const visitorPort = readVisitorPort(projectRoot); + + for (const [port, label] of [ + [adminPort, 'admin'], + [visitorPort, 'visitor site'], + [previewPort, 'theme preview'], + ]) { + if (isPortListening(port)) { + await ensurePortFree(port, { label, maxWaitMs: 5000 }); + } + } +} + +/** + * If `port` is in use, terminate listeners (TERM then KILL) and wait until free. + */ +async function ensurePortFree(port, { label = 'service', maxWaitMs = 8000 } = {}) { + const n = parseInt(port, 10); + if (!Number.isInteger(n) || n < 1) return false; + + if (PROTECTED_KILL_PORTS.has(n)) { + if (isPortListening(n)) { + console.warn(`[reactpress] Port ${n} (${label}) is protected — leaving existing listener`); + } + return true; + } + + if (process.env.REACTPRESS_DESKTOP_LOCAL === '1') { + const desktopApi = process.env.REACTPRESS_DESKTOP_LOCAL_API?.trim(); + if (desktopApi) { + try { + const desktopPort = parseInt(new URL(desktopApi).port || String(DEV_PORTS.API), 10); + if (Number.isInteger(desktopPort) && desktopPort === n) { + if (isPortListening(n)) { + console.warn( + `[reactpress] Port ${n} (${label}) is the embedded local API — leaving listener`, + ); + } + return true; + } + } catch { + // ignore malformed REACTPRESS_DESKTOP_LOCAL_API + } + } + } + + if (!isPortListening(n)) return true; + + console.warn(`[reactpress] Port ${n} (${label}) is busy — stopping existing listener…`); + killPortListeners(n, 'TERM'); + await sleep(500); + + const deadline = Date.now() + maxWaitMs; + while (Date.now() < deadline) { + if (!isPortListening(n)) return true; + killPortListeners(n, 'KILL'); + await sleep(500); + } + + if (isPortListening(n)) { + console.warn( + `[reactpress] Port ${n} (${label}) still in use — try: lsof -tiTCP:${n} -sTCP:LISTEN | xargs kill -9`, + ); + return false; + } + return true; +} + +function isPortBindable(port) { + return new Promise((resolve) => { + const server = net.createServer(); + server.once('error', () => resolve(false)); + server.once('listening', () => { + server.close(() => resolve(true)); + }); + server.listen(port, '127.0.0.1'); + }); +} + +/** + * Pick an API port: reuse healthy listener, else preferred, else next free slot in range. + * @returns {{ port: number, reused: boolean, shifted?: boolean }} + */ +async function resolveApiPortForBind(projectRoot, { preferred } = {}) { + const start = preferred ?? resolveStackPort(projectRoot, 'SERVER_PORT', DEV_PORTS.API); + const healthUrl = `http://127.0.0.1:${start}/api/health`; + const { checkHealth } = require('./http'); + + const health = await checkHealth(healthUrl, 1500); + if (health.ok) { + return { port: start, reused: true }; + } + + if (!isPortListening(start)) { + return { port: start, reused: false }; + } + + for (let port = start; port < start + 20; port += 1) { + if (!isPortListening(port) && (await isPortBindable(port))) { + console.warn(`[reactpress] API port ${start} is busy — using :${port}`); + return { port, reused: false, shifted: true }; + } + } + + throw new Error(`No available API port near ${start} (set SERVER_PORT or REACTPRESS_INSTANCE)`); +} + +/** Free theme/admin ports (API handled in {@link forceReleaseDevStackPorts} / {@link spawnApi}). */ +async function ensureDevStackPorts(projectRoot) { + const previewPort = readEnvPort( + projectRoot, + 'REACTPRESS_PREVIEW_PORT', + DEV_PORTS.THEME_PREVIEW, + ); + const adminPort = readEnvPort(projectRoot, 'WEB_ADMIN_PORT', DEV_PORTS.ADMIN_WEB); + const visitorPort = readVisitorPort(projectRoot); + + for (const [port, label] of [ + [adminPort, 'admin'], + [visitorPort, 'visitor site'], + [previewPort, 'theme preview'], + ]) { + await ensurePortFree(port, { label }); + } +} + +module.exports = { + DEV_PORTS, + INSTANCE_PORT_STEP, + BLOCKED_THEME_DEV_PORTS, + readEnvPort, + readVisitorPort, + readInstanceIndex, + instancePortOffset, + resolveStackPort, + resolveDevStackPorts, + applyDevStackPortsToEnv, + resolveApiPortForBind, + devSessionSuffix, + isPortListening, + killPortListeners, + ensurePortFree, + ensureApiPortFree, + forceReleaseApiPort, + forceReleaseDevStackPorts, + releaseStaleDevStackPorts, + ensureDevStackPorts, +}; diff --git a/cli/src/lib/process.ts b/cli/src/lib/process.ts new file mode 100644 index 00000000..4d1cda2d --- /dev/null +++ b/cli/src/lib/process.ts @@ -0,0 +1,71 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const { getPidFile, getClientPidFile } = require('./paths'); + +function readPidFromFile(pidFile) { + try { + const raw = fs.readFileSync(pidFile, 'utf8').trim(); + const pid = Number.parseInt(raw, 10); + return Number.isFinite(pid) ? pid : null; + } catch { + return null; + } +} + +function readPid(projectRoot) { + return readPidFromFile(getPidFile(projectRoot)); +} + +function readClientPid(projectRoot) { + return readPidFromFile(getClientPidFile(projectRoot)); +} + +function isProcessRunning(pid) { + if (!pid) return false; + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +function clearPidFile(projectRoot) { + const pidFile = getPidFile(projectRoot); + if (fs.existsSync(pidFile)) { + fs.unlinkSync(pidFile); + } +} + +function clearClientPidFile(projectRoot) { + const pidFile = getClientPidFile(projectRoot); + if (fs.existsSync(pidFile)) { + fs.unlinkSync(pidFile); + } +} + +function writePidToFile(pidFile, pid) { + fs.mkdirSync(path.dirname(pidFile), { recursive: true }); + fs.writeFileSync(pidFile, String(pid)); +} + +function writePid(projectRoot, pid) { + writePidToFile(getPidFile(projectRoot), pid); +} + +function writeClientPid(projectRoot, pid) { + writePidToFile(getClientPidFile(projectRoot), pid); +} + +module.exports = { + readPid, + readClientPid, + isProcessRunning, + clearPidFile, + clearClientPidFile, + writePid, + writeClientPid, + getPidFile, + getClientPidFile, +}; diff --git a/cli/src/lib/prod-memory.ts b/cli/src/lib/prod-memory.ts new file mode 100644 index 00000000..3c50572d --- /dev/null +++ b/cli/src/lib/prod-memory.ts @@ -0,0 +1,55 @@ +// @ts-nocheck +/** + * Optional production memory tuning — only active when REACTPRESS_LOW_MEM=1. + * Default deploy does not set this; all limits stay at normal Node/PM2 defaults. + */ + +function isLowMemMode() { + return process.env.REACTPRESS_LOW_MEM === '1'; +} + +/** Apply heap cap only when low-mem or explicitly configured. */ +function resolveBuildMaxOldSpaceMb() { + const fromEnv = parseInt(process.env.REACTPRESS_BUILD_MAX_OLD_SPACE_MB || '', 10); + if (Number.isInteger(fromEnv) && fromEnv >= 256) return fromEnv; + if (isLowMemMode()) return 768; + return null; +} + +function resolveBuildNodeEnv(baseEnv = process.env) { + const mb = resolveBuildMaxOldSpaceMb(); + if (!mb) return { ...baseEnv }; + const flag = `--max-old-space-size=${mb}`; + const existing = baseEnv.NODE_OPTIONS || ''; + if (existing.includes('max-old-space-size')) { + return { ...baseEnv }; + } + return { + ...baseEnv, + NODE_OPTIONS: existing ? `${existing} ${flag}` : flag, + }; +} + +function getPm2ServerMemoryRestart() { + if (process.env.REACTPRESS_PM2_SERVER_MEMORY) { + return process.env.REACTPRESS_PM2_SERVER_MEMORY; + } + if (isLowMemMode()) return '384M'; + return '1G'; +} + +function getPm2ClientMemoryRestart() { + if (process.env.REACTPRESS_PM2_CLIENT_MEMORY) { + return process.env.REACTPRESS_PM2_CLIENT_MEMORY; + } + if (isLowMemMode()) return '512M'; + return '1G'; +} + +module.exports = { + isLowMemMode, + resolveBuildMaxOldSpaceMb, + resolveBuildNodeEnv, + getPm2ServerMemoryRestart, + getPm2ClientMemoryRestart, +}; diff --git a/cli/src/lib/project-logs.ts b/cli/src/lib/project-logs.ts new file mode 100644 index 00000000..c09320e9 --- /dev/null +++ b/cli/src/lib/project-logs.ts @@ -0,0 +1,534 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); +const { brand, divider, gradientText, palette, sectionHeader } = require('../ui/theme'); +const { getServerDir, getPidFile } = require('./paths'); +const { readPid, isProcessRunning } = require('./process'); +const { loadServerSiteUrl } = require('./http'); +const { + getPm2App, + PM2_API_APP, + isPm2AppOnline, + getPm2AppLogPaths, + resolvePm2Bin, +} = require('./pm2-runtime'); +const { t } = require('./i18n'); + +const LOG_SOURCES = ['error', 'request', 'response']; +const PROCESS_LOG_SOURCES = ['pm2-out', 'pm2-error']; +const PM2_LOG_FILES = { + 'pm2-out': 'pm2-out.log', + 'pm2-error': 'pm2-error.log', +}; + +function measureLogDir(logDir) { + if (!fs.existsSync(logDir)) return -1; + let total = listLogFiles(logDir, 'all').reduce((sum, file) => sum + file.size, 0); + for (const filename of Object.values(PM2_LOG_FILES)) { + const filePath = path.join(logDir, filename); + try { + if (fs.existsSync(filePath)) { + total += fs.statSync(filePath).size; + } + } catch { + // ignore + } + } + return total; +} + +function getPm2ServerLogDir(projectRoot) { + try { + const app = getPm2App(projectRoot, PM2_API_APP); + const dir = app?.pm2_env?.env?.REACTPRESS_SERVER_LOG_DIR; + if (typeof dir === 'string' && dir.trim()) { + return path.resolve(dir.trim()); + } + } catch { + // ignore + } + return null; +} + +function parseUrlPort(urlString) { + try { + const url = new URL(urlString); + const port = Number(url.port); + if (port > 0) return port; + return url.protocol === 'https:' ? 443 : 80; + } catch { + return null; + } +} + +function resolveListeningPid(port) { + if (!port || process.platform === 'win32') return null; + const result = spawnSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }); + if (result.status !== 0 || !result.stdout.trim()) return null; + const pids = result.stdout + .trim() + .split('\n') + .map((line) => Number.parseInt(line.trim(), 10)) + .filter((pid) => Number.isFinite(pid)); + return pids[0] ?? null; +} + +function resolveLogDirFromPid(pid) { + if (!pid || process.platform === 'win32') return null; + const result = spawnSync('lsof', ['-Fn', '-p', String(pid)], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }); + if (result.status !== 0) return null; + for (const line of result.stdout.split('\n')) { + if (!line.startsWith('n') || !line.includes('.log')) continue; + const logPath = line.slice(1); + const match = logPath.match(/^(.*\/logs)\/(?:error|request|response)\//); + if (match) return match[1]; + } + return null; +} + +function resolveActiveServerLogDir(projectRoot) { + const port = parseUrlPort(loadServerSiteUrl(projectRoot)); + if (!port) return null; + const pid = resolveListeningPid(port); + if (!pid) return null; + return resolveLogDirFromPid(pid); +} + +function collectLogDirCandidates(projectRoot) { + const candidates = [ + path.join(projectRoot, '.reactpress', 'logs', 'server'), + path.join(getServerDir(projectRoot), 'logs'), + ]; + const pm2Dir = getPm2ServerLogDir(projectRoot); + if (pm2Dir) candidates.push(pm2Dir); + const activeDir = resolveActiveServerLogDir(projectRoot); + if (activeDir) candidates.push(activeDir); + return [...new Set(candidates.map((dir) => path.resolve(dir)))]; +} + +function getProjectServerLogDir(projectRoot) { + const preferred = path.join(projectRoot, '.reactpress', 'logs', 'server'); + const bundled = path.join(getServerDir(projectRoot), 'logs'); + const pm2Dir = getPm2ServerLogDir(projectRoot); + const activeDir = resolveActiveServerLogDir(projectRoot); + + const localCandidates = [preferred, bundled]; + if (pm2Dir) localCandidates.push(pm2Dir); + + let bestLocal = preferred; + let bestLocalScore = -1; + for (const dir of [...new Set(localCandidates.map((d) => path.resolve(d)))]) { + const score = measureLogDir(dir); + if (score > bestLocalScore) { + bestLocalScore = score; + bestLocal = dir; + } + } + + if (bestLocalScore > 0) { + return bestLocal; + } + + if (activeDir && fs.existsSync(activeDir) && measureLogDir(activeDir) > 0) { + return activeDir; + } + + return bestLocal; +} + +function normalizeSource(source) { + const value = String(source || 'error').toLowerCase(); + if (value === 'all') return 'all'; + if (value === 'process' || value === 'pm2') return 'process'; + if (LOG_SOURCES.includes(value)) return value; + return 'error'; +} + +function measureStructuredLogDir(logDir) { + if (!fs.existsSync(logDir)) return 0; + let total = 0; + for (const name of LOG_SOURCES) { + const dir = path.join(logDir, name); + if (!fs.existsSync(dir)) continue; + for (const entry of fs.readdirSync(dir)) { + if (!entry.endsWith('.log')) continue; + try { + total += fs.statSync(path.join(dir, entry)).size; + } catch { + // ignore + } + } + } + return total; +} + +function resolveLogSource(projectRoot, sourceOption) { + if (sourceOption != null && String(sourceOption).trim() !== '') { + return normalizeSource(sourceOption); + } + const logDir = path.join(projectRoot, '.reactpress', 'logs', 'server'); + if (measureStructuredLogDir(logDir) > 0) { + return 'error'; + } + if (isPm2AppOnline(projectRoot, PM2_API_APP)) { + return 'process'; + } + return 'error'; +} + +function listProcessLogFiles(projectRoot, logDir, source = 'process') { + const normalized = normalizeSource(source); + const names = + normalized === 'all' + ? PROCESS_LOG_SOURCES + : normalized === 'process' + ? PROCESS_LOG_SOURCES + : PROCESS_LOG_SOURCES.includes(normalized) + ? [normalized] + : []; + if (!names.length) return []; + + const pm2Paths = getPm2AppLogPaths(projectRoot, PM2_API_APP); + const pathBySource = { + 'pm2-out': pm2Paths.out, + 'pm2-error': pm2Paths.err, + }; + const files = []; + + for (const name of names) { + const localPath = path.join(logDir, PM2_LOG_FILES[name]); + const candidates = []; + if (fs.existsSync(localPath)) { + candidates.push(localPath); + } + const remotePath = pathBySource[name]; + if ( + remotePath && + path.resolve(remotePath) !== path.resolve(localPath) && + fs.existsSync(remotePath) + ) { + candidates.push(remotePath); + } + const seen = new Set(); + for (const fullPath of candidates) { + const resolved = path.resolve(fullPath); + if (seen.has(resolved)) continue; + seen.add(resolved); + let stat; + try { + stat = fs.statSync(resolved); + } catch { + continue; + } + if (!stat.isFile()) continue; + files.push({ + source: name, + path: resolved, + name: path.basename(resolved), + mtimeMs: stat.mtimeMs, + size: stat.size, + }); + break; + } + } + + files.sort((a, b) => b.mtimeMs - a.mtimeMs); + return files; +} + +function listLogFiles(logDir, source = 'error', projectRoot = null) { + const normalized = normalizeSource(source); + if (normalized === 'process' || PROCESS_LOG_SOURCES.includes(normalized)) { + return listProcessLogFiles(projectRoot || '', logDir, normalized); + } + + const sources = + normalized === 'all' + ? [...PROCESS_LOG_SOURCES, ...LOG_SOURCES] + : [normalized]; + const files = []; + + if (normalized === 'all' && projectRoot) { + files.push(...listProcessLogFiles(projectRoot, logDir, 'process')); + } + + for (const name of sources) { + if (PROCESS_LOG_SOURCES.includes(name)) continue; + const dir = path.join(logDir, name); + if (!fs.existsSync(dir)) continue; + for (const entry of fs.readdirSync(dir)) { + if (!entry.endsWith('.log')) continue; + const fullPath = path.join(dir, entry); + let stat; + try { + stat = fs.statSync(fullPath); + } catch { + continue; + } + if (!stat.isFile()) continue; + files.push({ + source: name, + path: fullPath, + name: entry, + mtimeMs: stat.mtimeMs, + size: stat.size, + }); + } + } + + files.sort((a, b) => b.mtimeMs - a.mtimeMs); + return files; +} + +function readTailLines(filePath, lineCount = 50) { + const maxLines = Math.max(1, Math.min(Number(lineCount) || 50, 5000)); + let content; + try { + content = fs.readFileSync(filePath, 'utf8'); + } catch { + return []; + } + if (!content) return []; + const lines = content.split(/\r?\n/); + if (lines.length && lines[lines.length - 1] === '') { + lines.pop(); + } + if (lines.length <= maxLines) return lines; + return lines.slice(-maxLines); +} + +function filterLines(lines, pattern) { + if (!pattern) return lines; + let re; + try { + re = new RegExp(pattern, 'i'); + } catch { + const err = new Error(t('doctor.logs.badPattern', { pattern })); + err.code = 'REACTPRESS_LOG_BAD_PATTERN'; + throw err; + } + return lines.filter((line) => re.test(line)); +} + +function formatBytes(size) { + if (size < 1024) return `${size} B`; + if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; + return `${(size / (1024 * 1024)).toFixed(1)} MB`; +} + +function formatTimestamp(ms) { + return new Date(ms).toISOString().replace('T', ' ').slice(0, 19); +} + +function printProcessHint(projectRoot) { + if (isPm2AppOnline(projectRoot, PM2_API_APP)) { + const pid = getPm2App(projectRoot, PM2_API_APP)?.pid; + console.log( + ` ${brand.dim(t('doctor.logs.pm2Supervised', { pid: pid ?? PM2_API_APP }))}` + ); + return; + } + + const pid = readPid(projectRoot); + const pidFile = getPidFile(projectRoot); + if (!pid) { + const port = parseUrlPort(loadServerSiteUrl(projectRoot)); + const activePid = port ? resolveListeningPid(port) : null; + if (activePid && isProcessRunning(activePid)) { + console.log( + ` ${brand.dim(t('doctor.logs.activePid', { pid: activePid, port }))}` + ); + return; + } + console.log(` ${brand.dim(t('doctor.logs.noPid', { path: pidFile }))}`); + return; + } + const alive = isProcessRunning(pid); + console.log( + ` ${brand.dim( + alive + ? t('doctor.logs.pidRunning', { pid, path: pidFile }) + : t('doctor.logs.pidStale', { pid, path: pidFile }) + )}` + ); +} + +function pickPrimaryLogFile(files, source) { + if (!files.length) return null; + if (source === 'process') { + return ( + files.find((file) => file.source === 'pm2-error') || + files.find((file) => file.source === 'pm2-out') || + files[0] + ); + } + const grouped = new Map(); + for (const file of files) { + if (!grouped.has(file.source)) grouped.set(file.source, file); + } + const order = source === 'all' ? [...PROCESS_LOG_SOURCES, ...LOG_SOURCES] : [source]; + for (const name of order) { + const file = grouped.get(name); + if (file) return file; + } + return files[0]; +} + +async function runFollowLogs(projectRoot, { tail, source, logDir, files }) { + const usePm2Stream = source === 'process' && isPm2AppOnline(projectRoot, PM2_API_APP); + if (usePm2Stream) { + const bin = resolvePm2Bin(projectRoot); + if (!bin) { + console.error(` ${brand.warn(t('doctor.logs.pm2Unavailable'))}`); + return 1; + } + const { spawn } = require('child_process'); + await new Promise((resolve) => { + const child = spawn(bin, ['logs', PM2_API_APP, '--lines', String(tail)], { + stdio: 'inherit', + }); + child.on('exit', (code) => resolve(code ?? 0)); + }); + return 0; + } + + const primary = pickPrimaryLogFile(files, source); + if (!primary) { + console.log(` ${brand.warn(t('doctor.logs.empty'))}`); + console.log(''); + return 1; + } + + if (process.platform === 'win32') { + console.log(` ${brand.dim(t('doctor.logs.followWindows', { path: primary.path }))}`); + console.log(''); + return 0; + } + + const { spawn } = require('child_process'); + await new Promise((resolve) => { + const child = spawn('tail', ['-n', String(tail), '-f', primary.path], { + stdio: 'inherit', + }); + child.on('exit', (code) => resolve(code ?? 0)); + }); + return 0; +} + +async function runDoctorLogs(projectRoot, options = {}) { + const tail = Math.max(1, Math.min(parseInt(String(options.tail ?? '50'), 10) || 50, 5000)); + const source = resolveLogSource(projectRoot, options.source); + const logDir = getProjectServerLogDir(projectRoot); + const files = listLogFiles(logDir, source, projectRoot); + const w = 56; + + console.log(''); + console.log( + ` ${gradientText(t('logs.title'), [palette.primary, palette.accent], { bold: true })} ${brand.dim(t('logs.subtitle'))}` + ); + console.log(` ${brand.dim(t('doctor.project', { path: projectRoot }))}`); + console.log(` ${brand.dim(t('doctor.logs.dir', { path: logDir }))}`); + printProcessHint(projectRoot); + console.log(` ${divider(w)}`); + + if (options.follow) { + return runFollowLogs(projectRoot, { tail, source, logDir, files }); + } + + if (options.list) { + if (!files.length) { + console.log(` ${brand.warn(t('doctor.logs.empty'))}`); + console.log(` ${brand.dim(t('doctor.logs.emptyHint'))}`); + console.log(''); + return 1; + } + console.log(sectionHeader(t('doctor.logs.listHeader'))); + for (const file of files) { + const rel = path.join(file.source, file.name); + console.log( + ` ${brand.primary(rel)} ${brand.dim(`${formatBytes(file.size)} ${formatTimestamp(file.mtimeMs)}`)}` + ); + } + console.log(''); + return 0; + } + + if (!files.length) { + console.log(` ${brand.warn(t('doctor.logs.empty'))}`); + console.log(` ${brand.dim(t('doctor.logs.emptyHint'))}`); + console.log(''); + return 1; + } + + const grouped = new Map(); + for (const file of files) { + if (!grouped.has(file.source)) grouped.set(file.source, file); + } + + const displayOrder = + source === 'all' + ? [...PROCESS_LOG_SOURCES, ...LOG_SOURCES] + : source === 'process' + ? PROCESS_LOG_SOURCES + : [source]; + + let printed = 0; + for (const name of displayOrder) { + const file = grouped.get(name); + if (!file) continue; + let lines = readTailLines(file.path, tail); + lines = filterLines(lines, options.grep); + const rel = path.join(file.source, file.name); + console.log(''); + console.log( + sectionHeader( + t('doctor.logs.fileHeader', { + file: rel, + count: lines.length, + }) + ) + ); + if (!lines.length) { + console.log(` ${brand.dim(t('doctor.logs.noMatchingLines'))}`); + continue; + } + for (const line of lines) { + const isError = + /\[ERROR\]/i.test(line) || name === 'error' || name === 'pm2-error'; + console.log(` ${isError ? brand.warn(line) : brand.dim(line)}`); + } + printed += lines.length; + } + + if (!printed) { + console.log(` ${brand.warn(t('doctor.logs.noMatchingLines'))}`); + console.log(''); + return 1; + } + + console.log(''); + console.log(` ${brand.dim(t('doctor.logs.moreHint'))}`); + console.log(''); + return 0; +} + +module.exports = { + LOG_SOURCES, + PROCESS_LOG_SOURCES, + getProjectServerLogDir, + collectLogDirCandidates, + measureLogDir, + resolveActiveServerLogDir, + resolveLogSource, + listLogFiles, + readTailLines, + filterLines, + runDoctorLogs, +}; diff --git a/cli/src/lib/project-type.ts b/cli/src/lib/project-type.ts new file mode 100644 index 00000000..6d497c22 --- /dev/null +++ b/cli/src/lib/project-type.ts @@ -0,0 +1,103 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); + +/** + * Decide whether a given directory is a ReactPress monorepo checkout (with + * editable `server/src`, `web/`, `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')); +} + +/** + * Admin SPA (`web/`), preferred over client `/admin` in monorepo dev. + * @param {string} root + */ +function hasWeb(root) { + return fs.existsSync(path.join(root, 'web', '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')); +} + +/** + * Electron desktop client (`desktop/`). + * @param {string} root + */ +function hasDesktop(root) { + return fs.existsSync(path.join(root, 'desktop', 'package.json')); +} + +/** + * Official plugins workspace (`plugins/`). + * @param {string} root + */ +function hasPluginsWorkspace(root) { + return fs.existsSync(path.join(root, 'plugins', 'package.json')); +} + +/** + * @param {string} root + */ +function describeProject(root) { + const type = detectProjectType(root); + return { + type, + root, + hasClient: hasClient(root), + hasWeb: hasWeb(root), + hasServerSource: hasServerSource(root), + hasToolkit: hasToolkit(root), + hasDesktop: hasDesktop(root), + hasPluginsWorkspace: hasPluginsWorkspace(root), + }; +} + +module.exports = { + detectProjectType, + describeProject, + hasClient, + hasWeb, + hasServerSource, + hasToolkit, + hasDesktop, + hasPluginsWorkspace, +}; diff --git a/cli/src/lib/publish.ts b/cli/src/lib/publish.ts new file mode 100644 index 00000000..0dc90f0a --- /dev/null +++ b/cli/src/lib/publish.ts @@ -0,0 +1,427 @@ +#!/usr/bin/env node +// @ts-nocheck + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); +const inquirer = require('inquirer'); +const { t } = require('./i18n'); +const { getMonorepoRoot } = require('./root'); + +const SEMVER_RE = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?$/; +const NPM_REGISTRY = 'https://registry.npmjs.org'; + +/** Publish order: dependencies first, CLI last. */ +const CORE_PUBLISH_PACKAGES = [ + { + name: '@fecommunity/reactpress-toolkit', + path: 'toolkit', + description: 'Official TypeScript SDK — API clients, theme SSR, plugin hooks', + }, + { + name: '@fecommunity/reactpress-web', + path: 'web', + description: 'Official Admin SPA — WordPress-style writing UI', + }, + { + name: '@fecommunity/reactpress-server', + path: 'server', + description: t('publish.pkg.server'), + deprecated: true, + }, + { + name: '@fecommunity/reactpress', + path: 'cli', + description: 'Publishing platform CLI — Publish with React. Ship like WordPress.', + }, +]; + +function getWorkspaceRoot() { + const root = getMonorepoRoot(); + if (fs.existsSync(path.join(root, 'pnpm-workspace.yaml'))) { + return root; + } + return process.cwd(); +} + +function getCurrentVersion(packagePath) { + const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + return pkg.version; +} + +function getCanonicalVersion() { + return getCurrentVersion('cli'); +} + +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 = String(version).match(/^(.*)-beta\.(\d+)$/); + if (match) return `${match[1]}-beta.${parseInt(match[2], 10) + 1}`; + return `${base}-beta.0`; + } + default: + return version; + } +} + +function resolveNpmTag(version, explicitTag) { + if (explicitTag) return explicitTag; + return String(version).includes('-') ? 'beta' : 'latest'; +} + +function parseCliPublishOptions(argv = process.argv.slice(2)) { + const opts = { + publish: argv.includes('--publish'), + build: argv.includes('--build'), + noBuild: argv.includes('--no-build'), + yes: argv.includes('--yes'), + tag: undefined, + version: undefined, + otp: process.env.NPM_OTP || undefined, + }; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === '--tag' && argv[i + 1]) opts.tag = argv[++i]; + else if (arg.startsWith('--tag=')) opts.tag = arg.slice('--tag='.length); + else if (arg === '--version' && argv[i + 1]) opts.version = argv[++i]; + else if (arg.startsWith('--version=')) opts.version = arg.slice('--version='.length); + else if (arg === '--otp' && argv[i + 1]) opts.otp = argv[++i]; + else if (arg.startsWith('--otp=')) opts.otp = arg.slice('--otp='.length); + } + + return opts; +} + +function printPackageVersions() { + console.log(chalk.cyan('📋 Package versions:')); + for (const pkg of CORE_PUBLISH_PACKAGES) { + console.log(chalk.gray(` ${pkg.name}: ${getCurrentVersion(pkg.path)}`)); + } + console.log(chalk.gray(` reactpress (root): ${getCurrentVersion('.')}`)); + console.log(); +} + +function checkEnvironment() { + try { + execSync('pnpm --version', { stdio: 'ignore' }); + } catch { + console.log(chalk.red('❌ pnpm is not installed.')); + return false; + } + + try { + execSync(`pnpm whoami --registry ${NPM_REGISTRY}`, { stdio: 'ignore' }); + } catch { + console.log( + chalk.red(`❌ Not logged in to npm. Run: pnpm login --registry ${NPM_REGISTRY}`), + ); + return false; + } + + return true; +} + +function updateVersion(packagePath, newVersion) { + const root = getWorkspaceRoot(); + const pkgPath = path.join(root, packagePath, 'package.json'); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + const oldVersion = pkg.version; + pkg.version = newVersion; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); + console.log(chalk.green(` ✓ ${packagePath}: ${oldVersion} → ${newVersion}`)); +} + +function syncMonorepoVersions(targetVersion) { + console.log(chalk.blue(`\n✏️ Syncing version → ${targetVersion}`)); + updateVersion('.', targetVersion); + for (const pkg of CORE_PUBLISH_PACKAGES) { + updateVersion(pkg.path, targetVersion); + } + const desktopPkg = path.join(getWorkspaceRoot(), 'desktop/package.json'); + if (fs.existsSync(desktopPkg)) { + updateVersion('desktop', targetVersion); + } +} + +function fixWorkspaceDependenciesForPublish(packagePath, packageVersions) { + const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + + for (const depType of ['dependencies', 'devDependencies', 'peerDependencies']) { + if (!pkg[depType]) continue; + for (const [depName, depValue] of Object.entries(pkg[depType])) { + if (!String(depValue).startsWith('workspace:')) continue; + const depPackage = CORE_PUBLISH_PACKAGES.find((p) => p.name === depName); + if (depPackage && packageVersions[depName]) { + pkg[depType][depName] = packageVersions[depName]; + } + } + } + + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); +} + +function restoreWorkspaceDependenciesAfterPublish(packagePath, publishedVersion) { + const pkgPath = path.join(getWorkspaceRoot(), packagePath, 'package.json'); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + + for (const depType of ['dependencies', 'devDependencies', 'peerDependencies']) { + if (!pkg[depType]) continue; + for (const [depName, depValue] of Object.entries(pkg[depType])) { + const depPackage = CORE_PUBLISH_PACKAGES.find((p) => p.name === depName); + if (depPackage && depValue === publishedVersion) { + pkg[depType][depName] = 'workspace:*'; + } + } + } + + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); +} + +function run(cmd, cwd) { + execSync(cmd, { cwd, stdio: 'inherit' }); +} + +function buildAllForPublish() { + const root = getWorkspaceRoot(); + console.log(chalk.blue('\n🔨 Building publish artifacts...\n')); + run('pnpm run build', path.join(root, 'toolkit')); + run('pnpm run build', path.join(root, 'server')); + run('pnpm run build', path.join(root, 'web')); + run('node scripts/sync-bundled-core.mjs', path.join(root, 'cli')); + run('node scripts/sync-monorepo-server.mjs', path.join(root, 'cli')); + run('node scripts/sync-bundled-toolkit.mjs', path.join(root, 'cli')); + run('node scripts/patch-bundled-dist.mjs', path.join(root, 'cli')); + run('node scripts/install-bundled-runtime.mjs', path.join(root, 'cli')); + run('pnpm run build', path.join(root, 'cli')); + console.log(chalk.green('\n✅ Build complete\n')); +} + +function publishPackage(packagePath, packageName, tag, otp) { + const otpFlag = otp ? ` --otp ${otp}` : ''; + const cmd = `pnpm publish --access public --tag ${tag} --registry ${NPM_REGISTRY} --no-git-checks${otpFlag}`; + console.log(chalk.blue(`\n🚀 ${packageName}@${getCurrentVersion(packagePath)} (${tag})`)); + run(cmd, path.join(getWorkspaceRoot(), packagePath)); + console.log(chalk.green(`✅ ${packageName} published`)); +} + +async function promptPublishPlan(defaults = {}) { + const current = getCanonicalVersion(); + const { channel } = await inquirer.prompt([ + { + type: 'list', + name: 'channel', + message: 'Release channel:', + choices: [ + { name: `Beta prerelease (npm tag: beta)`, value: 'beta' }, + { name: `Stable release (npm tag: latest)`, value: 'latest' }, + ], + default: defaults.tag === 'latest' ? 1 : 0, + }, + ]); + + const { versionMode } = await inquirer.prompt([ + { + type: 'list', + name: 'versionMode', + message: `Version (current: ${current}):`, + choices: [ + { name: `Keep ${current}`, value: 'keep' }, + { name: `Bump beta (${incrementVersion(current, 'beta')})`, value: 'beta' }, + { name: `Bump patch (${incrementVersion(current, 'patch')})`, value: 'patch' }, + { name: 'Enter custom version', value: 'custom' }, + ], + }, + ]); + + let version = current; + if (versionMode === 'beta' || versionMode === 'patch') { + version = incrementVersion(current, versionMode); + } else if (versionMode === 'custom') { + const { customVersion } = await inquirer.prompt([ + { + type: 'input', + name: 'customVersion', + message: 'Semver version for all core packages:', + default: current, + validate: (input) => SEMVER_RE.test(input) || 'Use semver, e.g. 4.0.0-beta.0', + }, + ]); + version = customVersion; + } + + const tag = channel === 'beta' ? 'beta' : resolveNpmTag(version, channel); + + console.log(chalk.cyan(`\nPlan: ${version} → npm tag "${tag}"\n`)); + printPackageVersions(); + + const { confirm } = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: 'Publish all core packages?', + default: false, + }, + ]); + + if (!confirm) { + console.log(chalk.yellow('Cancelled.')); + return null; + } + + return { version, tag, otp: process.env.NPM_OTP }; +} + +async function executePublish(plan, options = {}) { + const { version, tag, otp } = plan; + const noBuild = options.noBuild === true; + + if (!SEMVER_RE.test(version)) { + throw new Error(`Invalid semver: ${version}`); + } + + syncMonorepoVersions(version); + + if (!noBuild) { + buildAllForPublish(); + } + + const packageVersions = {}; + for (const pkg of CORE_PUBLISH_PACKAGES) { + packageVersions[pkg.name] = version; + } + + for (const pkg of CORE_PUBLISH_PACKAGES) { + fixWorkspaceDependenciesForPublish(pkg.path, packageVersions); + try { + publishPackage(pkg.path, pkg.name, tag, otp); + } finally { + restoreWorkspaceDependenciesAfterPublish(pkg.path, version); + } + } + + console.log(chalk.green(`\n🎉 Published ${CORE_PUBLISH_PACKAGES.length} packages @ ${version} (${tag})`)); + console.log(chalk.cyan('\nVerify:')); + console.log(chalk.gray(` npm view @fecommunity/reactpress dist-tags`)); + if (tag === 'beta') { + console.log(chalk.gray(` npm i -g @fecommunity/reactpress@beta`)); + } else { + console.log(chalk.gray(` npm i -g @fecommunity/reactpress@${version}`)); + } + console.log(chalk.cyan('\nNext:')); + console.log(chalk.gray(` git tag v${version} && git push && git push --tags`)); +} + +async function buildPackages() { + console.log(chalk.blue('🏗️ ReactPress publish build\n')); + printPackageVersions(); + buildAllForPublish(); +} + +async function publishPackages(cliOptions = {}) { + console.log(chalk.blue('📦 ReactPress Package Publisher\n')); + + if (!checkEnvironment()) { + process.exit(1); + } + + printPackageVersions(); + + let plan = null; + + if (cliOptions.version || cliOptions.tag) { + const version = cliOptions.version || getCanonicalVersion(); + const tag = resolveNpmTag(version, cliOptions.tag); + plan = { version, tag, otp: cliOptions.otp }; + + console.log(chalk.cyan(`Plan: ${version} → npm tag "${tag}"\n`)); + + if (!cliOptions.yes) { + const { confirm } = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: 'Publish all core packages?', + default: false, + }, + ]); + if (!confirm) { + console.log(chalk.yellow('Cancelled.')); + return; + } + } + } else if (cliOptions.yes) { + const version = getCanonicalVersion(); + plan = { version, tag: resolveNpmTag(version), otp: cliOptions.otp }; + } else { + plan = await promptPublishPlan(cliOptions); + if (!plan) return; + } + + await executePublish(plan, cliOptions); +} + +async function main() { + const opts = parseCliPublishOptions(); + + if (opts.build) { + await buildPackages(); + return; + } + + if (opts.publish) { + await publishPackages(opts); + return; + } + + console.log(chalk.blue('📦 ReactPress publish\n')); + console.log('Usage:'); + console.log(' pnpm run publish:build'); + console.log(' pnpm run publish:packages'); + console.log(''); + console.log('Options:'); + console.log(' --publish Publish (interactive if no --version/--yes)'); + console.log(' --build Build publish artifacts only'); + console.log(' --tag beta|latest npm dist-tag (default: auto from version)'); + console.log(' --version 4.0.0-beta.0 Target semver for all core packages'); + console.log(' --yes Skip confirmation'); + console.log(' --no-build Skip build before publish'); + console.log(' --otp npm 2FA (or NPM_OTP env)'); + console.log(''); + console.log('Examples:'); + console.log(' NPM_OTP=123456 pnpm run publish:packages -- --yes'); + console.log(' pnpm run publish:packages -- --tag beta --version 4.0.0-beta.0 --yes'); +} + +module.exports = { + main, + buildPackages, + publishPackages, + incrementVersion, + resolveNpmTag, + CORE_PUBLISH_PACKAGES, +}; + +if (require.main === module) { + main().catch((error) => { + console.error(chalk.red('❌ Publish failed:'), error.message || error); + process.exit(1); + }); +} diff --git a/cli/src/lib/remote-dev.ts b/cli/src/lib/remote-dev.ts new file mode 100644 index 00000000..bcd9aefd --- /dev/null +++ b/cli/src/lib/remote-dev.ts @@ -0,0 +1,177 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); + +/** Normalize user input (e.g. api.gaoredu.com) to an HTTPS origin without trailing slash. */ +function normalizeRemoteOrigin(input) { + const raw = typeof input === 'string' ? input.trim() : ''; + if (!raw) return null; + + let origin = raw; + if (!/^https?:\/\//i.test(origin)) { + origin = `https://${origin}`; + } + return origin.replace(/\/$/, ''); +} + +function readEnvValue(projectRoot, key) { + const envPath = path.join(projectRoot, '.env'); + try { + const content = fs.readFileSync(envPath, 'utf8'); + const match = content.match(new RegExp(`^${key}=(.+)$`, 'm')); + if (match) { + return match[1].trim().replace(/^['"]|['"]$/g, ''); + } + } catch { + // ignore + } + return null; +} + +function readOriginFromEnv(projectRoot, envKey) { + const fromShell = normalizeRemoteOrigin(process.env[envKey]); + if (fromShell) return fromShell; + return normalizeRemoteOrigin(readEnvValue(projectRoot, envKey)); +} + +/** Default remote API URL (--remote-origin or REACTPRESS_DEV_REMOTE_ORIGIN). */ +function readDevRemoteDefault(projectRoot) { + return readOriginFromEnv(projectRoot, 'REACTPRESS_DEV_REMOTE_ORIGIN'); +} + +/** @deprecated use readDevClientApiOrigin */ +function readDevRemoteOrigin(projectRoot) { + return readDevClientApiOrigin(projectRoot); +} + +/** Remote admin API origin; null = local Nest. */ +function readDevAdminApiOrigin(projectRoot) { + return readOriginFromEnv(projectRoot, 'REACTPRESS_DEV_ADMIN_API_ORIGIN'); +} + +/** Remote client/theme API origin (nginx /api); null = local Nest. */ +function readDevClientApiOrigin(projectRoot) { + return readOriginFromEnv(projectRoot, 'REACTPRESS_DEV_CLIENT_API_ORIGIN'); +} + +/** + * Parse one origin flag: local | remote | URL/host. + * @returns {{ url: string|null } | { error: string }} + */ +function parseOriginSpec(value, remoteDefault) { + const trimmed = typeof value === 'string' ? value.trim() : ''; + if (!trimmed) return { url: null }; + + const lower = trimmed.toLowerCase(); + if (lower === 'local') return { url: null }; + if (lower === 'remote') { + if (!remoteDefault) return { error: 'REMOTE_DEFAULT_REQUIRED' }; + return { url: remoteDefault }; + } + + const url = normalizeRemoteOrigin(trimmed); + if (!url) return { error: 'INVALID_ORIGIN' }; + return { url }; +} + +/** + * Resolve admin/client API targets for this dev session. + * @returns {{ admin: string|null, client: string|null, remoteDefault: string|null, needsLocalApi: boolean, error?: string }} + */ +function resolveDevApiOrigins(projectRoot, cli = {}) { + const remoteDefault = + normalizeRemoteOrigin(cli.remoteOrigin) || readDevRemoteDefault(projectRoot); + + const onlyRemoteShorthand = + cli.remoteOrigin !== undefined && + cli.adminOrigin === undefined && + cli.clientOrigin === undefined; + + const resolveSide = (cliValue, envKey, useRemoteShorthand) => { + if (cliValue !== undefined) { + return parseOriginSpec(cliValue, remoteDefault); + } + const fromEnv = readOriginFromEnv(projectRoot, envKey); + if (fromEnv) return { url: fromEnv }; + if (useRemoteShorthand && remoteDefault) return { url: remoteDefault }; + return { url: null }; + }; + + const adminParsed = resolveSide( + cli.adminOrigin, + 'REACTPRESS_DEV_ADMIN_API_ORIGIN', + onlyRemoteShorthand, + ); + if (adminParsed.error) return { error: adminParsed.error }; + + const clientParsed = resolveSide( + cli.clientOrigin, + 'REACTPRESS_DEV_CLIENT_API_ORIGIN', + onlyRemoteShorthand, + ); + if (clientParsed.error) return { error: clientParsed.error }; + + const admin = adminParsed.url; + const client = clientParsed.url; + + return { + admin, + client, + remoteDefault, + needsLocalApi: !admin || !client, + }; +} + +function applyDevApiOriginsToEnv(origins) { + if (origins.remoteDefault) { + process.env.REACTPRESS_DEV_REMOTE_ORIGIN = origins.remoteDefault; + } else { + delete process.env.REACTPRESS_DEV_REMOTE_ORIGIN; + } + + if (origins.admin) { + process.env.REACTPRESS_DEV_ADMIN_API_ORIGIN = origins.admin; + } else { + delete process.env.REACTPRESS_DEV_ADMIN_API_ORIGIN; + } + + if (origins.client) { + process.env.REACTPRESS_DEV_CLIENT_API_ORIGIN = origins.client; + } else { + delete process.env.REACTPRESS_DEV_CLIENT_API_ORIGIN; + } +} + +/** @deprecated use resolveDevApiOrigins */ +function applyDevRemoteOrigin(cliValue) { + const normalized = normalizeRemoteOrigin(cliValue); + if (normalized) { + process.env.REACTPRESS_DEV_REMOTE_ORIGIN = normalized; + process.env.REACTPRESS_DEV_ADMIN_API_ORIGIN = normalized; + process.env.REACTPRESS_DEV_CLIENT_API_ORIGIN = normalized; + } + return normalized; +} + +/** Nest client base URL (includes /api when origin is host-only). */ +function resolveRemoteThemeApiBase(origin) { + const base = String(origin || '') + .trim() + .replace(/\/$/, ''); + if (!base) return '/api'; + if (/\/api$/i.test(base)) return base; + return `${base}/api`; +} + +module.exports = { + normalizeRemoteOrigin, + readDevRemoteDefault, + readDevRemoteOrigin, + readDevAdminApiOrigin, + readDevClientApiOrigin, + parseOriginSpec, + resolveDevApiOrigins, + applyDevApiOriginsToEnv, + applyDevRemoteOrigin, + resolveRemoteThemeApiBase, +}; diff --git a/cli/src/lib/root.ts b/cli/src/lib/root.ts new file mode 100644 index 00000000..6de8ef4d --- /dev/null +++ b/cli/src/lib/root.ts @@ -0,0 +1,89 @@ +// @ts-nocheck +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. + */ +function getMonorepoRoot() { + const packageRoot = path.resolve(__dirname, '..'); + const parentOfPackage = path.resolve(__dirname, '../..'); + if (fs.existsSync(path.join(parentOfPackage, 'pnpm-workspace.yaml'))) { + return parentOfPackage; + } + 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() { + 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() { + const root = getProjectRoot(); + process.env.REACTPRESS_ORIGINAL_CWD = root; + return root; +} + +function isMonorepoCheckout(cwd) { + const resolved = path.resolve(cwd || process.cwd()); + return ( + fs.existsSync(path.join(resolved, 'pnpm-workspace.yaml')) || + fs.existsSync(path.join(resolved, 'server', 'src', 'main.ts')) + ); +} + +module.exports = { + getMonorepoRoot, + getProjectRoot, + ensureOriginalCwd, + isMonorepoCheckout, + isProjectRoot, + findProjectRoot, + isPublishedCliRoot, +}; diff --git a/cli/src/lib/server-bundle.ts b/cli/src/lib/server-bundle.ts new file mode 100644 index 00000000..d1e8cbaf --- /dev/null +++ b/cli/src/lib/server-bundle.ts @@ -0,0 +1,94 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); +const { getCliPackageRoot, isUsingMonorepoServer } = require('./paths'); +const { t } = require('./i18n'); + +const SERVER_RUNTIME_MODULES = [ + '@nestjs/core', + '@nestjs/typeorm', + 'typeorm', + 'express', + '@fecommunity/reactpress-toolkit', +]; + +function getBundledServerDir() { + return path.join(getCliPackageRoot(), 'server'); +} + +function getBundledToolkitEntry() { + return path.join(getCliPackageRoot(), 'toolkit', 'dist', 'index.js'); +} + +function pathExists(target) { + try { + return fs.existsSync(target); + } catch { + return false; + } +} + +function isBundledServerReady() { + const serverDir = getBundledServerDir(); + if (!pathExists(path.join(serverDir, 'dist', 'main.js'))) return false; + if (!pathExists(getBundledToolkitEntry())) return false; + + for (const mod of SERVER_RUNTIME_MODULES) { + const modPath = mod.startsWith('@') + ? path.join(serverDir, 'node_modules', ...mod.split('/')) + : path.join(serverDir, 'node_modules', mod); + if (!pathExists(modPath)) return false; + } + return true; +} + +function getNpmCommand() { + return process.platform === 'win32' ? 'npm.cmd' : 'npm'; +} + +async function ensureBundledServerDeps(projectRoot) { + if (isUsingMonorepoServer(projectRoot)) { + return { ok: true }; + } + if (isBundledServerReady()) { + return { ok: true }; + } + + const serverDir = getBundledServerDir(); + if (!pathExists(path.join(serverDir, 'package.json'))) { + return { ok: false, message: t('bundle.serverBundle.missing') }; + } + if (!pathExists(path.join(serverDir, 'dist', 'main.js'))) { + return { ok: false, message: t('bundle.serverBundle.notBuilt') }; + } + if (!pathExists(getBundledToolkitEntry())) { + return { ok: false, message: t('bundle.serverBundle.toolkitMissing') }; + } + + console.log(t('bundle.serverBundle.installing')); + + const result = spawnSync( + getNpmCommand(), + ['install', '--omit=dev', '--legacy-peer-deps', '--no-bin-links'], + { + cwd: serverDir, + stdio: 'inherit', + shell: process.platform === 'win32', + }, + ); + + if (result.status !== 0) { + return { ok: false, message: t('bundle.serverBundle.installFailed') }; + } + if (!isBundledServerReady()) { + return { ok: false, message: t('bundle.serverBundle.notBuilt') }; + } + return { ok: true }; +} + +module.exports = { + ensureBundledServerDeps, + isBundledServerReady, + getBundledServerDir, +}; diff --git a/cli/src/lib/spawn.ts b/cli/src/lib/spawn.ts new file mode 100644 index 00000000..3d18fa8d --- /dev/null +++ b/cli/src/lib/spawn.ts @@ -0,0 +1,106 @@ +// @ts-nocheck +const { spawn, spawnSync } = require('child_process'); +const path = require('path'); +const chalk = require('chalk'); +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 || ensureOriginalCwd(), + stdio: options.stdio ?? 'inherit', + env: { + ...process.env, + REACTPRESS_LANG: process.env.REACTPRESS_LANG || resolveLocale(), + REACTPRESS_ORIGINAL_CWD: + options.cwd || process.env.REACTPRESS_ORIGINAL_CWD || process.cwd(), + ...options.env, + }, + shell: options.shell ?? false, + }); + if (result.status !== 0) { + const err = new Error( + t('spawn.commandFailed', { command, code: result.status ?? 1 }) + ); + err.exitCode = result.status ?? 1; + throw err; + } + return result; +} + +function runNodeScript(scriptPath, args = [], options = {}) { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, [scriptPath, ...args], { + stdio: 'inherit', + cwd: options.cwd || ensureOriginalCwd(), + env: { + ...process.env, + REACTPRESS_LANG: process.env.REACTPRESS_LANG || resolveLocale(), + REACTPRESS_ORIGINAL_CWD: + options.cwd || process.env.REACTPRESS_ORIGINAL_CWD || process.cwd(), + ...options.env, + }, + }); + + child.on('error', (error) => { + console.error(chalk.red('[ReactPress]'), error.message || error); + reject(error); + }); + + child.on('close', (code) => { + if (code !== 0) { + reject(Object.assign(new Error(t('spawn.exitCode', { code })), { exitCode: code })); + return; + } + resolve(code); + }); + }); +} + +function spawnDetached(scriptPath, args = [], options = {}) { + const child = spawn(process.execPath, [scriptPath, ...args], { + stdio: options.stdio ?? 'ignore', + detached: true, + cwd: options.cwd, + env: { + ...process.env, + REACTPRESS_LANG: process.env.REACTPRESS_LANG || resolveLocale(), + REACTPRESS_ORIGINAL_CWD: + options.cwd || process.env.REACTPRESS_ORIGINAL_CWD || process.cwd(), + ...options.env, + }, + }); + child.unref(); + return child; +} + +async function runReactpressCli(args, options = {}) { + const { initProject } = require('../core/services/init'); + const directory = args[1] ?? options.cwd ?? ensureOriginalCwd(); + const force = args.includes('--force'); + const local = args.includes('--local'); + const result = await initProject({ + directory: path.resolve(String(directory)), + force, + local, + }); + console.log(`[reactpress] ${result.message}`); + if (!result.ok) { + const err = new Error(result.message); + (err as NodeJS.ErrnoException & { exitCode?: number }).exitCode = 1; + throw err; + } +} + +function resolveCliScript(relativePath) { + return path.join(__dirname, '..', relativePath); +} + +module.exports = { + runSync, + runNodeScript, + spawnDetached, + runReactpressCli, + resolveCliScript, +}; diff --git a/cli/src/lib/status.ts b/cli/src/lib/status.ts new file mode 100644 index 00000000..a47f1cb8 --- /dev/null +++ b/cli/src/lib/status.ts @@ -0,0 +1,133 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const { + brand, + icon, + divider, + padRight, + statusPill, + sectionHeader, + terminalWidth, + gradientText, + palette, +} = require('../ui/theme'); +const { + loadServerSiteUrl, + loadClientSiteUrl, + getHealthUrl, + checkHealth, + isHttpResponding, +} = require('./http'); +const { isUsingMonorepoServer } = require('./paths'); +const { readPid, isProcessRunning } = require('./process'); +const { isDockerRunning } = require('./docker'); +const { ensureOriginalCwd } = require('./root'); +const { t } = require('./i18n'); + +function envFileStatus(projectRoot) { + const envPath = path.join(projectRoot, '.env'); + const configPath = path.join(projectRoot, '.reactpress', 'config.json'); + return { + env: fs.existsSync(envPath), + config: fs.existsSync(configPath), + envPath, + configPath, + }; +} + +function fieldRow(label, value) { + return ` ${brand.muted(padRight(label, 10))} ${value}`; +} + +async function printUnifiedStatus(projectRoot = ensureOriginalCwd()) { + const env = envFileStatus(projectRoot); + const apiUrl = loadServerSiteUrl(projectRoot); + const clientUrl = loadClientSiteUrl(projectRoot); + const pid = readPid(projectRoot); + const healthUrl = getHealthUrl(projectRoot); + const [apiHttp, clientHttp, health] = await Promise.all([ + isHttpResponding(apiUrl), + isHttpResponding(clientUrl), + checkHealth(healthUrl), + ]); + + 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(` ${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( + fieldRow( + t('status.field.config'), + env.config ? brand.success(t('status.configOk')) : brand.warn(t('status.configBad')) + ) + ); + console.log( + 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( + fieldRow( + t('status.field.health'), + health.ok + ? `${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( + fieldRow( + t('status.field.database'), + statusPill(dbOk, { on: t('status.dbUp'), off: t('status.dbDown') }) + ) + ); + } + const pidAlive = pid && isProcessRunning(pid); + console.log( + fieldRow( + t('status.field.pid'), + `${brand.dim(pid ?? '—')}${pidAlive ? ` ${brand.success(t('status.pidRunning'))}` : ''}` + ) + ); + + 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( + fieldRow( + t('status.field.engine'), + statusPill(isDockerRunning(), { + on: t('status.dockerUp'), + off: t('status.dockerDown'), + }) + ) + ); + + console.log(` ${divider(w)}`); + console.log(''); +} + +module.exports = { printUnifiedStatus, envFileStatus }; diff --git a/cli/src/lib/theme-api-proxy-patch.ts b/cli/src/lib/theme-api-proxy-patch.ts new file mode 100644 index 00000000..b82b3c5e --- /dev/null +++ b/cli/src/lib/theme-api-proxy-patch.ts @@ -0,0 +1,113 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); + +const PATCH_MARKER = '.reactpress-api-proxy-patched'; +const ENCODING_PATCH_MARKER = 'reactpress-api-proxy-encoding'; + +const ROUTE_REL = path.join('app', 'api', '[[...path]]', 'route.ts'); +const PROXY_REL = path.join('lib', 'mock-api', 'proxy.ts'); + +/** Buggy pattern: body read before mock/proxy branch (breaks upstream proxy). */ +const BUGGY_ROUTE_BLOCK = ` const keyword = new URL(request.url).searchParams.get('keyword')?.trim() ?? '' + const body = await readJsonBody(request) + + let response: Response + if (isMockApiEnabled()) { + const mockResponse = matchMockRoute(request.method, path, { keyword, body })`; + +const FIXED_ROUTE_BLOCK = ` const keyword = new URL(request.url).searchParams.get('keyword')?.trim() ?? '' + + let response: Response + if (isMockApiEnabled()) { + const body = await readJsonBody(request) + const mockResponse = matchMockRoute(request.method, path, { keyword, body })`; + +const BUGGY_PROXY_BLOCK = ` return new Response(upstream.body, { + status: upstream.status, + headers: upstream.headers, + })`; + +const FIXED_PROXY_BLOCK = ` const responseHeaders = new Headers(upstream.headers) + // ${ENCODING_PATCH_MARKER}: Node fetch decompresses bodies; strip stale encoding headers. + responseHeaders.delete('content-encoding') + responseHeaders.delete('content-length') + responseHeaders.delete('transfer-encoding') + + return new Response(upstream.body, { + status: upstream.status, + headers: responseHeaders, + })`; + +function resolveApiRoutePath(themeDir) { + if (!themeDir) return null; + const routePath = path.join(themeDir, ROUTE_REL); + return fs.existsSync(routePath) ? routePath : null; +} + +function resolveApiProxyPath(themeDir) { + if (!themeDir) return null; + const proxyPath = path.join(themeDir, PROXY_REL); + return fs.existsSync(proxyPath) ? proxyPath : null; +} + +function touchPatchMarker(themeDir) { + fs.writeFileSync(path.join(themeDir, PATCH_MARKER), `${new Date().toISOString()}\n`, 'utf8'); +} + +function patchThemeApiRouteBodyRead(themeDir) { + const routePath = resolveApiRoutePath(themeDir); + if (!routePath) return false; + + let src = fs.readFileSync(routePath, 'utf8'); + if (src.includes(FIXED_ROUTE_BLOCK) || !src.includes(BUGGY_ROUTE_BLOCK)) { + return false; + } + + src = src.replace(BUGGY_ROUTE_BLOCK, FIXED_ROUTE_BLOCK); + fs.writeFileSync(routePath, src, 'utf8'); + return true; +} + +/** + * Node fetch() auto-decompresses upstream bodies but leaves Content-Encoding on the Response. + * Browsers then fail with ERR_CONTENT_DECODING_FAILED (HTTP 200). + */ +function patchThemeApiProxyEncodingHeaders(themeDir) { + const proxyPath = resolveApiProxyPath(themeDir); + if (!proxyPath) return false; + + let src = fs.readFileSync(proxyPath, 'utf8'); + if (src.includes(ENCODING_PATCH_MARKER) || !src.includes(BUGGY_PROXY_BLOCK)) { + return false; + } + + src = src.replace(BUGGY_PROXY_BLOCK, FIXED_PROXY_BLOCK); + fs.writeFileSync(proxyPath, src, 'utf8'); + return true; +} + +/** + * App Router themes proxy /api via app/api/[[...path]]/route.ts. + * Patches known theme-starter issues before production build. + */ +function patchThemeApiProxyRoute(themeDir) { + const routePatched = patchThemeApiRouteBodyRead(themeDir); + const proxyPatched = patchThemeApiProxyEncodingHeaders(themeDir); + if (routePatched || proxyPatched) { + touchPatchMarker(themeDir); + } + return routePatched || proxyPatched; +} + +module.exports = { + PATCH_MARKER, + ENCODING_PATCH_MARKER, + ROUTE_REL, + PROXY_REL, + patchThemeApiProxyRoute, + patchThemeApiRouteBodyRead, + patchThemeApiProxyEncodingHeaders, + resolveApiRoutePath, + resolveApiProxyPath, +}; diff --git a/cli/src/lib/theme-catalog.ts b/cli/src/lib/theme-catalog.ts new file mode 100644 index 00000000..0e285f39 --- /dev/null +++ b/cli/src/lib/theme-catalog.ts @@ -0,0 +1,3 @@ +// @ts-nocheck +/** @deprecated Import from ./theme-registry — kept for backward compatibility. */ +module.exports = require('./theme-registry'); diff --git a/cli/src/lib/theme-cli.ts b/cli/src/lib/theme-cli.ts new file mode 100644 index 00000000..26355dfd --- /dev/null +++ b/cli/src/lib/theme-cli.ts @@ -0,0 +1,54 @@ +// @ts-nocheck +const chalk = require('chalk'); +const { installThemeFromNpm } = require('./theme-install'); +const { readThemeLock } = require('./theme-lock'); +const { readThemeCatalog, resolveCatalogInstallSpec } = require('./theme-catalog'); +const { listAvailableThemeIds } = require('./theme-runtime'); +const { t } = require('./i18n'); + +async function runThemeAdd(projectRoot, spec, options = {}) { + const trimmed = String(spec || '').trim(); + if (!trimmed) { + throw new Error(t('themeInstall.specRequired')); + } + + const resolvedSpec = resolveCatalogInstallSpec(projectRoot, trimmed) || trimmed; + console.log(chalk.cyan('[reactpress]'), t('themeInstall.installing', { spec: resolvedSpec })); + const result = await installThemeFromNpm(projectRoot, resolvedSpec, { + skipDependencies: options.skipDependencies === true, + }); + + console.log( + chalk.green('[reactpress]'), + t('themeInstall.success', { + id: result.themeId, + name: result.name, + dir: result.themeDirRel, + }), + ); + console.log(chalk.gray(t('themeInstall.nextActivate', { id: result.themeId }))); + return result; +} + +function runThemeList(projectRoot) { + const ids = listAvailableThemeIds(projectRoot); + const lock = readThemeLock(projectRoot); + if (!ids.length) { + console.log(t('themeInstall.listEmpty')); + return; + } + console.log(t('themeInstall.listHeading')); + for (const id of ids.sort()) { + const npm = lock.themes[id]; + if (npm?.source === 'npm') { + console.log(` - ${id} (${npm.spec})`); + } else { + console.log(` - ${id}`); + } + } +} + +module.exports = { + runThemeAdd, + runThemeList, +}; diff --git a/cli/src/lib/theme-dev.ts b/cli/src/lib/theme-dev.ts new file mode 100644 index 00000000..c9599099 --- /dev/null +++ b/cli/src/lib/theme-dev.ts @@ -0,0 +1,844 @@ +// @ts-nocheck +const { spawnSync } = require('child_process'); +const { spawnDevChild } = require('./dev-child-io'); +const path = require('path'); +const fs = require('fs'); +const { loadClientSiteUrl, loadServerSiteUrl, getApiPrefix, waitForHttpOk } = require('./http'); +const { warmupThemeHomepage } = require('./theme-warmup'); +const { nginxEntryUrl } = require('./nginx'); +const { + readActiveThemeManifest, + readPreviewThemeManifest, + resolveThemeDirectory, + readManifestSignature, + readPreviewManifestSignature, + getPreviewThemePort, + isThemePackageDir, + isAllowedThemePort, + themeWorkspaceRoot, + listAvailableThemeIds, +} = require('./theme-runtime'); +const { isDevVerbose, logDevDetail, logDevLine } = require('./dev-log'); +const { resolveProjectRoot } = require('./paths'); +const { t } = require('./i18n'); +const { + ensurePreviewThemeRunning, + stopAllPreviewPool, + stopPreviewPoolTheme, + isPreviewHomepageReady, + getPreviewSiteUrlForPort, + getPreviewProxyPort, + ensurePreviewProxyRunning, + resolvePreviewThemeLaunchPlan, + spawnThemeProcess, + withPreviewPortLock, + setPreviewProxyTarget, + isIntegratedDesktopDev, +} = require('./theme-preview-pool'); +const { enqueueThemeBuild, ensureThemeDependenciesInstalled } = require('./theme-prod'); + +let themeChild = null; +let themeWatchStop = null; +let runningSignature = null; +let trackedThemePid = null; +let restartChain = Promise.resolve(); + +let previewRunningSignature = null; +let previewRestartChain = Promise.resolve(); + +/** Drop `.next` only when it does not match the theme package React major (avoids wiping React 17 caches). */ +function cleanStaleThemeDevCache(themeDir) { + if (process.env.REACTPRESS_KEEP_THEME_CACHE === '1') return; + + const nextDir = path.join(themeDir, '.next'); + if (!fs.existsSync(nextDir)) return; + + if (process.env.REACTPRESS_CLEAR_THEME_CACHE === '1') { + fs.rmSync(nextDir, { recursive: true, force: true }); + logDevDetail('themeDev.cacheCleared'); + return; + } + + let expectedMajor = '17'; + try { + const pkg = JSON.parse(fs.readFileSync(path.join(themeDir, 'package.json'), 'utf8')); + const reactDep = pkg.dependencies?.react || pkg.devDependencies?.react || ''; + const match = String(reactDep).match(/(\d+)/); + if (match) expectedMajor = match[1]; + } catch { + return; + } + + try { + const marker = `react@${expectedMajor}`; + const result = spawnSync('grep', ['-rl', marker, nextDir], { + encoding: 'utf8', + maxBuffer: 1024 * 1024, + }); + if (result.stdout?.trim()) return; + + fs.rmSync(nextDir, { recursive: true, force: true }); + logDevDetail('themeDev.cacheStaleCleared', { marker }); + } catch { + // ignore grep / rm failures + } +} + +function getClientPort(projectRoot) { + try { + const url = new URL(loadClientSiteUrl(projectRoot)); + const port = parseInt(url.port || '3001', 10); + if (isAllowedThemePort(port)) return String(port); + } catch { + // fall through + } + return '3001'; +} + +function assertThemePort(port) { + const n = parseInt(port, 10); + if (!isAllowedThemePort(n)) { + throw new Error(`Refusing theme dev on protected port ${port}`); + } + return n; +} + +function isPortListening(port) { + const result = spawnSync('lsof', [`-tiTCP:${port}`, '-sTCP:LISTEN'], { encoding: 'utf8' }); + return result.status === 0 && Boolean(result.stdout?.trim()); +} + +function getProcessCwd(pid) { + const n = parseInt(pid, 10); + if (!Number.isFinite(n) || n <= 0) return null; + const res = spawnSync('lsof', ['-p', String(n)], { encoding: 'utf8' }); + if (res.status !== 0) return null; + const line = res.stdout.split('\n').find((row) => row.includes(' cwd ')); + if (!line) return null; + const parts = line.trim().split(/\s+/); + return parts[parts.length - 1] || null; +} + +function isUnderThemesDir(projectRoot, cwd) { + if (!cwd) return false; + const themesRoot = path.join(path.resolve(projectRoot), 'themes'); + const resolved = path.resolve(cwd); + return resolved === themesRoot || resolved.startsWith(`${themesRoot}${path.sep}`); +} + +/** Child PIDs of `rootPid` (pnpm → next dev tree). */ +function collectDescendantPids(rootPid) { + const root = parseInt(rootPid, 10); + if (!Number.isFinite(root) || root <= 0) return []; + + const out = []; + const queue = [String(root)]; + const seen = new Set(); + + while (queue.length) { + const pid = queue.shift(); + if (!pid || seen.has(pid)) continue; + seen.add(pid); + + const children = spawnSync('pgrep', ['-P', pid], { encoding: 'utf8' }); + if (children.status !== 0 || !children.stdout?.trim()) continue; + + for (const child of children.stdout.trim().split(/\s+/)) { + if (!child || seen.has(child)) continue; + out.push(child); + queue.push(child); + } + } + return out; +} + +function isPidSafeToSignal(pid) { + const n = parseInt(pid, 10); + if (!Number.isFinite(n) || n <= 1) return false; + if (n === process.pid) return false; + if (process.ppid && n === process.ppid) return false; + return true; +} + +/** LISTEN pids for this theme dev port (package cwd, themes/, or tracked child tree). */ +function getThemeListenerPids(projectRoot, port) { + const result = spawnSync('lsof', [`-tiTCP:${port}`, '-sTCP:LISTEN'], { encoding: 'utf8' }); + if (result.status !== 0 || !result.stdout?.trim()) return []; + + const allowed = new Set(); + + if (trackedThemePid && isPidSafeToSignal(trackedThemePid)) { + allowed.add(String(trackedThemePid)); + for (const child of collectDescendantPids(trackedThemePid)) { + if (isPidSafeToSignal(child)) allowed.add(child); + } + } + + for (const pid of result.stdout.trim().split(/\s+/)) { + if (!isPidSafeToSignal(pid)) continue; + const cwd = getProcessCwd(pid); + if ( + cwd && + (isThemePackageDir(projectRoot, cwd) || isUnderThemesDir(projectRoot, cwd)) + ) { + allowed.add(pid); + for (const child of collectDescendantPids(pid)) { + if (isPidSafeToSignal(child)) allowed.add(child); + } + } + } + + return [...allowed]; +} + +function signalPids(pids, signal) { + const flag = signal === 'KILL' ? '-9' : '-TERM'; + for (const pid of pids) { + if (isPidSafeToSignal(pid)) { + spawnSync('kill', [flag, pid], { stdio: 'ignore' }); + } + } +} + +function killThemeListenersOnPort(projectRoot, port, signal = 'TERM') { + signalPids(getThemeListenerPids(projectRoot, port), signal); +} + +function waitForPortFree(port, timeoutMs = 10_000) { + const start = Date.now(); + return new Promise((resolve) => { + const tick = () => { + if (!isPortListening(port)) { + resolve(true); + return; + } + if (Date.now() - start >= timeoutMs) { + resolve(false); + return; + } + setTimeout(tick, 250); + }; + tick(); + }); +} + +async function releaseThemePort(projectRoot, port, { fast = false } = {}) { + stopActiveThemeProcess(); + + const maxAttempts = fast ? 3 : 4; + const waitSchedule = fast ? [1200, 800, 800] : [12_000, 6000, 6000, 6000]; + + for (let attempt = 0; attempt < maxAttempts; attempt += 1) { + const signal = fast ? (attempt === 0 ? 'TERM' : 'KILL') : attempt < 2 ? 'TERM' : 'KILL'; + killThemeListenersOnPort(projectRoot, port, signal); + if (trackedThemePid && isPidSafeToSignal(trackedThemePid)) { + const tree = [String(trackedThemePid), ...collectDescendantPids(trackedThemePid)]; + signalPids(tree, signal); + } + const waitMs = waitSchedule[attempt] ?? 6000; + const freed = await waitForPortFree(port, waitMs); + if (freed) { + trackedThemePid = null; + return true; + } + } + + trackedThemePid = null; + return false; +} + +/** Optional theme-only API override (admin / Nest API stay on SERVER_SITE_URL). */ +function readThemeApiOverride(projectRoot, envKey) { + const fromShell = process.env[envKey]?.trim(); + if (fromShell) return fromShell.replace(/\/$/, ''); + + const envPath = path.join(projectRoot, '.env'); + try { + const content = fs.readFileSync(envPath, 'utf8'); + const match = content.match(new RegExp(`^${envKey}=(.+)$`, 'm')); + if (match) { + const raw = match[1].trim().replace(/^['"]|['"]$/g, ''); + if (raw) return raw.replace(/\/$/, ''); + } + } catch { + // ignore + } + return null; +} + +function useLocalThemeApiInDev() { + return process.env.REACTPRESS_DEV_FORCE_LOCAL_THEME_API === '1'; +} + +function buildLocalThemeApiUrl(projectRoot, { forBrowser = false } = {}) { + const desktopApi = process.env.REACTPRESS_DESKTOP_LOCAL_API?.trim().replace(/\/$/, ''); + if (desktopApi) { + return desktopApi; + } + if (forBrowser && process.env.REACTPRESS_BEHIND_NGINX === '1') { + return `${nginxEntryUrl(projectRoot).replace(/\/$/, '')}/api`; + } + const server = loadServerSiteUrl(projectRoot).replace(/\/$/, ''); + const prefix = getApiPrefix(projectRoot).replace(/\/$/, '') || '/api'; + return `${server}${prefix.startsWith('/') ? prefix : `/${prefix}`}`; +} + +/** Direct Nest API — used for Next.js SSR (runs before nginx is up). */ +function buildThemeServerApiUrl(projectRoot) { + if (useLocalThemeApiInDev()) { + return buildLocalThemeApiUrl(projectRoot, { forBrowser: false }); + } + + const override = readThemeApiOverride(projectRoot, 'REACTPRESS_THEME_API_URL'); + if (override) return override; + + return buildLocalThemeApiUrl(projectRoot); +} + +/** Browser-facing API — nginx unified entry when behind proxy. */ +function buildThemePublicApiUrl(projectRoot) { + if (useLocalThemeApiInDev()) { + return buildLocalThemeApiUrl(projectRoot, { forBrowser: true }); + } + + const publicOverride = readThemeApiOverride(projectRoot, 'REACTPRESS_THEME_PUBLIC_API_URL'); + if (publicOverride) return publicOverride; + + const themeOverride = readThemeApiOverride(projectRoot, 'REACTPRESS_THEME_API_URL'); + if (themeOverride) return themeOverride; + + return buildLocalThemeApiUrl(projectRoot); +} + +/** @deprecated use buildThemeServerApiUrl / buildThemePublicApiUrl */ +function buildThemeApiUrl(projectRoot) { + return buildThemeServerApiUrl(projectRoot); +} + +function buildThemeChildEnv(projectRoot, { port, serverApiUrl, publicApiUrl, themeId }) { + const keys = [ + 'PATH', + 'HOME', + 'USER', + 'LANG', + 'LC_ALL', + 'NODE_ENV', + 'PNPM_HOME', + 'npm_config_user_agent', + ]; + const env = {}; + for (const key of keys) { + if (process.env[key] !== undefined) env[key] = process.env[key]; + } + return { + ...env, + PORT: String(port), + // Next inlines SERVER_API_URL from next.config (localhost); override for remote dev SSR. + SERVER_API_URL: serverApiUrl, + REACTPRESS_API_URL: serverApiUrl, + NEXT_PUBLIC_REACTPRESS_API_URL: publicApiUrl, + REACTPRESS_THEME_ID: themeId || '', + REACTPRESS_ORIGINAL_CWD: projectRoot, + REACTPRESS_SKIP_BROWSER_OPEN: '1', + NEXT_IGNORE_INCORRECT_LOCKFILE: '1', + NEXT_TELEMETRY_DISABLED: '1', + ...(process.env.REACTPRESS_NGINX_ENTRY_URL + ? { + REACTPRESS_NGINX_ENTRY_URL: process.env.REACTPRESS_NGINX_ENTRY_URL, + NGINX_ENTRY_URL: process.env.REACTPRESS_NGINX_ENTRY_URL, + NEXT_PUBLIC_REACTPRESS_ADMIN_URL: `${String(process.env.REACTPRESS_NGINX_ENTRY_URL).replace(/\/$/, '')}/admin`, + } + : { REACTPRESS_SKIP_DEV_PORT_REDIRECT: '1' }), + ...(process.env.REACTPRESS_DESKTOP_LOCAL === '1' || process.env.REACTPRESS_DESKTOP_SITE_ROOT + ? { REACTPRESS_HONOR_PREVIEW: '1' } + : {}), + }; +} + +function stopThemeProcess(childRef, trackedPidRef) { + const child = childRef.current; + if (!child || child.killed) { + childRef.current = null; + return; + } + + const pid = child.pid; + if (pid && isPidSafeToSignal(pid)) { + trackedPidRef.current = pid; + } + try { + if (process.platform !== 'win32' && pid && isPidSafeToSignal(pid)) { + try { + process.kill(-pid, 'SIGTERM'); + } catch { + spawnSync('pkill', ['-TERM', '-P', String(pid)], { stdio: 'ignore' }); + child.kill('SIGTERM'); + } + for (const descendant of collectDescendantPids(pid)) { + if (isPidSafeToSignal(descendant)) { + spawnSync('kill', ['-TERM', descendant], { stdio: 'ignore' }); + } + } + } else if (pid && isPidSafeToSignal(pid)) { + child.kill('SIGTERM'); + } + } catch { + // ignore — process may already be gone + } + childRef.current = null; +} + +const activeChildRef = { get current() { return themeChild; }, set current(v) { themeChild = v; } }; +const activeTrackedPidRef = { + get current() { return trackedThemePid; }, + set current(v) { trackedThemePid = v; }, +}; + +function stopActiveThemeProcess() { + stopThemeProcess(activeChildRef, activeTrackedPidRef); +} + +function stopPreviewThemeProcess() { + /* Preview pool stays warm — torn down only on full dev shutdown. */ +} + +function stopThemeSite() { + if (themeWatchStop) { + themeWatchStop(); + themeWatchStop = null; + } + stopActiveThemeProcess(); + void stopAllPreviewPool(resolveProjectRoot()); + runningSignature = null; + previewRunningSignature = null; + restartChain = Promise.resolve(); + previewRestartChain = Promise.resolve(); +} + +async function spawnThemeSite(projectRoot, { onClose } = {}) { + const signature = readManifestSignature(projectRoot); + if (!signature) { + console.warn(`[reactpress] ${t('themeDev.invalidManifest')}`); + runningSignature = null; + return null; + } + + const { activeTheme } = readActiveThemeManifest(projectRoot); + const themeDir = resolveThemeDirectory(projectRoot, activeTheme); + const port = assertThemePort(getClientPort(projectRoot)); + const serverApiUrl = buildThemeServerApiUrl(projectRoot); + const publicApiUrl = buildThemePublicApiUrl(projectRoot); + const siteUrl = loadClientSiteUrl(projectRoot); + + if (!themeDir || !isThemePackageDir(projectRoot, themeDir)) { + console.warn( + `[reactpress] ${t('themeDev.notFound', { id: activeTheme })} — ${siteUrl} ${t('themeDev.unavailable')}`, + ); + runningSignature = null; + return null; + } + + const relDir = path.relative(projectRoot, themeDir) || themeDir; + const launch = resolvePreviewThemeLaunchPlan(themeDir, port); + const useProduction = launch.mode === 'production'; + + logDevDetail('themeDev.startingShort', { + id: activeTheme, + port, + dir: relDir, + mode: useProduction ? 'production' : 'dev', + }); + if (isDevVerbose()) { + logDevLine('themeDev.apiSplit', { ssr: serverApiUrl, browser: publicApiUrl }); + } + + if (useProduction) { + try { + ensureThemeDependenciesInstalled(projectRoot, themeDir, activeTheme, 'themeProd'); + } catch (err) { + console.warn( + `[reactpress] ${t('themePreview.buildFailed', { + id: activeTheme, + message: err.message || err, + })}`, + ); + runningSignature = null; + return null; + } + try { + await enqueueThemeBuild(projectRoot, activeTheme, { logPrefix: 'themeProd' }); + const { ensureBuildAllowsPreviewFrame } = require('./theme-preview-frame'); + ensureBuildAllowsPreviewFrame(themeDir, '.next'); + } catch (err) { + console.warn( + `[reactpress] ${t('themePreview.buildFailed', { + id: activeTheme, + message: err.message || err, + })}`, + ); + runningSignature = null; + return null; + } + } else { + cleanStaleThemeDevCache(themeDir); + } + + try { + const { ensurePreviewFrameAllowed } = require('./theme-preview-frame'); + ensurePreviewFrameAllowed(themeDir); + } catch { + // ignore — preview patch optional for themes without next.config headers + } + + if (useProduction) { + themeChild = spawnThemeProcess(projectRoot, { + themeDir, + themeId: activeTheme, + port, + serverApiUrl, + publicApiUrl, + launch, + role: 'visitor', + }); + } else { + themeChild = spawnDevChild('pnpm', ['run', 'dev'], { + cwd: themeDir, + detached: process.platform !== 'win32', + shell: process.platform === 'win32', + env: buildThemeChildEnv(projectRoot, { port, serverApiUrl, publicApiUrl, themeId: activeTheme }), + }); + } + + const child = themeChild; + trackedThemePid = child.pid ?? null; + runningSignature = signature; + + child.on('close', (code) => { + if (themeChild === child) { + themeChild = null; + trackedThemePid = null; + if (runningSignature === signature) { + runningSignature = null; + } + } + if (onClose) onClose(code); + }); + + if (process.env.REACTPRESS_DEV_FORCE_LOCAL_THEME_API !== '1') { + const homepageUrl = `${siteUrl.replace(/\/$/, '')}/`; + const pollMs = parseInt(process.env.REACTPRESS_THEME_READY_POLL_MS || '150', 10) || 150; + waitForHttpOk(homepageUrl, 120_000, pollMs).then((ready) => { + if (ready && runningSignature === signature) { + console.log(t('themeDev.ready', { url: siteUrl, id: activeTheme })); + warmupThemeHomepage(projectRoot, siteUrl).catch(() => {}); + } else if (!ready && runningSignature === signature) { + console.warn(t('themeDev.slow', { url: siteUrl })); + } + }); + } + + return themeChild; +} + +async function restartThemeSite(projectRoot, { onClose } = {}) { + const signature = readManifestSignature(projectRoot); + if (!signature) return; + + if (signature === runningSignature && themeChild && !themeChild.killed) { + return; + } + + const port = assertThemePort(getClientPort(projectRoot)); + let freed = await releaseThemePort(projectRoot, port, { fast: true }); + if (!freed || isPortListening(port)) { + freed = await releaseThemePort(projectRoot, port, { fast: true }); + } + if (!freed || isPortListening(port)) { + console.warn(`[reactpress] ${t('themeDev.portBusy', { port })}`); + console.warn( + `[reactpress] ${t('themeDev.portBusyHint', { + port, + cmd: `lsof -tiTCP:${port} -sTCP:LISTEN | xargs kill -9`, + })}`, + ); + return; + } + + await spawnThemeSite(projectRoot, { onClose }); +} + +async function restartPreviewThemeSite(projectRoot, { onClose } = {}) { + await withPreviewPortLock(async () => { + const signature = readPreviewManifestSignature(projectRoot); + + if (!signature) { + previewRunningSignature = null; + await stopAllPreviewPool(projectRoot); + if (onClose) onClose(0); + return; + } + + const previewManifest = readPreviewThemeManifest(projectRoot); + const themeId = previewManifest?.activeTheme; + if (!themeId) return; + + const { activeTheme } = readActiveThemeManifest(projectRoot); + if (themeId === activeTheme) { + previewRunningSignature = null; + await stopAllPreviewPool(projectRoot); + if (onClose) onClose(0); + return; + } + + await ensurePreviewProxyRunning(getPreviewProxyPort()); + + if (signature === previewRunningSignature) { + const { previewPool } = require('./theme-preview-pool'); + const entry = previewPool.get(themeId); + const proxyPort = getPreviewProxyPort(); + const childAlive = + entry?.child && + !entry.child.killed && + entry.child.exitCode == null && + entry.child.signalCode == null; + if (childAlive && entry?.backendPort) { + setPreviewProxyTarget(entry.backendPort); + entry.lastUsed = Date.now(); + const ready = await isPreviewHomepageReady(projectRoot, proxyPort); + if (ready) return; + } + if (entry) { + stopPreviewPoolTheme(themeId); + } + previewRunningSignature = null; + } + + const serverApiUrl = buildThemeServerApiUrl(projectRoot); + const publicApiUrl = buildThemePublicApiUrl(projectRoot); + + const result = await ensurePreviewThemeRunning(projectRoot, themeId, { + serverApiUrl, + publicApiUrl, + }); + + if (!result) { + previewRunningSignature = null; + console.warn(`[reactpress] Preview failed to start for theme "${themeId}"`); + if (onClose) onClose(1); + return; + } + + previewRunningSignature = signature; + if (result.reused) { + console.log(`[reactpress] Preview ready (reused): ${result.url} (theme: ${themeId})`); + } else { + console.log(`[reactpress] Preview ready: ${result.url} (theme: ${themeId})`); + } + if (onClose) onClose(0); + }); +} + +async function prewarmPreviewThemeBackends(projectRoot) { + if (process.env.REACTPRESS_SKIP_PREVIEW_BUILD === '1') return; + if (!isIntegratedDesktopDev()) return; + + const { activeTheme } = readActiveThemeManifest(projectRoot); + const themeIds = listAvailableThemeIds(projectRoot).filter((id) => id !== activeTheme); + if (themeIds.length === 0) return; + + console.log( + `[reactpress] ${t('dev.previewPrewarmStarting')} (${themeIds.length} backend(s) on :${getPreviewProxyPort()}+)`, + ); + + await ensurePreviewProxyRunning(getPreviewProxyPort()); + const serverApiUrl = buildThemeServerApiUrl(projectRoot); + const publicApiUrl = buildThemePublicApiUrl(projectRoot); + + for (const themeId of themeIds) { + try { + const result = await ensurePreviewThemeRunning(projectRoot, themeId, { + serverApiUrl, + publicApiUrl, + }); + if (result?.reused) { + console.log(`[reactpress] Preview backend reused for "${themeId}" (:${result.backendPort})`); + } else if (result) { + console.log(`[reactpress] Preview backend ready for "${themeId}" (:${result.backendPort})`); + } + } catch (err) { + console.warn( + `[reactpress] Preview backend prewarm failed for "${themeId}": ${err.message || err}`, + ); + } + } +} + +function watchActiveThemeManifest(projectRoot, onChange) { + const manifestPath = path.join(themeWorkspaceRoot(projectRoot), '.reactpress', 'active-theme.json'); + const dir = path.dirname(manifestPath); + fs.mkdirSync(dir, { recursive: true }); + + let lastSignature = readManifestSignature(projectRoot); + let debounce = null; + + const scheduleCheck = () => { + clearTimeout(debounce); + debounce = setTimeout(() => { + const next = readManifestSignature(projectRoot); + if (!next || next === lastSignature) return; + lastSignature = next; + console.log(`\n[reactpress] ${t('themeDev.restart')}`); + restartChain = restartChain + .then(() => onChange()) + .catch((err) => { + console.warn(`[reactpress] ${t('themeDev.restartFailed', { message: err.message || err })}`); + }); + }, 200); + }; + + const watcher = fs.watch(dir, scheduleCheck); + const poller = setInterval(scheduleCheck, 1000); + + return () => { + clearTimeout(debounce); + clearInterval(poller); + watcher.close(); + }; +} + +function watchPreviewThemeManifest(projectRoot, onChange) { + const manifestPath = path.join(themeWorkspaceRoot(projectRoot), '.reactpress', 'preview-theme.json'); + const dir = path.dirname(manifestPath); + fs.mkdirSync(dir, { recursive: true }); + + let lastSignature = readPreviewManifestSignature(projectRoot); + let debounce = null; + /** Delay teardown when manifest is deleted — admin preview session may remount immediately. */ + let clearDebounce = null; + const PREVIEW_CLEAR_GRACE_MS = 500; + + const enqueueRestart = (nextSignature) => { + if (nextSignature === lastSignature) return; + lastSignature = nextSignature; + if (nextSignature) { + console.log('\n[reactpress] preview-theme.json changed — restarting preview theme…'); + } + previewRestartChain = previewRestartChain + .then(() => onChange()) + .catch((err) => { + console.warn( + `[reactpress] ${t('themeDev.restartFailed', { message: err.message || err })}`, + ); + }); + }; + + const scheduleCheck = () => { + clearTimeout(debounce); + debounce = setTimeout(() => { + const next = readPreviewManifestSignature(projectRoot); + if (next === lastSignature) return; + + if (!next && lastSignature) { + if (clearDebounce) clearTimeout(clearDebounce); + clearDebounce = setTimeout(() => { + clearDebounce = null; + const still = readPreviewManifestSignature(projectRoot); + if (still) { + if (still !== lastSignature) enqueueRestart(still); + return; + } + enqueueRestart(''); + }, PREVIEW_CLEAR_GRACE_MS); + return; + } + + if (clearDebounce) { + clearTimeout(clearDebounce); + clearDebounce = null; + } + enqueueRestart(next); + }, 200); + }; + + const watcher = fs.watch(dir, (event, filename) => { + if (filename && filename !== 'preview-theme.json') return; + scheduleCheck(); + }); + const poller = setInterval(scheduleCheck, 1000); + + return () => { + clearTimeout(debounce); + if (clearDebounce) clearTimeout(clearDebounce); + clearInterval(poller); + watcher.close(); + }; +} + +/** Drop stale preview manifest so `pnpm dev` does not auto-start :3003 from a prior admin session. */ +function clearPreviewThemeManifestFile(projectRoot) { + const manifestPath = path.join(themeWorkspaceRoot(projectRoot), '.reactpress', 'preview-theme.json'); + if (fs.existsSync(manifestPath)) { + fs.unlinkSync(manifestPath); + } +} + +async function releaseThemePortIfBusy(projectRoot, port, options) { + if (!isPortListening(port)) return true; + return releaseThemePort(projectRoot, port, options); +} + +async function prepareThemeDevBoot(projectRoot) { + clearPreviewThemeManifestFile(projectRoot); + stopActiveThemeProcess(); + previewRunningSignature = null; + runningSignature = null; + trackedThemePid = null; + + await ensurePreviewProxyRunning(getPreviewProxyPort()); + + const visitorPort = assertThemePort(getClientPort(projectRoot)); + await releaseThemePortIfBusy(projectRoot, visitorPort); +} + +async function startThemeSiteWithWatch(projectRoot, { onClose } = {}) { + await prepareThemeDevBoot(projectRoot); + + const restartActive = () => restartThemeSite(projectRoot, { onClose }); + const restartPreview = () => restartPreviewThemeSite(projectRoot, { onClose }); + + restartChain = restartChain.then(() => restartThemeSite(projectRoot, { onClose })); + await restartChain; + + const stopActiveWatch = watchActiveThemeManifest(projectRoot, restartActive); + const stopPreviewWatch = watchPreviewThemeManifest(projectRoot, restartPreview); + + const initialPreviewSignature = readPreviewManifestSignature(projectRoot); + if (initialPreviewSignature) { + previewRestartChain = previewRestartChain.then(() => restartPreviewThemeSite(projectRoot, { onClose })); + } + + themeWatchStop = () => { + stopActiveWatch(); + stopPreviewWatch(); + }; + + if (isIntegratedDesktopDev()) { + void prewarmPreviewThemeBackends(projectRoot); + } + + return Boolean(runningSignature && themeChild && !themeChild.killed); +} + +module.exports = { + spawnThemeSite, + restartThemeSite, + startThemeSiteWithWatch, + stopThemeSite, + getClientPort, + buildThemeApiUrl, + buildThemeServerApiUrl, + buildThemePublicApiUrl, + readManifestSignature, + isPortListening, + getThemeListenerPids, +}; diff --git a/cli/src/lib/theme-env.ts b/cli/src/lib/theme-env.ts new file mode 100644 index 00000000..520ceddc --- /dev/null +++ b/cli/src/lib/theme-env.ts @@ -0,0 +1,112 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); + +const { loadClientSiteUrl, loadServerSiteUrl, getApiPrefix } = require('./http'); + +function parseEnvFile(content) { + const out = {}; + for (const line of String(content).split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const idx = trimmed.indexOf('='); + if (idx <= 0) continue; + const key = trimmed.slice(0, idx).trim(); + let value = trimmed.slice(idx + 1).trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + out[key] = value; + } + return out; +} + +function readProjectEnv(projectRoot) { + const envPath = path.join(path.resolve(projectRoot), '.env'); + if (!fs.existsSync(envPath)) return {}; + try { + return parseEnvFile(fs.readFileSync(envPath, 'utf8')); + } catch { + return {}; + } +} + +function buildThemeEnvOverrides(projectRoot, projectEnv = readProjectEnv(projectRoot)) { + const clientSiteUrl = ( + projectEnv.CLIENT_SITE_URL || loadClientSiteUrl(projectRoot) + ).replace(/\/$/, ''); + const serverSiteUrl = ( + projectEnv.SERVER_SITE_URL || loadServerSiteUrl(projectRoot) + ).replace(/\/$/, ''); + const apiPrefix = projectEnv.SERVER_API_PREFIX || getApiPrefix(projectRoot) || '/api'; + const normalizedPrefix = apiPrefix.startsWith('/') ? apiPrefix : `/${apiPrefix}`; + const apiUrl = + projectEnv.REACTPRESS_API_URL || `${serverSiteUrl}${normalizedPrefix}`.replace(/\/$/, ''); + + const publicApiUrl = + projectEnv.NEXT_PUBLIC_REACTPRESS_API_URL || + projectEnv.REACTPRESS_THEME_PUBLIC_API_URL || + apiUrl; + + const adminUrl = + projectEnv.NEXT_PUBLIC_REACTPRESS_ADMIN_URL || + projectEnv.WEB_ADMIN_URL || + 'http://localhost:3000'; + + return { + REACTPRESS_API_URL: apiUrl, + SERVER_API_URL: apiUrl, + NEXT_PUBLIC_REACTPRESS_API_URL: publicApiUrl, + CLIENT_SITE_URL: clientSiteUrl, + NEXT_PUBLIC_REACTPRESS_ADMIN_URL: adminUrl.replace(/\/$/, ''), + }; +} + +function upsertEnvLines(existingContent, overrides) { + const lines = existingContent.split('\n'); + for (const [key, value] of Object.entries(overrides)) { + if (value == null || value === '') continue; + const entry = `${key}=${value}`; + const index = lines.findIndex((line) => { + const trimmed = line.trim(); + return trimmed && !trimmed.startsWith('#') && trimmed.startsWith(`${key}=`); + }); + if (index >= 0) { + lines[index] = entry; + } else { + lines.push(entry); + } + } + return `${lines.join('\n').trimEnd()}\n`; +} + +/** + * Point an installed theme's `.env` at the host ReactPress project API URLs. + */ +function syncThemeEnvFromProject(projectRoot, themeDir) { + const root = path.resolve(projectRoot); + const dir = path.resolve(themeDir); + const overrides = buildThemeEnvOverrides(root); + const envPath = path.join(dir, '.env'); + const examplePath = path.join(dir, '.env.example'); + + let base = ''; + if (fs.existsSync(envPath)) { + base = fs.readFileSync(envPath, 'utf8'); + } else if (fs.existsSync(examplePath)) { + base = fs.readFileSync(examplePath, 'utf8'); + } + + const next = upsertEnvLines(base, overrides); + fs.writeFileSync(envPath, next, 'utf8'); + return envPath; +} + +module.exports = { + buildThemeEnvOverrides, + readProjectEnv, + syncThemeEnvFromProject, +}; diff --git a/cli/src/lib/theme-install.ts b/cli/src/lib/theme-install.ts new file mode 100644 index 00000000..d18658da --- /dev/null +++ b/cli/src/lib/theme-install.ts @@ -0,0 +1,381 @@ +// @ts-nocheck +const crypto = require('crypto'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const { upsertNpmThemeLock } = require('./theme-lock'); +const { buildThemeEnvOverrides, syncThemeEnvFromProject } = require('./theme-env'); +const { ensurePreviewFrameAllowed } = require('./theme-preview-frame'); +const { patchThemeApiProxyRoute } = require('./theme-api-proxy-patch'); +const { resolveCatalogInstallSpec } = require('./theme-registry'); + +const THEME_ID_RE = /^[a-z0-9][a-z0-9-]*$/i; +const THEME_RUNTIME_REL = path.join('.reactpress', 'runtime'); +const COPY_SKIP_NAMES = new Set([ + 'node_modules', + '.next', + '.git', + 'dist', + '.turbo', + 'coverage', + '.reactpress', + '.cache', + 'package-lock.json', +]); + +function isValidThemeId(id) { + return typeof id === 'string' && THEME_ID_RE.test(id) && id.length <= 64; +} + +function parseNpmSpec(spec) { + const trimmed = String(spec || '').trim(); + if (!trimmed) { + return { error: 'EMPTY_SPEC' }; + } + if (trimmed.endsWith('.tgz') || trimmed.endsWith('.tar.gz')) { + const resolved = path.resolve(trimmed); + if (!fs.existsSync(resolved)) { + return { error: 'TARBALL_NOT_FOUND', path: resolved }; + } + return { kind: 'tarball', path: resolved }; + } + if (/^file:/i.test(trimmed)) { + const filePath = trimmed.replace(/^file:/i, ''); + const resolved = path.resolve(filePath); + if (!fs.existsSync(resolved)) { + return { error: 'TARBALL_NOT_FOUND', path: resolved }; + } + return { kind: 'tarball', path: resolved }; + } + return { kind: 'npm', spec: trimmed }; +} + +function readThemeManifestFromDir(dir) { + const manifestPath = path.join(dir, 'theme.json'); + if (!fs.existsSync(manifestPath)) return null; + try { + const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + if (raw && typeof raw.id === 'string' && typeof raw.name === 'string') { + return raw; + } + } catch { + // ignore + } + return null; +} + +function inferThemeIdFromPackageName(name) { + if (typeof name !== 'string' || !name) return null; + const base = name.includes('/') ? name.split('/').pop() : name; + const match = base.match(/(?:reactpress-)?(?:template-)?(.+)$/i); + if (!match) return null; + const id = match[1].replace(/^template-/, ''); + return isValidThemeId(id) ? id : null; +} + +function resolveThemeIdentity(packageDir) { + const manifest = readThemeManifestFromDir(packageDir); + if (manifest?.id && isValidThemeId(manifest.id)) { + return { + themeId: manifest.id, + manifest, + }; + } + + const pkgPath = path.join(packageDir, 'package.json'); + if (fs.existsSync(pkgPath)) { + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + const fromReactpress = pkg.reactpress?.themeId || pkg.reactpress?.id; + if (typeof fromReactpress === 'string' && isValidThemeId(fromReactpress)) { + return { + themeId: fromReactpress, + manifest: manifest || { + id: fromReactpress, + name: typeof pkg.description === 'string' ? pkg.description : fromReactpress, + version: typeof pkg.version === 'string' ? pkg.version : '1.0.0', + }, + }; + } + const inferred = inferThemeIdFromPackageName(pkg.name); + if (inferred) { + return { + themeId: inferred, + manifest: manifest || { + id: inferred, + name: typeof pkg.description === 'string' ? pkg.description : inferred, + version: typeof pkg.version === 'string' ? pkg.version : '1.0.0', + }, + packageName: pkg.name, + packageVersion: pkg.version, + }; + } + } catch { + // ignore + } + } + + return null; +} + +function isThemePackageDir(dir) { + return ( + fs.existsSync(path.join(dir, 'theme.json')) || + fs.existsSync(path.join(dir, 'package.json')) + ); +} + +function copyDir(src, dest) { + fs.mkdirSync(dest, { recursive: true }); + for (const entry of fs.readdirSync(src, { withFileTypes: true })) { + if (COPY_SKIP_NAMES.has(entry.name)) continue; + const from = path.join(src, entry.name); + const to = path.join(dest, entry.name); + if (entry.isSymbolicLink()) { + const link = fs.readlinkSync(from); + fs.symlinkSync(link, to); + } else if (entry.isDirectory()) { + copyDir(from, to); + } else if (entry.isFile()) { + fs.copyFileSync(from, to); + } + } +} + +function removeDir(dir) { + if (!fs.existsSync(dir)) return; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const target = path.join(dir, entry.name); + if (entry.isSymbolicLink()) { + fs.unlinkSync(target); + } else if (entry.isDirectory()) { + removeDir(target); + } else { + fs.unlinkSync(target); + } + } + fs.rmdirSync(dir); +} + +function extractTarball(tarballPath, destDir) { + fs.mkdirSync(destDir, { recursive: true }); + const result = spawnSync('tar', ['-xzf', tarballPath, '-C', destDir], { + stdio: 'pipe', + encoding: 'utf8', + }); + if (result.status !== 0) { + throw new Error(result.stderr?.trim() || result.stdout?.trim() || 'Failed to extract theme tarball'); + } + const packageDir = path.join(destDir, 'package'); + if (fs.existsSync(packageDir) && fs.statSync(packageDir).isDirectory()) { + return packageDir; + } + const entries = fs.readdirSync(destDir, { withFileTypes: true }).filter((d) => d.isDirectory()); + if (entries.length === 1) { + return path.join(destDir, entries[0].name); + } + if (isThemePackageDir(destDir)) { + return destDir; + } + throw new Error('Theme package root not found after extracting tarball'); +} + +function npmPack(spec, destDir) { + fs.mkdirSync(destDir, { recursive: true }); + const result = spawnSync('npm', ['pack', spec, '--pack-destination', destDir], { + stdio: 'pipe', + encoding: 'utf8', + shell: process.platform === 'win32', + }); + if (result.status !== 0) { + const message = [result.stderr, result.stdout].filter(Boolean).join('\n').trim(); + throw new Error(message || `npm pack failed for "${spec}"`); + } + const files = fs + .readdirSync(destDir) + .filter((name) => name.endsWith('.tgz') || name.endsWith('.tar.gz')) + .map((name) => path.join(destDir, name)) + .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs); + if (!files.length) { + throw new Error(`npm pack produced no tarball for "${spec}"`); + } + return files[0]; +} + +function ensureRuntimeThemeTsconfigBase(projectRoot, runtimeDir) { + const baseSrc = path.join(path.resolve(projectRoot), 'tsconfig.base.json'); + if (!fs.existsSync(baseSrc)) return; + const runtimeBase = path.join(runtimeDir, 'tsconfig.base.json'); + fs.mkdirSync(runtimeDir, { recursive: true }); + fs.copyFileSync(baseSrc, runtimeBase); +} + +function readThemePackageManager(themeDir) { + const pkgPath = path.join(themeDir, 'package.json'); + if (!fs.existsSync(pkgPath)) return null; + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + return typeof pkg.packageManager === 'string' ? pkg.packageManager : null; + } catch { + return null; + } +} + +function themePrefersPnpm(themeDir) { + if (fs.existsSync(path.join(themeDir, 'pnpm-lock.yaml'))) return true; + const pm = readThemePackageManager(themeDir); + return typeof pm === 'string' && pm.startsWith('pnpm@'); +} + +function installThemeDependencies(themeDir, projectRoot) { + const envOverrides = buildThemeEnvOverrides(projectRoot); + const installEnv = { + ...process.env, + ...envOverrides, + npm_config_ignore_scripts: 'false', + }; + + const usePnpm = themePrefersPnpm(themeDir); + let result; + + if (usePnpm) { + result = spawnSync('pnpm', ['install', '--ignore-workspace'], { + cwd: themeDir, + stdio: 'inherit', + shell: process.platform === 'win32', + env: installEnv, + }); + } else { + result = spawnSync('npm', ['install', '--legacy-peer-deps'], { + cwd: themeDir, + stdio: 'inherit', + shell: process.platform === 'win32', + env: installEnv, + }); + } + + if (result.status !== 0 && usePnpm) { + result = spawnSync('npm', ['install', '--legacy-peer-deps'], { + cwd: themeDir, + stdio: 'inherit', + shell: process.platform === 'win32', + env: installEnv, + }); + } + + if (result.status !== 0 && !usePnpm) { + result = spawnSync('npm', ['install', '--ignore-scripts', '--legacy-peer-deps'], { + cwd: themeDir, + stdio: 'inherit', + shell: process.platform === 'win32', + env: installEnv, + }); + } + + if (result.status !== 0) { + throw new Error(`${usePnpm ? 'pnpm' : 'npm'} install failed in theme directory`); + } + + syncThemeEnvFromProject(projectRoot, themeDir); +} + +function readPackageMeta(packageDir) { + const pkgPath = path.join(packageDir, 'package.json'); + if (!fs.existsSync(pkgPath)) return { name: undefined, version: undefined }; + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + return { + name: typeof pkg.name === 'string' ? pkg.name : undefined, + version: typeof pkg.version === 'string' ? pkg.version : undefined, + }; + } catch { + return { name: undefined, version: undefined }; + } +} + +/** + * Install a theme from an npm spec or local .tgz into `.reactpress/runtime/{id}/`. + * @param {string} projectRoot + * @param {string} spec npm package spec or path to .tgz + * @param {{ skipDependencies?: boolean }} [options] + */ +async function installThemeFromNpm(projectRoot, spec, options = {}) { + const root = path.resolve(projectRoot); + const resolvedSpec = resolveCatalogInstallSpec(root, spec) || spec; + const parsed = parseNpmSpec(resolvedSpec); + if (parsed.error === 'EMPTY_SPEC') { + throw new Error('Theme npm spec is required'); + } + if (parsed.error === 'TARBALL_NOT_FOUND') { + throw new Error(`Theme tarball not found: ${parsed.path}`); + } + + const tmpRoot = path.join(root, '.reactpress', 'tmp', `theme-npm-${crypto.randomBytes(4).toString('hex')}`); + fs.mkdirSync(tmpRoot, { recursive: true }); + + try { + const tarballPath = + parsed.kind === 'tarball' ? parsed.path : npmPack(parsed.spec, tmpRoot); + const extractDir = path.join(tmpRoot, 'extract'); + const packageDir = extractTarball(tarballPath, extractDir); + + if (!isThemePackageDir(packageDir)) { + throw new Error('Package is not a ReactPress theme (missing theme.json or package.json)'); + } + + const identity = resolveThemeIdentity(packageDir); + if (!identity?.themeId) { + throw new Error('Could not resolve theme id from theme.json or package.json'); + } + + const { themeId, manifest } = identity; + const runtimeRoot = path.join(root, THEME_RUNTIME_REL); + const targetDir = path.join(runtimeRoot, themeId); + const pkgMeta = readPackageMeta(packageDir); + + if (fs.existsSync(targetDir)) { + removeDir(targetDir); + } + fs.mkdirSync(runtimeRoot, { recursive: true }); + copyDir(packageDir, targetDir); + ensureRuntimeThemeTsconfigBase(root, runtimeRoot); + + if (!options.skipDependencies) { + installThemeDependencies(targetDir, root); + } else { + syncThemeEnvFromProject(root, targetDir); + } + ensurePreviewFrameAllowed(targetDir); + patchThemeApiProxyRoute(targetDir); + + const npmSpec = parsed.kind === 'npm' ? parsed.spec : resolvedSpec; + upsertNpmThemeLock(root, themeId, { + spec: npmSpec, + resolvedVersion: pkgMeta.version || manifest.version || '0.0.0', + packageName: pkgMeta.name, + }); + + return { + themeId, + name: manifest.name, + version: manifest.version || pkgMeta.version || '0.0.0', + packageName: pkgMeta.name, + npmSpec, + themeDir: targetDir, + themeDirRel: path.relative(root, targetDir), + }; + } finally { + removeDir(tmpRoot); + } +} + +module.exports = { + THEME_RUNTIME_REL, + parseNpmSpec, + resolveThemeIdentity, + installThemeFromNpm, + installThemeDependencies, + isValidThemeId, +}; diff --git a/cli/src/lib/theme-lock.ts b/cli/src/lib/theme-lock.ts new file mode 100644 index 00000000..cae14928 --- /dev/null +++ b/cli/src/lib/theme-lock.ts @@ -0,0 +1,72 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); + +const LOCK_REL = path.join('.reactpress', 'themes.lock.json'); +const LOCK_VERSION = 1; + +function lockPath(projectRoot) { + return path.join(path.resolve(projectRoot), LOCK_REL); +} + +function readThemeLock(projectRoot) { + const file = lockPath(projectRoot); + if (!fs.existsSync(file)) { + return { version: LOCK_VERSION, themes: {} }; + } + try { + const raw = JSON.parse(fs.readFileSync(file, 'utf8')); + if (!raw || typeof raw !== 'object') { + return { version: LOCK_VERSION, themes: {} }; + } + return { + version: typeof raw.version === 'number' ? raw.version : LOCK_VERSION, + themes: raw.themes && typeof raw.themes === 'object' ? raw.themes : {}, + }; + } catch { + return { version: LOCK_VERSION, themes: {} }; + } +} + +function writeThemeLock(projectRoot, lock) { + const file = lockPath(projectRoot); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, `${JSON.stringify(lock, null, 2)}\n`, 'utf8'); +} + +function upsertNpmThemeLock(projectRoot, themeId, entry) { + const lock = readThemeLock(projectRoot); + lock.themes[themeId] = { + source: 'npm', + spec: entry.spec, + resolvedVersion: entry.resolvedVersion, + packageName: entry.packageName, + installedAt: entry.installedAt || new Date().toISOString(), + }; + writeThemeLock(projectRoot, lock); + return lock.themes[themeId]; +} + +function getNpmThemeLockEntry(projectRoot, themeId) { + const lock = readThemeLock(projectRoot); + const entry = lock.themes[themeId]; + if (!entry || entry.source !== 'npm') return null; + return entry; +} + +function removeThemeLockEntry(projectRoot, themeId) { + const lock = readThemeLock(projectRoot); + if (!lock.themes[themeId]) return false; + delete lock.themes[themeId]; + writeThemeLock(projectRoot, lock); + return true; +} + +module.exports = { + LOCK_REL, + readThemeLock, + writeThemeLock, + upsertNpmThemeLock, + getNpmThemeLockEntry, + removeThemeLockEntry, +}; diff --git a/cli/src/lib/theme-paths.ts b/cli/src/lib/theme-paths.ts new file mode 100644 index 00000000..c56bb9ea --- /dev/null +++ b/cli/src/lib/theme-paths.ts @@ -0,0 +1,82 @@ +// @ts-nocheck +const path = require('path'); + +/** Shared path and id constants for theme runtime, registry, and server bridge. */ +const REACTPRESS_DIR = '.reactpress'; +const THEMES_DIR = 'themes'; + +const THEME_RUNTIME_REL = path.join(REACTPRESS_DIR, 'runtime'); +const LEGACY_THEMES_RUNTIME_REL = path.join(THEMES_DIR, 'runtime'); +const ACTIVE_THEME_MANIFEST_REL = path.join(REACTPRESS_DIR, 'active-theme.json'); +const PREVIEW_THEME_MANIFEST_REL = path.join(REACTPRESS_DIR, 'preview-theme.json'); +const PREVIEW_POOL_MANIFEST_REL = path.join(REACTPRESS_DIR, 'preview-pool.json'); +const THEME_LOCK_REL = path.join(REACTPRESS_DIR, 'themes.lock.json'); + +const THEMES_PACKAGE_REL = path.join(THEMES_DIR, 'package.json'); +const THEMES_CATALOG_REL = path.join(THEMES_DIR, 'catalog.json'); +const CLI_CATALOG_TEMPLATE_REL = path.join('cli', 'templates', 'theme-catalog.json'); + +/** Reserved under `themes/` — not bundled templates or theme source trees. */ +const THEMES_RESERVED_SUBDIRS = ['starter', 'bundled', 'core', 'theme-starter']; +const THEMES_LEGACY_STARTER_SUBDIRS = ['starter', 'bundled', 'core']; + +const THEME_ID_RE = /^[a-z0-9][a-z0-9-]*$/i; +const DEFAULT_ACTIVE_THEME = 'hello-world'; +const PREVIEW_PROXY_PORT = 3003; +const PREVIEW_BACKEND_BASE = 3004; +const DEFAULT_PREVIEW_POOL_MAX = 3; + +function getPreviewPoolMaxSize() { + const parsed = parseInt(process.env.REACTPRESS_PREVIEW_POOL_MAX || '', 10); + return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_PREVIEW_POOL_MAX; +} + +function getPreviewProxyPort() { + const parsed = parseInt(process.env.REACTPRESS_PREVIEW_PORT || '', 10); + return Number.isInteger(parsed) && parsed > 0 ? parsed : PREVIEW_PROXY_PORT; +} + +function getPreviewBackendPorts() { + const max = getPreviewPoolMaxSize(); + const parsed = parseInt(process.env.REACTPRESS_PREVIEW_BACKEND_BASE || '', 10); + const base = Number.isInteger(parsed) && parsed > 0 ? parsed : PREVIEW_BACKEND_BASE; + return Array.from({ length: max }, (_, index) => base + index); +} + +/** Public preview URL port (proxy). Backend slots use getPreviewBackendPorts(). */ +const PREVIEW_POOL_PORTS = [PREVIEW_PROXY_PORT]; + +function themesRoot(projectRoot) { + return path.join(path.resolve(projectRoot), THEMES_DIR); +} + +function runtimeRoot(projectRoot) { + return path.join(path.resolve(projectRoot), THEME_RUNTIME_REL); +} + +module.exports = { + REACTPRESS_DIR, + THEMES_DIR, + THEME_RUNTIME_REL, + LEGACY_THEMES_RUNTIME_REL, + ACTIVE_THEME_MANIFEST_REL, + PREVIEW_THEME_MANIFEST_REL, + PREVIEW_POOL_MANIFEST_REL, + THEME_LOCK_REL, + THEMES_PACKAGE_REL, + THEMES_CATALOG_REL, + CLI_CATALOG_TEMPLATE_REL, + THEMES_RESERVED_SUBDIRS, + THEMES_LEGACY_STARTER_SUBDIRS, + THEME_ID_RE, + DEFAULT_ACTIVE_THEME, + PREVIEW_PROXY_PORT, + PREVIEW_BACKEND_BASE, + DEFAULT_PREVIEW_POOL_MAX, + PREVIEW_POOL_PORTS, + getPreviewPoolMaxSize, + getPreviewProxyPort, + getPreviewBackendPorts, + themesRoot, + runtimeRoot, +}; diff --git a/cli/src/lib/theme-placeholder-cover.ts b/cli/src/lib/theme-placeholder-cover.ts new file mode 100644 index 00000000..96ed0c41 --- /dev/null +++ b/cli/src/lib/theme-placeholder-cover.ts @@ -0,0 +1,102 @@ +/** + * SVG placeholder cover for themes without a cover image file. + * Used by the extension API and web dev mocks. + */ + +export type ThemePlaceholderCoverOptions = { + id: string; + name: string; + primary?: string; + accent?: string; + version?: string; +}; + +function escapeXml(text: string): string { + return String(text).replace(/[<>&"']/g, (ch) => { + const map: Record = { + "<": "<", + ">": ">", + "&": "&", + '"': """, + "'": "'", + }; + return map[ch] ?? ch; + }); +} + +function sanitizeColor(color: string | undefined, fallback: string): string { + if (!color) return fallback; + const trimmed = String(color).trim(); + if (/^#[0-9a-fA-F]{3,8}$/.test(trimmed)) return trimmed; + return fallback; +} + +function safeSvgId(id: string): string { + return String(id).replace(/[^a-zA-Z0-9_-]/g, "") || "theme"; +} + +export function buildThemePlaceholderCoverSvg(options: ThemePlaceholderCoverOptions): string { + const svgId = safeSvgId(options.id); + const primary = sanitizeColor(options.primary, "#2563eb"); + const accent = sanitizeColor(options.accent, "#7c3aed"); + const rawName = String(options.name ?? options.id ?? "Theme"); + const safeName = escapeXml(rawName.length > 48 ? `${rawName.slice(0, 45)}…` : rawName); + const version = options.version ? escapeXml(String(options.version)) : ""; + const versionBadge = version + ? ` + v${version}` + : ""; + + return ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + REACTPRESS + ${versionBadge} + + + + + + + + + + + + + + + + + + ${safeName} + Theme Preview +`; +} diff --git a/cli/src/lib/theme-preview-frame.ts b/cli/src/lib/theme-preview-frame.ts new file mode 100644 index 00000000..53c0b470 --- /dev/null +++ b/cli/src/lib/theme-preview-frame.ts @@ -0,0 +1,113 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); + +const PATCH_MARKER = '.reactpress-preview-frame-patched'; +const X_FRAME_OPTIONS_RE = + /\{\s*key:\s*['"]X-Frame-Options['"],\s*value:\s*['"]SAMEORIGIN['"]\s*\},?\s*\n?/g; +const X_FRAME_OPTIONS_HEADER_KEY = 'X-Frame-Options'; + +/** Admin iframe loads theme on another port — skip X-Frame-Options in local/desktop dev. */ +function shouldHonorThemePreviewFrame() { + if (process.env.REACTPRESS_HONOR_PREVIEW === '1') return true; + if (process.env.REACTPRESS_DESKTOP_LOCAL === '1') return true; + if (process.env.REACTPRESS_DESKTOP_SITE_ROOT?.trim()) return true; + return false; +} + +/** + * Next.js bakes `headers()` into routes-manifest.json at build time. + * Strip X-Frame-Options so admin iframes work without a full rebuild. + */ +function stripBakedFrameOptionsFromBuild(themeDir, distDir = '.next') { + if (!themeDir || !distDir) return false; + + const manifestPath = path.join(themeDir, distDir, 'routes-manifest.json'); + if (!fs.existsSync(manifestPath)) return false; + + let manifest; + try { + manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + } catch { + return false; + } + + if (!Array.isArray(manifest.headers)) return false; + + let changed = false; + for (const entry of manifest.headers) { + if (!entry || !Array.isArray(entry.headers)) continue; + const nextHeaders = entry.headers.filter( + (header) => header?.key !== X_FRAME_OPTIONS_HEADER_KEY, + ); + if (nextHeaders.length !== entry.headers.length) { + entry.headers = nextHeaders; + changed = true; + } + } + + if (!changed) return false; + + fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8'); + return true; +} + +/** Patch next.config and strip baked headers when admin preview needs iframe embedding. */ +function ensureBuildAllowsPreviewFrame(themeDir, distDir = '.next') { + if (!shouldHonorThemePreviewFrame()) return false; + ensurePreviewFrameAllowed(themeDir); + return stripBakedFrameOptionsFromBuild(themeDir, distDir); +} + +const X_FRAME_OPTIONS_PATCH = `...(process.env.REACTPRESS_HONOR_PREVIEW === '1' + ? [] + : [{ key: 'X-Frame-Options', value: 'SAMEORIGIN' }]), + `; + +/** + * Admin preview iframes load :3003 from a different origin than /admin/. + * Drop X-Frame-Options for preview dev only (REACTPRESS_HONOR_PREVIEW=1). + */ +function ensurePreviewFrameAllowed(themeDir) { + if (!themeDir || !fs.existsSync(themeDir)) return false; + + const markerPath = path.join(themeDir, PATCH_MARKER); + const configPath = path.join(themeDir, 'next.config.js'); + const configMjsPath = path.join(themeDir, 'next.config.mjs'); + + const target = fs.existsSync(configPath) + ? configPath + : fs.existsSync(configMjsPath) + ? configMjsPath + : null; + + if (!target) return false; + if (fs.existsSync(markerPath)) return true; + + let src = fs.readFileSync(target, 'utf8'); + if (!X_FRAME_OPTIONS_RE.test(src)) { + fs.writeFileSync(markerPath, `${new Date().toISOString()}\n`, 'utf8'); + return false; + } + + X_FRAME_OPTIONS_RE.lastIndex = 0; + if (src.includes('REACTPRESS_HONOR_PREVIEW')) { + fs.writeFileSync(markerPath, `${new Date().toISOString()}\n`, 'utf8'); + return true; + } + + const next = src.replace(X_FRAME_OPTIONS_RE, X_FRAME_OPTIONS_PATCH); + if (next === src) return false; + + fs.writeFileSync(target, next, 'utf8'); + fs.writeFileSync(markerPath, `${new Date().toISOString()}\n`, 'utf8'); + return true; +} + +module.exports = { + PATCH_MARKER, + shouldHonorThemePreviewFrame, + stripBakedFrameOptionsFromBuild, + ensureBuildAllowsPreviewFrame, + ensurePreviewFrameAllowed, +}; diff --git a/cli/src/lib/theme-preview-pool.ts b/cli/src/lib/theme-preview-pool.ts new file mode 100644 index 00000000..7ba6e32a --- /dev/null +++ b/cli/src/lib/theme-preview-pool.ts @@ -0,0 +1,570 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const { spawnDevChild } = require('./dev-child-io'); +const { loadClientSiteUrl, normalizeProbeUrl, probeHttp, waitForHttpOk } = require('./http'); +const { isPortListening, killPortListeners } = require('./ports'); +const { + resolveThemeDirectory, + isThemePackageDir, + themeWorkspaceRoot, +} = require('./theme-runtime'); +const { + getPreviewBackendPorts, + getPreviewPoolMaxSize, + getPreviewProxyPort, + PREVIEW_POOL_PORTS, +} = require('./theme-paths'); +const { + enqueueThemeBuild, + resolvePreviewThemeEnv, + ensureThemeDependenciesInstalled, + PREVIEW_DIST_DIR, +} = require('./theme-prod'); +const { warmupThemeHomepage } = require('./theme-warmup'); +const { + ensurePreviewFrameAllowed, + ensureBuildAllowsPreviewFrame, + shouldHonorThemePreviewFrame, +} = require('./theme-preview-frame'); +const { + ensurePreviewProxyRunning, + setPreviewProxyTarget, + stopPreviewProxy, +} = require('./theme-preview-proxy'); +const { t } = require('./i18n'); + +const PREVIEW_POOL_MANIFEST = path.join('.reactpress', 'preview-pool.json'); +const PREVIEW_READY_POLL_MS = 100; +const PREVIEW_READY_TIMEOUT_MS = + process.env.REACTPRESS_DESKTOP_LOCAL === '1' ? 15_000 : 120_000; +const PREVIEW_PORT_RELEASE_PAUSE_MS = + process.env.REACTPRESS_DESKTOP_LOCAL === '1' ? 120 : 400; + +/** @type {Map} */ +const previewPool = new Map(); + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function getPreviewPoolManifestPath(projectRoot) { + return path.join(themeWorkspaceRoot(projectRoot), PREVIEW_POOL_MANIFEST); +} + +function getPreviewSiteUrlForPort(projectRoot, port) { + try { + const url = new URL(loadClientSiteUrl(projectRoot)); + url.port = String(port); + return `${url.origin}/`; + } catch { + return `http://127.0.0.1:${port}/`; + } +} + +function getPreviewPublicUrl(projectRoot) { + return getPreviewSiteUrlForPort(projectRoot, getPreviewProxyPort()); +} + +function readPreviewPoolManifest(projectRoot) { + const manifestPath = getPreviewPoolManifestPath(projectRoot); + if (!fs.existsSync(manifestPath)) return {}; + try { + const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + return raw && typeof raw === 'object' ? raw : {}; + } catch { + return {}; + } +} + +function writePreviewPoolManifest(projectRoot) { + const manifestPath = getPreviewPoolManifestPath(projectRoot); + const proxyPort = getPreviewProxyPort(); + const next = {}; + for (const [themeId, entry] of previewPool) { + if (!entry?.backendPort) continue; + next[themeId] = { + port: String(proxyPort), + backendPort: String(entry.backendPort), + url: getPreviewSiteUrlForPort(projectRoot, proxyPort), + updatedAt: new Date(entry.lastUsed || Date.now()).toISOString(), + }; + } + fs.mkdirSync(path.dirname(manifestPath), { recursive: true }); + fs.writeFileSync(manifestPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8'); +} + +async function isBackendReady(projectRoot, backendPort) { + const url = `${getPreviewSiteUrlForPort(projectRoot, backendPort).replace(/\/$/, '')}/`; + const result = await probeHttp(normalizeProbeUrl(url), 1200); + return result.ok; +} + +async function isPreviewHomepageReady(projectRoot, port) { + const url = `${getPreviewSiteUrlForPort(projectRoot, port).replace(/\/$/, '')}/`; + const result = await probeHttp(normalizeProbeUrl(url), 1200); + return result.ok; +} + +function stopPreviewPoolChild(entry) { + const child = entry?.child; + if (!child || child.killed) { + if (entry) entry.child = null; + return; + } + const pid = child.pid; + try { + if (process.platform !== 'win32' && pid) { + try { + process.kill(-pid, 'SIGTERM'); + } catch { + child.kill('SIGTERM'); + } + } else if (pid) { + child.kill('SIGTERM'); + } + } catch { + // ignore + } + entry.child = null; +} + +function stopPreviewPoolTheme(themeId) { + const entry = previewPool.get(themeId); + if (!entry) return; + stopPreviewPoolChild(entry); + previewPool.delete(themeId); +} + +async function stopAllPreviewPool(projectRoot) { + for (const themeId of [...previewPool.keys()]) { + stopPreviewPoolTheme(themeId); + } + await stopPreviewProxy(); + const manifestPath = getPreviewPoolManifestPath(projectRoot); + if (fs.existsSync(manifestPath)) { + fs.unlinkSync(manifestPath); + } +} + +async function releasePreviewPort(port) { + if (!isPortListening(port)) return true; + killPortListeners(port, 'TERM'); + await sleep(PREVIEW_PORT_RELEASE_PAUSE_MS); + if (!isPortListening(port)) return true; + killPortListeners(port, 'KILL'); + await sleep(PREVIEW_PORT_RELEASE_PAUSE_MS); + return !isPortListening(port); +} + +/** Serialize preview pool mutations (desktop + CLI). */ +let previewPortLock = Promise.resolve(); + +function withPreviewPortLock(fn) { + const run = previewPortLock.then(() => fn()); + previewPortLock = run.catch(() => {}); + return run; +} + +function allocateBackendPort(themeId) { + const existing = previewPool.get(themeId); + if (existing?.backendPort) { + return existing.backendPort; + } + + const backendPorts = getPreviewBackendPorts(); + const usedPorts = new Set( + [...previewPool.values()] + .map((entry) => entry.backendPort) + .filter((port) => Number.isInteger(port)), + ); + + for (const port of backendPorts) { + if (!usedPorts.has(port)) return port; + } + + let oldestId = null; + let oldestAt = Infinity; + for (const [id, entry] of previewPool) { + if (id === themeId) continue; + const ts = entry.lastUsed || 0; + if (ts < oldestAt) { + oldestAt = ts; + oldestId = id; + } + } + + if (oldestId) { + const evicted = previewPool.get(oldestId); + const port = evicted?.backendPort ?? backendPorts[0]; + stopPreviewPoolTheme(oldestId); + return port; + } + + return backendPorts[0]; +} + +function isChildAlive(child) { + return Boolean(child && !child.killed && child.exitCode == null && child.signalCode == null); +} + +function buildPreviewResult(projectRoot, themeId, backendPort, reused) { + const proxyPort = getPreviewProxyPort(); + return { + themeId, + port: proxyPort, + backendPort, + url: getPreviewPublicUrl(projectRoot), + reused, + }; +} + +async function activateWarmPreviewEntry(projectRoot, themeId, entry) { + setPreviewProxyTarget(entry.backendPort); + entry.lastUsed = Date.now(); + writePreviewPoolManifest(projectRoot); + const proxyPort = getPreviewProxyPort(); + const ready = await isPreviewHomepageReady(projectRoot, proxyPort); + if (!ready) { + const homepageUrl = `${getPreviewPublicUrl(projectRoot).replace(/\/$/, '')}/`; + await waitForHttpOk(homepageUrl, PREVIEW_READY_TIMEOUT_MS, PREVIEW_READY_POLL_MS); + } + return buildPreviewResult(projectRoot, themeId, entry.backendPort, true); +} + +/** + * Spawn a theme server child for desktop visitor/preview roles. + * Caller owns lifecycle logging and process tracking. + */ +function spawnThemeProcess(projectRoot, options) { + const { spawn } = require('child_process'); + const { + themeDir, + themeId, + port, + serverApiUrl, + publicApiUrl, + launch, + role = 'visitor', + extraEnv = {}, + } = options; + const distDir = role === 'preview' ? PREVIEW_DIST_DIR : '.next'; + const { cmd, args } = launch; + + if (launch.mode === 'production' && shouldHonorThemePreviewFrame()) { + ensureBuildAllowsPreviewFrame(themeDir, distDir); + } + + return spawn(cmd, args, { + cwd: themeDir, + detached: process.platform !== 'win32', + shell: process.platform === 'win32', + stdio: ['ignore', 'pipe', 'pipe'], + env: { + ...resolvePreviewThemeEnv(projectRoot, themeDir, port, { + mode: launch.mode, + distDir, + }), + SERVER_API_URL: serverApiUrl, + REACTPRESS_API_URL: serverApiUrl, + NEXT_PUBLIC_REACTPRESS_API_URL: publicApiUrl, + REACTPRESS_THEME_ID: themeId, + REACTPRESS_HONOR_PREVIEW: role === 'preview' || shouldHonorThemePreviewFrame() ? '1' : '0', + REACTPRESS_SKIP_DEV_PORT_REDIRECT: '1', + REACTPRESS_SKIP_BROWSER_OPEN: '1', + REACTPRESS_DESKTOP_THEME_ROLE: role, + ...extraEnv, + }, + }); +} + +function themeHasCustomServer(themeDir) { + return fs.existsSync(path.join(themeDir, 'server.js')); +} + +function themeHasDevScript(themeDir) { + try { + const pkg = JSON.parse(fs.readFileSync(path.join(themeDir, 'package.json'), 'utf8')); + return typeof pkg.scripts?.dev === 'string'; + } catch { + return false; + } +} + +function themeUsesAppRouter(themeDir) { + return fs.existsSync(path.join(themeDir, 'app')); +} + +function isThemeOnlyDevMode() { + return process.env.REACTPRESS_THEME_DEV_ONLY === '1'; +} + +function isIntegratedDesktopDev() { + if (isThemeOnlyDevMode()) return false; + if (process.env.REACTPRESS_DESKTOP_LOCAL === '1') return true; + if (process.env.REACTPRESS_DESKTOP_SITE_ROOT?.trim()) return true; + return false; +} + +function shouldPreferProductionLaunch(themeDir) { + if (isThemeOnlyDevMode()) return false; + if (themeUsesAppRouter(themeDir)) return true; + if (isIntegratedDesktopDev() && themeHasCustomServer(themeDir)) return true; + return false; +} + +/** Resolve Next CLI bin — theme-local, NODE_PATH, or packaged Resources/runtime-deps. */ +function resolveThemeNextBin(themeDir) { + const rel = path.join('next', 'dist', 'bin', 'next'); + const local = path.join(themeDir, 'node_modules', rel); + if (fs.existsSync(local)) return local; + + const nodePath = process.env.NODE_PATH?.split(path.delimiter).filter(Boolean) ?? []; + for (const dir of nodePath) { + const candidate = path.join(dir, rel); + if (fs.existsSync(candidate)) return candidate; + } + + const monorepoRoot = process.env.REACTPRESS_MONOREPO_ROOT?.trim(); + if (monorepoRoot) { + const bundled = [ + path.join(monorepoRoot, 'runtime-deps', 'node_modules', rel), + path.join(monorepoRoot, 'node_modules', rel), + ]; + for (const candidate of bundled) { + if (fs.existsSync(candidate)) return candidate; + } + } + + try { + return require.resolve('next/dist/bin/next', { paths: [themeDir, ...nodePath] }); + } catch { + return null; + } +} + +function resolvePreviewThemeLaunchPlan(themeDir, port, options = {}) { + const preferProduction = + options.preferProduction ?? shouldPreferProductionLaunch(themeDir); + + if (!preferProduction && themeHasDevScript(themeDir)) { + return { mode: 'dev', cmd: 'pnpm', args: ['run', 'dev', '--', '--port', String(port)] }; + } + + const nextBin = resolveThemeNextBin(themeDir); + // Prefer `next start -p` — hello-world server.js uses Next CLI internals that ignore `-p` on Next 15. + if (preferProduction && nextBin) { + return { mode: 'production', cmd: process.execPath, args: [nextBin, 'start', '-p', String(port)] }; + } + + if (themeHasCustomServer(themeDir)) { + return { mode: 'production', cmd: 'node', args: ['server.js'] }; + } + + if (nextBin) { + return { mode: 'production', cmd: process.execPath, args: [nextBin, 'start', '-p', String(port)] }; + } + + return { mode: 'production', cmd: 'pnpm', args: ['run', 'start'] }; +} + +/** @type {Map>} */ +const previewEnsureInflight = new Map(); + +async function ensurePreviewThemeRunning( + projectRoot, + themeId, + { serverApiUrl, publicApiUrl, spawnOptions = {} } = {}, +) { + const inflight = previewEnsureInflight.get(themeId); + if (inflight) return inflight; + + const job = startPreviewThemeRunning(projectRoot, themeId, { + serverApiUrl, + publicApiUrl, + spawnOptions, + }).finally(() => { + if (previewEnsureInflight.get(themeId) === job) { + previewEnsureInflight.delete(themeId); + } + }); + previewEnsureInflight.set(themeId, job); + return job; +} + +async function startPreviewThemeRunning( + projectRoot, + themeId, + { serverApiUrl, publicApiUrl, spawnOptions = {} } = {}, +) { + let themeDir = resolveThemeDirectory(projectRoot, themeId); + if (spawnOptions.resolveThemeDir) { + themeDir = spawnOptions.resolveThemeDir(projectRoot, themeId) || themeDir; + } + if (!themeDir || !isThemePackageDir(projectRoot, themeDir)) { + return null; + } + + await ensurePreviewProxyRunning(getPreviewProxyPort()); + + const pooled = previewPool.get(themeId); + if (pooled && isChildAlive(pooled.child)) { + const backendReady = await isBackendReady(projectRoot, pooled.backendPort); + if (backendReady) { + return activateWarmPreviewEntry(projectRoot, themeId, pooled); + } + stopPreviewPoolChild(pooled); + } + + const backendPort = allocateBackendPort(themeId); + await releasePreviewPort(backendPort); + + try { + ensureThemeDependenciesInstalled(projectRoot, themeDir, themeId, 'themePreview'); + ensurePreviewFrameAllowed(themeDir); + } catch (err) { + console.warn( + `[reactpress] ${t('themePreview.buildFailed', { + id: themeId, + message: err.message || err, + })}`, + ); + return null; + } + + let launch = resolvePreviewThemeLaunchPlan(themeDir, backendPort); + if (typeof spawnOptions.normalizeLaunch === 'function') { + launch = spawnOptions.normalizeLaunch(launch, { + themeDir, + port: backendPort, + projectRoot, + themeId, + }); + } + + if (launch.mode === 'production') { + try { + await enqueueThemeBuild(projectRoot, themeId, { + logPrefix: 'themePreview', + distDir: PREVIEW_DIST_DIR, + }); + ensureBuildAllowsPreviewFrame(themeDir, PREVIEW_DIST_DIR); + } catch (err) { + console.warn( + `[reactpress] ${t('themePreview.buildFailed', { + id: themeId, + message: err.message || err, + })}`, + ); + return null; + } + } + + const relDir = path.relative(projectRoot, themeDir) || themeDir; + const modeLabel = launch.mode === 'dev' ? 'dev' : 'production'; + console.log( + `[reactpress] ${t('themePreview.starting', { + id: themeId, + url: getPreviewPublicUrl(projectRoot), + port: backendPort, + dir: relDir, + mode: modeLabel, + })}`, + ); + + const { cmd, args } = launch; + const child = spawnOptions.useThemeProcessSpawn + ? spawnThemeProcess(projectRoot, { + themeDir, + themeId, + port: backendPort, + serverApiUrl, + publicApiUrl, + launch, + role: 'preview', + extraEnv: spawnOptions.extraEnv || {}, + }) + : spawnDevChild(cmd, args, { + cwd: themeDir, + detached: process.platform !== 'win32', + shell: process.platform === 'win32', + env: { + ...resolvePreviewThemeEnv(projectRoot, themeDir, backendPort, { + mode: launch.mode, + distDir: PREVIEW_DIST_DIR, + }), + SERVER_API_URL: serverApiUrl, + REACTPRESS_API_URL: serverApiUrl, + NEXT_PUBLIC_REACTPRESS_API_URL: publicApiUrl, + REACTPRESS_THEME_ID: themeId, + REACTPRESS_HONOR_PREVIEW: '1', + REACTPRESS_SKIP_DEV_PORT_REDIRECT: '1', + REACTPRESS_SKIP_BROWSER_OPEN: '1', + ...(spawnOptions.extraEnv || {}), + }, + }); + + previewPool.set(themeId, { child, backendPort, lastUsed: Date.now() }); + writePreviewPoolManifest(projectRoot); + + child.on('exit', () => { + const current = previewPool.get(themeId); + if (current?.child === child) { + previewPool.delete(themeId); + writePreviewPoolManifest(projectRoot); + } + }); + + const backendUrl = `${getPreviewSiteUrlForPort(projectRoot, backendPort).replace(/\/$/, '')}/`; + const backendReady = await waitForHttpOk( + backendUrl, + PREVIEW_READY_TIMEOUT_MS, + PREVIEW_READY_POLL_MS, + ); + if (!backendReady) { + console.warn(t('themeDev.slow', { url: backendUrl })); + } + + setPreviewProxyTarget(backendPort); + writePreviewPoolManifest(projectRoot); + + const homepageUrl = `${getPreviewPublicUrl(projectRoot).replace(/\/$/, '')}/`; + const proxyReady = await waitForHttpOk(homepageUrl, PREVIEW_READY_TIMEOUT_MS, PREVIEW_READY_POLL_MS); + if (proxyReady) { + console.log(`[reactpress] ${t('themePreview.ready', { url: homepageUrl, id: themeId })}`); + warmupThemeHomepage(projectRoot, homepageUrl).catch(() => {}); + } else { + console.warn(t('themeDev.slow', { url: homepageUrl })); + } + + return buildPreviewResult(projectRoot, themeId, backendPort, false); +} + +module.exports = { + PREVIEW_POOL_PORTS, + PREVIEW_POOL_MANIFEST, + previewPool, + getPreviewProxyPort, + getPreviewBackendPorts, + getPreviewPoolMaxSize, + getPreviewSiteUrlForPort, + getPreviewPublicUrl, + readPreviewPoolManifest, + writePreviewPoolManifest, + ensurePreviewThemeRunning, + ensurePreviewProxyRunning, + stopPreviewPoolTheme, + stopAllPreviewPool, + isPreviewHomepageReady, + isBackendReady, + resolvePreviewThemeLaunchPlan, + themeUsesAppRouter, + shouldPreferProductionLaunch, + isIntegratedDesktopDev, + shouldHonorThemePreviewFrame, + releasePreviewPort, + withPreviewPortLock, + spawnThemeProcess, + allocateBackendPort, + setPreviewProxyTarget, +}; diff --git a/cli/src/lib/theme-preview-proxy.ts b/cli/src/lib/theme-preview-proxy.ts new file mode 100644 index 00000000..64609ac3 --- /dev/null +++ b/cli/src/lib/theme-preview-proxy.ts @@ -0,0 +1,117 @@ +// @ts-nocheck +/** Stable :3003 front door — forwards to warm theme backends on :3004+. */ +const http = require('http'); +const { getPreviewProxyPort } = require('./theme-paths'); + +const PREVIEW_CORS_HEADERS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With', +}; + +let proxyTargetPort = null; +/** @type {import('http').Server | null} */ +let proxyServer = null; +let listenPort = null; + +function setPreviewProxyTarget(backendPort) { + proxyTargetPort = backendPort; +} + +function getPreviewProxyTarget() { + return proxyTargetPort; +} + +function proxyRequest(req, res) { + if (req.method === 'OPTIONS') { + res.writeHead(204, PREVIEW_CORS_HEADERS); + res.end(); + return; + } + + if (!proxyTargetPort) { + res.writeHead(503, { + ...PREVIEW_CORS_HEADERS, + 'Content-Type': 'text/plain; charset=utf-8', + }); + res.end('Theme preview starting…'); + return; + } + + const headers = { + ...req.headers, + host: `127.0.0.1:${proxyTargetPort}`, + }; + delete headers.origin; + delete headers.referer; + + const proxyReq = http.request( + { + hostname: '127.0.0.1', + port: proxyTargetPort, + path: req.url, + method: req.method, + headers, + }, + (proxyRes) => { + const outHeaders = { ...proxyRes.headers, ...PREVIEW_CORS_HEADERS }; + res.writeHead(proxyRes.statusCode || 502, outHeaders); + proxyRes.pipe(res); + }, + ); + + proxyReq.on('error', () => { + if (!res.headersSent) { + res.writeHead(502, { + ...PREVIEW_CORS_HEADERS, + 'Content-Type': 'text/plain; charset=utf-8', + }); + res.end('Preview backend unavailable'); + } + }); + + if (req.method === 'GET' || req.method === 'HEAD') { + proxyReq.end(); + } else { + req.pipe(proxyReq); + } +} + +function ensurePreviewProxyRunning(port) { + const proxyPort = port ?? getPreviewProxyPort(); + if (proxyServer && listenPort === proxyPort) { + return Promise.resolve(proxyServer); + } + + return stopPreviewProxy().then( + () => + new Promise((resolve, reject) => { + const server = http.createServer(proxyRequest); + server.on('error', reject); + server.listen(proxyPort, '127.0.0.1', () => { + proxyServer = server; + listenPort = proxyPort; + resolve(server); + }); + }), + ); +} + +function stopPreviewProxy() { + proxyTargetPort = null; + if (!proxyServer) return Promise.resolve(); + + const server = proxyServer; + proxyServer = null; + listenPort = null; + return new Promise((resolve) => { + server.close(() => resolve()); + }); +} + +module.exports = { + setPreviewProxyTarget, + getPreviewProxyTarget, + ensurePreviewProxyRunning, + stopPreviewProxy, +}; diff --git a/cli/src/lib/theme-prod.ts b/cli/src/lib/theme-prod.ts new file mode 100644 index 00000000..82bcc834 --- /dev/null +++ b/cli/src/lib/theme-prod.ts @@ -0,0 +1,439 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); +const { runSync, runNodeScript, resolveCliScript } = require('./spawn'); +const { getThemeBin, resolveProjectRoot } = require('./paths'); +const { + readActiveThemeManifest, + resolveThemeDirectory, + listAvailableThemeIds, +} = require('./theme-runtime'); +const { t } = require('./i18n'); +const { resolveBuildNodeEnv } = require('./prod-memory'); +const { shouldHonorThemePreviewFrame } = require('./theme-preview-frame'); + +function resolveProductionThemeEnv(projectRoot, themeDir) { + const nginxEntry = ( + process.env.REACTPRESS_NGINX_ENTRY_URL || + process.env.NGINX_ENTRY_URL || + 'http://localhost' + ).replace(/\/$/, ''); + const visitorPort = + process.env.CLIENT_PORT || process.env.PORT || '3001'; + const serverApiUrl = + process.env.REACTPRESS_THEME_API_URL || + process.env.SERVER_API_URL || + process.env.REACTPRESS_API_URL || + `${nginxEntry}/api`; + const publicApiUrl = + process.env.REACTPRESS_THEME_PUBLIC_API_URL || + process.env.NEXT_PUBLIC_REACTPRESS_API_URL || + `${nginxEntry}/api`; + + const clientSiteUrl = + process.env.CLIENT_SITE_URL?.trim() || `http://127.0.0.1:${visitorPort}`; + + return { + ...process.env, + NODE_ENV: 'production', + REACTPRESS_ORIGINAL_CWD: projectRoot, + REACTPRESS_THEME_DIR: themeDir, + PORT: String(visitorPort), + CLIENT_PORT: String(visitorPort), + CLIENT_SITE_URL: clientSiteUrl, + NGINX_ENTRY_URL: nginxEntry, + REACTPRESS_NGINX_ENTRY_URL: nginxEntry, + REACTPRESS_API_URL: serverApiUrl, + SERVER_API_URL: serverApiUrl, + NEXT_PUBLIC_REACTPRESS_API_URL: publicApiUrl, + NEXT_PUBLIC_REACTPRESS_ADMIN_URL: + process.env.NEXT_PUBLIC_REACTPRESS_ADMIN_URL || `${nginxEntry}/admin`, + }; +} + +function resolveThemeClientBin(projectRoot, themeDir) { + const themeBin = path.join(themeDir, 'bin', 'reactpress-client.js'); + if (fs.existsSync(themeBin)) return themeBin; + const generic = resolveCliScript('bin/reactpress-theme-client.js'); + if (fs.existsSync(generic)) return generic; + throw new Error(`Theme entry not found under ${themeDir}`); +} + +const LAUNCH_FILE_REL_PATHS = ['server.js']; + +function syncThemeLaunchFilesFromTemplate(projectRoot, themeId, themeDir) { + const templateDir = path.join(resolveProjectRoot(projectRoot), 'themes', themeId); + if (!templateDir || !fs.existsSync(templateDir)) return; + if (path.resolve(templateDir) === path.resolve(themeDir)) return; + + for (const rel of LAUNCH_FILE_REL_PATHS) { + const src = path.join(templateDir, rel); + const dest = path.join(themeDir, rel); + if (!fs.existsSync(src)) continue; + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.copyFileSync(src, dest); + } + + const templatePkg = path.join(templateDir, 'package.json'); + const destPkg = path.join(themeDir, 'package.json'); + if (fs.existsSync(templatePkg) && fs.existsSync(destPkg)) { + try { + const srcScripts = JSON.parse(fs.readFileSync(templatePkg, 'utf8')).scripts || {}; + const destPkgJson = JSON.parse(fs.readFileSync(destPkg, 'utf8')); + destPkgJson.scripts = { ...destPkgJson.scripts, start: srcScripts.start, dev: srcScripts.dev }; + fs.writeFileSync(destPkg, `${JSON.stringify(destPkgJson, null, 2)}\n`, 'utf8'); + } catch { + // ignore corrupt package.json + } + } +} + +const PREVIEW_DIST_DIR = '.next-preview'; +const BUILD_STAMP_REL = path.join('.next', '.reactpress-theme-id'); + +function resolveBuildDistDir(options = {}) { + return options.distDir || '.next'; +} + +function buildStampRel(distDir) { + return path.join(distDir, '.reactpress-theme-id'); +} + +function writeThemeBuildStamp(themeDir, themeId, options = {}) { + const distDir = resolveBuildDistDir(options); + const stampPath = path.join(themeDir, buildStampRel(distDir)); + fs.mkdirSync(path.dirname(stampPath), { recursive: true }); + fs.writeFileSync(stampPath, themeId, 'utf8'); +} + +function newestSourceMtime(rootDir, depth = 0) { + if (!fs.existsSync(rootDir)) return 0; + let max = 0; + for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name === '.next' || entry.name === PREVIEW_DIST_DIR) { + continue; + } + const full = path.join(rootDir, entry.name); + if (entry.isDirectory()) { + if (depth < 10) max = Math.max(max, newestSourceMtime(full, depth + 1)); + continue; + } + if (entry.isFile()) max = Math.max(max, fs.statSync(full).mtimeMs); + } + return max; +} + +/** Post-build artifacts — not source changes; ignored when comparing freshness. */ +const GENERATED_PUBLIC_FILES = new Set([ + 'robots.txt', + 'sitemap.xml', + 'sitemap-0.xml', + 'sitemap-1.xml', +]); + +function themeSourcesNewerThanBuild(themeDir, distDir = '.next') { + const stampPath = path.join(themeDir, buildStampRel(distDir)); + if (!fs.existsSync(stampPath)) return true; + const buildMtime = fs.statSync(stampPath).mtimeMs; + + for (const rel of [ + 'app', + 'pages', + 'src', + 'public', + 'theme.json', + 'package.json', + 'next.config.js', + ]) { + const target = path.join(themeDir, rel); + if (!fs.existsSync(target)) continue; + const stat = fs.statSync(target); + if (stat.isDirectory()) { + if (rel === 'public') { + for (const entry of fs.readdirSync(target, { withFileTypes: true })) { + if (!entry.isFile() || GENERATED_PUBLIC_FILES.has(entry.name)) continue; + if (fs.statSync(path.join(target, entry.name)).mtimeMs > buildMtime) return true; + } + continue; + } + if (newestSourceMtime(target) > buildMtime) return true; + continue; + } + if (stat.mtimeMs > buildMtime) return true; + } + return false; +} + +function hasProductionBuildArtifacts(nextDir) { + if (fs.existsSync(path.join(nextDir, 'BUILD_ID'))) return true; + // Next 12 Pages Router — no BUILD_ID at dist root + return fs.existsSync(path.join(nextDir, 'server', 'pages-manifest.json')); +} + +function hasUsableProductionBuild(themeDir, themeId, options = {}) { + if (process.env.REACTPRESS_FORCE_THEME_BUILD === '1') return false; + const distDir = resolveBuildDistDir(options); + const nextDir = path.join(themeDir, distDir); + if (!hasProductionBuildArtifacts(nextDir)) return false; + if (!fs.existsSync(path.join(nextDir, 'server'))) return false; + const stampPath = path.join(themeDir, buildStampRel(distDir)); + if (!fs.existsSync(stampPath)) return false; + try { + if (fs.readFileSync(stampPath, 'utf8').trim() !== themeId) return false; + } catch { + return false; + } + if (themeSourcesNewerThanBuild(themeDir, distDir)) return false; + return true; +} + +function resolvePreviewThemeEnv(projectRoot, themeDir, port, options = {}) { + const distDir = options.distDir || PREVIEW_DIST_DIR; + const base = resolveProductionThemeEnv(projectRoot, themeDir); + let clientSiteUrl = base.CLIENT_SITE_URL; + try { + const url = new URL(clientSiteUrl || 'http://127.0.0.1:3001'); + url.port = String(port); + clientSiteUrl = url.origin; + } catch { + clientSiteUrl = `http://127.0.0.1:${port}`; + } + return { + ...base, + NODE_ENV: options.mode === 'dev' ? 'development' : 'production', + INIT_CWD: themeDir, + NEXT_DIST_DIR: distDir, + PORT: String(port), + CLIENT_PORT: String(port), + CLIENT_SITE_URL: clientSiteUrl, + REACTPRESS_THEME_DIR: themeDir, + NEXT_TELEMETRY_DISABLED: '1', + NEXT_IGNORE_INCORRECT_LOCKFILE: '1', + }; +} + +function canResolveSharedNext(themeDir) { + const searchPaths = [themeDir]; + const nodePath = String(process.env.NODE_PATH || '').trim(); + if (nodePath) { + searchPaths.push(...nodePath.split(path.delimiter).filter(Boolean)); + } + try { + require.resolve('next/package.json', { paths: searchPaths }); + return true; + } catch { + return false; + } +} + +function ensureThemeDependenciesInstalled(projectRoot, themeDir, themeId, logPrefix = 'themePreview') { + if (process.env.REACTPRESS_SKIP_THEME_INSTALL === '1' || canResolveSharedNext(themeDir)) { + return; + } + + const nextModule = path.join(themeDir, 'node_modules', 'next'); + if (fs.existsSync(nextModule)) return; + + const { installThemeDependencies } = require('./theme-install'); + const installingKey = + logPrefix === 'themePreview' ? 'themePreview.installingDeps' : 'themeProd.installingDeps'; + console.log(`[reactpress] ${t(installingKey, { id: themeId })}`); + installThemeDependencies(themeDir, projectRoot); +} + +function resolveThemeBuildState(projectRoot, themeId) { + const themeDir = resolveThemeDirectory(projectRoot, themeId); + if (!themeDir || !fs.existsSync(path.join(themeDir, 'package.json'))) { + return null; + } + return { themeId, themeDir }; +} + +function readActiveThemeBuildState(projectRoot) { + const { activeTheme } = readActiveThemeManifest(projectRoot); + const state = resolveThemeBuildState(projectRoot, activeTheme); + if (!state) return null; + return { activeTheme, themeDir: state.themeDir }; +} + +/** @type {Promise} */ +let themeBuildChain = Promise.resolve(); + +function doBuildThemeSync( + projectRoot, + themeId, + { force = false, logPrefix = 'themeProd', distDir } = {}, +) { + const state = resolveThemeBuildState(projectRoot, themeId); + if (!state) { + const err = new Error(`Theme not found: ${themeId}`); + err.code = 'REACTPRESS_THEME_NOT_FOUND'; + throw err; + } + const { themeDir } = state; + const buildDistDir = + distDir || (logPrefix === 'themePreview' ? PREVIEW_DIST_DIR : '.next'); + + if (!force && hasUsableProductionBuild(themeDir, themeId, { distDir: buildDistDir })) { + if (logPrefix === 'themePreview') { + console.log(`[reactpress] ${t('themePreview.reusingBuild', { id: themeId })}`); + } else { + console.log(`[reactpress] ${t('themeProd.reusingBuild', { id: themeId })}`); + } + return { themeId, themeDir, skippedBuild: true }; + } + + syncThemeLaunchFilesFromTemplate(projectRoot, themeId, themeDir); + ensureThemeDependenciesInstalled(projectRoot, themeDir, themeId, logPrefix); + + const buildingKey = + logPrefix === 'themePreview' ? 'themePreview.building' : 'themeProd.building'; + console.log(`[reactpress] ${t(buildingKey, { id: themeId })}`); + runSync('pnpm', ['run', 'build'], { + cwd: themeDir, + stdio: ['ignore', 'inherit', 'inherit'], + env: resolveBuildNodeEnv({ + ...resolveProductionThemeEnv(projectRoot, themeDir), + NEXT_DIST_DIR: buildDistDir, + CI: '1', + ...(logPrefix === 'themeProd' ? { REACTPRESS_BUILD_ACTIVE: '1' } : {}), + ...(shouldHonorThemePreviewFrame() || logPrefix === 'themePreview' + ? { REACTPRESS_HONOR_PREVIEW: '1' } + : {}), + }), + }); + if (shouldHonorThemePreviewFrame() || logPrefix === 'themePreview') { + const { stripBakedFrameOptionsFromBuild } = require('./theme-preview-frame'); + stripBakedFrameOptionsFromBuild(themeDir, buildDistDir); + } + writeThemeBuildStamp(themeDir, themeId, { distDir: buildDistDir }); + return { themeId, themeDir, skippedBuild: false }; +} + +function enqueueThemeBuild(projectRoot, themeId, options = {}) { + const task = themeBuildChain.then(() => doBuildThemeSync(projectRoot, themeId, options)); + themeBuildChain = task.catch(() => {}); + return task; +} + +function buildTheme(projectRoot, themeId, options = {}) { + return doBuildThemeSync(projectRoot, themeId, options); +} + +function buildActiveTheme(projectRoot, { force = false } = {}) { + const { activeTheme } = readActiveThemeManifest(projectRoot); + const result = doBuildThemeSync(projectRoot, activeTheme, { force, logPrefix: 'themeProd' }); + return { activeTheme, themeDir: result.themeDir, skippedBuild: result.skippedBuild }; +} + +function scheduleBackgroundThemeBuilds(projectRoot, { excludeThemeId } = {}) { + if (process.env.REACTPRESS_SKIP_PREVIEW_BUILD === '1') return; + + const activeTheme = + excludeThemeId || readActiveThemeManifest(projectRoot).activeTheme; + const themeIds = listAvailableThemeIds(projectRoot).filter((id) => id !== activeTheme); + if (themeIds.length === 0) return; + + console.log( + `[reactpress] ${t('themePreview.backgroundBuildScheduled', { count: themeIds.length })}`, + ); + + setImmediate(() => { + void warmupAllPreviewThemeBuilds(projectRoot, { themeIds }).catch(() => {}); + }); +} + +/** + * Pre-build `.next-preview` for catalog themes so admin preview switches stay under ~10s. + * Runs builds in parallel (local/desktop dev only — awaited before the ready banner). + */ +async function warmupAllPreviewThemeBuilds( + projectRoot, + { themeIds, concurrency = 2, excludeThemeId } = {}, +) { + if (process.env.REACTPRESS_SKIP_PREVIEW_BUILD === '1') return { built: 0, skipped: 0 }; + + const activeTheme = excludeThemeId || readActiveThemeManifest(projectRoot).activeTheme; + const ids = (themeIds || listAvailableThemeIds(projectRoot)).filter((id) => id !== activeTheme); + if (ids.length === 0) return { built: 0, skipped: 0 }; + + const { mapWithConcurrency } = require('./theme-warmup'); + const limit = Math.max( + 1, + parseInt(process.env.REACTPRESS_PREVIEW_BUILD_CONCURRENCY || String(concurrency), 10) || + concurrency, + ); + + console.log(`[reactpress] ${t('themePreview.warmingAll', { count: ids.length })}`); + + let built = 0; + let skipped = 0; + + await mapWithConcurrency(ids, limit, async (themeId) => { + const state = resolveThemeBuildState(projectRoot, themeId); + if (!state) return; + if (hasUsableProductionBuild(state.themeDir, themeId, { distDir: PREVIEW_DIST_DIR })) { + skipped += 1; + return; + } + try { + const result = await enqueueThemeBuild(projectRoot, themeId, { + logPrefix: 'themePreview', + distDir: PREVIEW_DIST_DIR, + }); + if (result.skippedBuild) skipped += 1; + else { + built += 1; + console.log(`[reactpress] ${t('themePreview.buildDone', { id: themeId })}`); + } + } catch (err) { + console.warn( + `[reactpress] ${t('themePreview.buildFailed', { + id: themeId, + message: err.message || err, + })}`, + ); + } + }); + + if (skipped > 0) { + console.log(`[reactpress] ${t('themePreview.warmingAllSkipped', { count: skipped })}`); + } + return { built, skipped }; +} + +/** + * Rebuild active theme and restart PM2 visitor process (production deploy). + */ +async function restartProductionVisitorClient(projectRoot = resolveProjectRoot()) { + const { activeTheme, themeDir } = buildActiveTheme(projectRoot); + const bin = resolveThemeClientBin(projectRoot, themeDir); + const env = resolveProductionThemeEnv(projectRoot, themeDir); + + spawnSync('pm2', ['delete', 'reactpress-client'], { stdio: 'ignore' }); + spawnSync('pm2', ['delete', '@fecommunity/reactpress-template-hello-world'], { + stdio: 'ignore', + }); + + console.log(`[reactpress] ${t('themeProd.restarting', { id: activeTheme })}`); + await runNodeScript(bin, ['--pm2'], { cwd: projectRoot, env }); + console.log(`[reactpress] ${t('themeProd.restarted', { id: activeTheme })}`); +} + +module.exports = { + PREVIEW_DIST_DIR, + buildActiveTheme, + buildTheme, + enqueueThemeBuild, + scheduleBackgroundThemeBuilds, + warmupAllPreviewThemeBuilds, + restartProductionVisitorClient, + resolveProductionThemeEnv, + resolvePreviewThemeEnv, + ensureThemeDependenciesInstalled, + hasUsableProductionBuild, + readActiveThemeBuildState, + resolveThemeBuildState, + writeThemeBuildStamp, +}; diff --git a/cli/src/lib/theme-registry.ts b/cli/src/lib/theme-registry.ts new file mode 100644 index 00000000..1cb67f5f --- /dev/null +++ b/cli/src/lib/theme-registry.ts @@ -0,0 +1,151 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); + +const { getCliPackageRoot } = require('./paths'); +const { + THEMES_PACKAGE_REL, + themesRoot, +} = require('./theme-paths'); +const { + readThemesRegistryMeta, + readThemesPackageMeta, + readNpmThemeSources, + readThemeSources, + readNpmEntryFromPackageDir, + normalizeNpmCatalogEntry, + isValidNpmCatalogEntry, + validateLocalThemes, + validateNpmThemes, + validateBundledThemes, + validateCatalogThemes, +} = require('./theme-sources'); + +/** Official npm spec for the theme-starter package. */ +const OFFICIAL_THEME_STARTER_SPEC = '@fecommunity/reactpress-theme-starter@1.0.0-beta.0'; +const OFFICIAL_THEME_STARTER_ID = 'reactpress-theme-starter'; +/** Catalog anchor directory under themes/ (metadata in package.json). */ +const OFFICIAL_THEME_STARTER_DIR = 'theme-starter'; + +function readJsonFile(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch { + return null; + } +} + +function readLegacyCatalogFile(projectRoot, filePath) { + if (!fs.existsSync(filePath)) return null; + const raw = readJsonFile(filePath); + if (!raw) return null; + const themes = (Array.isArray(raw.themes) ? raw.themes : []) + .map(normalizeNpmCatalogEntry) + .filter(Boolean); + if (!themes.length) return null; + return { + version: typeof raw.version === 'number' ? raw.version : 1, + themes, + source: path.relative(path.resolve(projectRoot), filePath) || filePath, + }; +} + +function readCatalogFromRegistry(projectRoot) { + const npmSources = readNpmThemeSources(projectRoot); + if (npmSources.length) { + return { + version: 1, + themes: npmSources.map(({ kind, ...entry }) => entry), + source: THEMES_PACKAGE_REL, + }; + } + return null; +} + +/** Aggregate npm catalog from themes/package.json, then CLI template fallback. */ +function readThemeCatalog(projectRoot) { + const fromRegistry = readCatalogFromRegistry(projectRoot); + if (fromRegistry) return fromRegistry; + + const legacyCatalog = path.join(themesRoot(projectRoot), 'catalog.json'); + const legacy = readLegacyCatalogFile(projectRoot, legacyCatalog); + if (legacy) return legacy; + + const templatePath = path.join(getCliPackageRoot(), 'templates', 'theme-catalog.json'); + const fromTemplate = readLegacyCatalogFile(projectRoot, templatePath); + if (fromTemplate) return fromTemplate; + + return { version: 1, themes: [], source: null }; +} + +function findCatalogTheme(projectRoot, idOrSpec) { + const needle = String(idOrSpec || '').trim(); + if (!needle) return null; + const { themes } = readThemeCatalog(projectRoot); + return ( + themes.find((entry) => entry.id === needle || entry.npm === needle) ?? + themes.find((entry) => entry.npm.startsWith(needle) || needle.startsWith(entry.id)) ?? + null + ); +} + +function resolveCatalogInstallSpec(projectRoot, input) { + const trimmed = String(input || '').trim(); + if (!trimmed) return null; + if (trimmed === 'reactpress-theme-starter' || trimmed === OFFICIAL_THEME_STARTER_ID) { + const fromCatalog = findCatalogTheme(projectRoot, OFFICIAL_THEME_STARTER_ID); + if (fromCatalog?.npm) return fromCatalog.npm; + return OFFICIAL_THEME_STARTER_SPEC; + } + const fromCatalog = findCatalogTheme(projectRoot, trimmed); + if (fromCatalog?.npm) return fromCatalog.npm; + return trimmed; +} + +/** Map npm catalog metadata to a minimal ThemeManifest-shaped object. */ +function catalogEntryToManifest(entry) { + if (!entry) return null; + return { + id: entry.id, + name: entry.name, + version: entry.version, + description: entry.description, + author: entry.author, + themeUri: entry.themeUri, + previewUrl: entry.previewUrl, + cover: entry.cover, + tags: entry.tags, + requires: entry.requires, + }; +} + +/** Build aggregated catalog JSON for CLI template sync. */ +function buildAggregatedCatalog(projectRoot) { + const { themes } = readCatalogFromRegistry(projectRoot) ?? { themes: [] }; + return { + version: 1, + themes: themes.map(({ dir, ...entry }) => entry), + }; +} + +module.exports = { + OFFICIAL_THEME_STARTER_SPEC, + OFFICIAL_THEME_STARTER_ID, + OFFICIAL_THEME_STARTER_DIR, + isValidNpmCatalogEntry, + isValidCatalogEntry: isValidNpmCatalogEntry, + normalizeCatalogEntry: normalizeNpmCatalogEntry, + readThemesRegistryMeta, + readThemesPackageMeta, + readPackageCatalogEntry: readNpmEntryFromPackageDir, + readThemeSources, + readThemeCatalog, + findCatalogTheme, + resolveCatalogInstallSpec, + catalogEntryToManifest, + validateLocalThemes, + validateNpmThemes, + validateBundledThemes, + validateCatalogThemes, + buildAggregatedCatalog, +}; diff --git a/cli/src/lib/theme-runtime.ts b/cli/src/lib/theme-runtime.ts new file mode 100644 index 00000000..cf054603 --- /dev/null +++ b/cli/src/lib/theme-runtime.ts @@ -0,0 +1,377 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); + +const { DEV_PORTS, BLOCKED_THEME_DEV_PORTS } = require('./ports'); +const { + REACTPRESS_DIR, + THEME_RUNTIME_REL, + LEGACY_THEMES_RUNTIME_REL, + ACTIVE_THEME_MANIFEST_REL, + PREVIEW_THEME_MANIFEST_REL, + THEMES_RESERVED_SUBDIRS, + THEMES_LEGACY_STARTER_SUBDIRS, + THEME_ID_RE, + DEFAULT_ACTIVE_THEME, +} = require('./theme-paths'); + +const MANIFEST_REL = ACTIVE_THEME_MANIFEST_REL; +const PREVIEW_MANIFEST_REL = PREVIEW_THEME_MANIFEST_REL; +const DEFAULT_PREVIEW_THEME_PORT = DEV_PORTS.THEME_PREVIEW; +const BLOCKED_DEV_PORTS = BLOCKED_THEME_DEV_PORTS; + +/** Desktop dev stores manifests under `.reactpress/desktop-dev-site/` (embedded SQLite site root). */ +function themeWorkspaceRoot(projectRoot) { + const site = process.env.REACTPRESS_DESKTOP_SITE_ROOT?.trim(); + return site ? path.resolve(site) : path.resolve(projectRoot); +} + +function resolveMonorepoRoot(projectRoot) { + const fromEnv = process.env.REACTPRESS_MONOREPO_ROOT?.trim(); + if (fromEnv) return path.resolve(fromEnv); + let dir = path.resolve(projectRoot); + for (let depth = 0; depth < 10; depth += 1) { + if (fs.existsSync(path.join(dir, 'cli', 'lib', 'theme-registry.js'))) { + return dir; + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return path.resolve(projectRoot); +} + +function isValidThemeId(id) { + return typeof id === 'string' && THEME_ID_RE.test(id) && id.length <= 64; +} + +function isUnderDir(child, parent) { + const rel = path.relative(path.resolve(parent), path.resolve(child)); + return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel)); +} + +function themeRoots(projectRoot) { + const root = themeWorkspaceRoot(projectRoot); + const themes = path.join(root, 'themes'); + return { + themes, + runtime: path.join(root, THEME_RUNTIME_REL), + legacyThemesRuntime: path.join(root, LEGACY_THEMES_RUNTIME_REL), + legacyStarter: THEMES_LEGACY_STARTER_SUBDIRS.map((name) => path.join(themes, name)), + legacyBundled: path.join(root, 'templates'), + }; +} + +function isThemePackageAt(dir) { + return ( + fs.existsSync(path.join(dir, 'package.json')) || + fs.existsSync(path.join(dir, 'theme.json')) + ); +} + +/** `readdir` symlinks report as links, not directories — follow for desktop-dev-site theme seeds. */ +function isResolvableThemeDirEntry(entry, parentDir) { + if (!entry.isDirectory() && !entry.isSymbolicLink()) return false; + try { + return fs.statSync(path.join(parentDir, entry.name)).isDirectory(); + } catch { + return false; + } +} + +function isThemePackageDir(projectRoot, dir) { + if (!dir) return false; + const resolved = path.resolve(dir); + const { themes, runtime, legacyThemesRuntime, legacyStarter, legacyBundled } = + themeRoots(projectRoot); + + if (isUnderDir(resolved, runtime) && isThemePackageAt(resolved)) { + return true; + } + + if (isUnderDir(resolved, legacyThemesRuntime) && isThemePackageAt(resolved)) { + return true; + } + + if (isUnderDir(resolved, themes)) { + const rel = path.relative(themes, resolved); + const top = rel.split(path.sep)[0]; + if (top && !THEMES_RESERVED_SUBDIRS.includes(top) && isThemePackageAt(resolved)) { + return true; + } + } + + for (const base of [...legacyStarter, legacyBundled]) { + if (!fs.existsSync(base)) continue; + if (isUnderDir(resolved, base) && isThemePackageAt(resolved)) { + return true; + } + } + + return false; +} + +function isAllowedThemePort(port) { + const n = Number(port); + return Number.isInteger(n) && n >= 1024 && n <= 65535 && !BLOCKED_DEV_PORTS.has(n); +} + +function isAllowedThemeDirRel(themeDir) { + if (typeof themeDir !== 'string') return false; + if (themeDir.includes('..') || path.isAbsolute(themeDir)) return false; + + if (themeDir.startsWith(`${THEME_RUNTIME_REL}/`)) { + return true; + } + + if (themeDir.startsWith(`${LEGACY_THEMES_RUNTIME_REL}/`)) { + return true; + } + + if (themeDir.startsWith('themes/') && !themeDir.startsWith(`${LEGACY_THEMES_RUNTIME_REL}/`)) { + const rest = themeDir.slice('themes/'.length); + const top = rest.split('/')[0]; + if (top && !THEMES_RESERVED_SUBDIRS.includes(top)) { + return true; + } + } + + const legacyPrefixes = THEMES_LEGACY_STARTER_SUBDIRS.map((name) => `themes/${name}/`); + if (legacyPrefixes.some((prefix) => themeDir.startsWith(prefix))) { + return true; + } + + return themeDir.startsWith('templates/'); +} + +function readActiveThemeManifest(projectRoot) { + const manifestPath = path.join(themeWorkspaceRoot(projectRoot), MANIFEST_REL); + if (!fs.existsSync(manifestPath)) { + return { activeTheme: DEFAULT_ACTIVE_THEME }; + } + try { + const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + const id = + typeof raw.activeTheme === 'string' && isValidThemeId(raw.activeTheme) + ? raw.activeTheme + : DEFAULT_ACTIVE_THEME; + const themeDir = isAllowedThemeDirRel(raw.themeDir) ? raw.themeDir : undefined; + return { activeTheme: id, themeDir, updatedAt: raw.updatedAt }; + } catch { + return { activeTheme: DEFAULT_ACTIVE_THEME }; + } +} + +function resolveThemeDirectory(projectRoot, themeId) { + if (!isValidThemeId(themeId)) return null; + + const root = themeWorkspaceRoot(projectRoot); + const runtime = path.join(root, THEME_RUNTIME_REL, themeId); + if (isThemePackageAt(runtime)) return runtime; + + const legacyRuntime = path.join(root, LEGACY_THEMES_RUNTIME_REL, themeId); + if (isThemePackageAt(legacyRuntime)) return legacyRuntime; + + const template = path.join(root, 'themes', themeId); + if (isThemePackageAt(template) && !THEMES_RESERVED_SUBDIRS.includes(themeId)) { + return template; + } + + for (const legacyStarterName of THEMES_LEGACY_STARTER_SUBDIRS) { + const legacyStarter = path.join(root, 'themes', legacyStarterName, themeId); + if (isThemePackageAt(legacyStarter)) return legacyStarter; + } + + const legacyBundled = path.join(root, 'templates', themeId); + if (isThemePackageAt(legacyBundled)) return legacyBundled; + + const mono = resolveMonorepoRoot(projectRoot); + if (mono !== root) { + const monoRuntime = path.join(mono, THEME_RUNTIME_REL, themeId); + if (isThemePackageAt(monoRuntime)) return monoRuntime; + const monoTheme = path.join(mono, 'themes', themeId); + if (isThemePackageAt(monoTheme) && !THEMES_RESERVED_SUBDIRS.includes(themeId)) { + return monoTheme; + } + } + + return null; +} + +function readManifestSignatureFromPath(projectRoot, manifestRel) { + const root = themeWorkspaceRoot(projectRoot); + const manifestPath = path.join(root, manifestRel); + try { + if (!fs.existsSync(manifestPath)) return ''; + const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + const id = typeof raw.activeTheme === 'string' ? raw.activeTheme : ''; + if (!isValidThemeId(id)) return ''; + const themeDir = resolveThemeDirectory(projectRoot, id); + if (!themeDir) return ''; + const rel = path.relative(root, themeDir); + return `${id}:${rel}`; + } catch { + return ''; + } +} + +function readManifestSignature(projectRoot) { + return readManifestSignatureFromPath(projectRoot, MANIFEST_REL); +} + +function readPreviewManifestSignature(projectRoot) { + const root = themeWorkspaceRoot(projectRoot); + const manifestPath = path.join(root, PREVIEW_MANIFEST_REL); + try { + if (!fs.existsSync(manifestPath)) return ''; + const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + const id = typeof raw.activeTheme === 'string' ? raw.activeTheme : ''; + if (!isValidThemeId(id)) return ''; + const themeDir = resolveThemeDirectory(projectRoot, id); + if (!themeDir) return ''; + const rel = path.relative(root, themeDir); + const stamp = typeof raw.updatedAt === 'string' ? raw.updatedAt : ''; + return `${id}:${rel}:${stamp}`; + } catch { + return ''; + } +} + +function readPreviewThemeManifest(projectRoot) { + const root = themeWorkspaceRoot(projectRoot); + const manifestPath = path.join(root, PREVIEW_MANIFEST_REL); + if (!fs.existsSync(manifestPath)) { + return null; + } + try { + const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + const id = + typeof raw.activeTheme === 'string' && isValidThemeId(raw.activeTheme) + ? raw.activeTheme + : null; + if (!id) return null; + const themeDir = resolveThemeDirectory(projectRoot, id); + return { activeTheme: id, themeDir: themeDir ? path.relative(root, themeDir) : null }; + } catch { + return null; + } +} + +function getPreviewThemePort() { + const fromEnv = parseInt(process.env.REACTPRESS_PREVIEW_PORT || '', 10); + if (Number.isInteger(fromEnv) && isAllowedThemePort(fromEnv)) { + return String(fromEnv); + } + return String(DEFAULT_PREVIEW_THEME_PORT); +} + +function hasResolvableActiveTheme(projectRoot) { + if (!hasThemePackages(projectRoot)) return false; + const { activeTheme } = readActiveThemeManifest(projectRoot); + const themeDir = resolveThemeDirectory(projectRoot, activeTheme); + return Boolean(themeDir && isThemePackageDir(projectRoot, themeDir)); +} + +/** Installed / bundled theme ids (active-theme.json entries may point into runtime/). */ +function listAvailableThemeIds(projectRoot) { + const ids = new Set(); + const { themes, runtime, legacyThemesRuntime, legacyStarter, legacyBundled } = + themeRoots(projectRoot); + + for (const dir of [runtime, legacyThemesRuntime]) { + if (!fs.existsSync(dir)) continue; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (!isResolvableThemeDirEntry(entry, dir) || !isValidThemeId(entry.name)) continue; + if (isThemePackageAt(path.join(dir, entry.name))) ids.add(entry.name); + } + } + + if (fs.existsSync(themes)) { + for (const entry of fs.readdirSync(themes, { withFileTypes: true })) { + if (!isResolvableThemeDirEntry(entry, themes)) continue; + if (THEMES_RESERVED_SUBDIRS.includes(entry.name)) continue; + if (!isValidThemeId(entry.name)) continue; + if (isThemePackageAt(path.join(themes, entry.name))) ids.add(entry.name); + } + } + + for (const base of [...legacyStarter, legacyBundled]) { + if (!fs.existsSync(base)) continue; + for (const entry of fs.readdirSync(base, { withFileTypes: true })) { + if (!isResolvableThemeDirEntry(entry, base) || !isValidThemeId(entry.name)) continue; + if (isThemePackageAt(path.join(base, entry.name))) ids.add(entry.name); + } + } + + return [...ids].sort(); +} + +function hasThemePackages(projectRoot) { + const { themes, runtime, legacyThemesRuntime, legacyStarter, legacyBundled } = + themeRoots(projectRoot); + + for (const dir of [runtime, legacyThemesRuntime]) { + if (!fs.existsSync(dir)) continue; + if ( + fs + .readdirSync(dir, { withFileTypes: true }) + .some((entry) => isResolvableThemeDirEntry(entry, dir)) + ) { + return true; + } + } + + if (fs.existsSync(themes)) { + if ( + fs + .readdirSync(themes, { withFileTypes: true }) + .some( + (entry) => + isResolvableThemeDirEntry(entry, themes) && + !THEMES_RESERVED_SUBDIRS.includes(entry.name), + ) + ) { + return true; + } + } + + for (const dir of [...legacyStarter, legacyBundled]) { + if (!fs.existsSync(dir)) continue; + if ( + fs + .readdirSync(dir, { withFileTypes: true }) + .some((entry) => isResolvableThemeDirEntry(entry, dir)) + ) { + return true; + } + } + + return false; +} + +module.exports = { + MANIFEST_REL, + PREVIEW_MANIFEST_REL, + DEFAULT_PREVIEW_THEME_PORT, + THEME_ID_RE, + THEME_RUNTIME_REL, + LEGACY_THEMES_RUNTIME_REL, + THEMES_RESERVED_SUBDIRS, + THEMES_LEGACY_STARTER_SUBDIRS, + BLOCKED_DEV_PORTS, + isValidThemeId, + isThemePackageDir, + isAllowedThemePort, + isAllowedThemeDirRel, + readActiveThemeManifest, + resolveThemeDirectory, + readManifestSignature, + readPreviewManifestSignature, + readPreviewThemeManifest, + getPreviewThemePort, + hasThemePackages, + hasResolvableActiveTheme, + listAvailableThemeIds, + themeRoots, + themeWorkspaceRoot, +}; diff --git a/cli/src/lib/theme-sources.ts b/cli/src/lib/theme-sources.ts new file mode 100644 index 00000000..61422f4b --- /dev/null +++ b/cli/src/lib/theme-sources.ts @@ -0,0 +1,377 @@ +// @ts-nocheck +/** + * ReactPress theme sources — unified model. + * + * TWO SOURCES (how a theme is installed into `.reactpress/runtime/{id}/`): + * local — copy from `themes/{id}/` (must contain `theme.json`) + * npm — `npm pack` a package spec, then copy into runtime + * + * NPM REGISTRY SPEC (canonical): + * themes/{anchor}/package.json → dependencies + reactpress.theme (see npm-catalog.schema.json) + * themes/package.json → reactpress.npm: ["{anchor}", …] + * + * THREE LAYERS: + * themes/ registry — what is available to install + * .reactpress/runtime/ materialized — installed copies the CLI runs + * DB + *.json activation — which theme is active / previewing + * + * Legacy: reactpress.bundled / catalog keys; inline objects in reactpress.npm array. + */ +const fs = require('fs'); +const path = require('path'); + +const { THEMES_PACKAGE_REL, themesRoot } = require('./theme-paths'); + +function readJsonFile(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch { + return null; + } +} + +function isNonEmptyString(value) { + return typeof value === 'string' && value.trim().length > 0; +} + +function formatNpmInstallSpec(name, version) { + return `${name.trim()}@${version.trim()}`; +} + +/** Split `package@version` into structured dependency (supports scoped packages). */ +function parseNpmSpecToDependency(spec) { + const trimmed = String(spec || '').trim(); + const atIndex = trimmed.lastIndexOf('@'); + if (atIndex <= 0) return null; + const name = trimmed.slice(0, atIndex).trim(); + const version = trimmed.slice(atIndex + 1).trim(); + if (!name || !version) return null; + return { name, version }; +} + +function readPackageDependencies(raw) { + const deps = raw?.dependencies; + if (!deps || typeof deps !== 'object') return null; + for (const [name, version] of Object.entries(deps)) { + if (isNonEmptyString(name) && isNonEmptyString(version)) { + return { name: name.trim(), version: String(version).trim() }; + } + } + return null; +} + +function readThemeDependency(theme, raw) { + const fromDeps = raw ? readPackageDependencies(raw) : null; + if (fromDeps) return fromDeps; + + if (theme && typeof theme === 'object') { + const dep = theme.dependency; + if (dep && typeof dep === 'object' && isNonEmptyString(dep.name) && isNonEmptyString(dep.version)) { + return { name: dep.name.trim(), version: dep.version.trim() }; + } + if (isNonEmptyString(theme.npm)) { + return parseNpmSpecToDependency(theme.npm); + } + } + + const pkgName = typeof raw?.name === 'string' ? raw.name : undefined; + const pkgVersion = typeof raw?.version === 'string' ? raw.version : undefined; + if (isNonEmptyString(pkgName) && isNonEmptyString(pkgVersion)) { + return { name: pkgName.trim(), version: pkgVersion.trim() }; + } + + return null; +} + +function resolveThemeNpmSpec(theme, raw) { + const dependency = readThemeDependency(theme, raw); + return dependency ? formatNpmInstallSpec(dependency.name, dependency.version) : undefined; +} + +function isValidNpmCatalogEntry(entry) { + return ( + entry && + isNonEmptyString(entry.id) && + isNonEmptyString(entry.name) && + (isNonEmptyString(entry.npm) || + (entry.dependency && + typeof entry.dependency === 'object' && + isNonEmptyString(entry.dependency.name) && + isNonEmptyString(entry.dependency.version))) + ); +} + +function normalizeNpmCatalogEntry(entry) { + if (!entry || !isNonEmptyString(entry.id) || !isNonEmptyString(entry.name)) return null; + + let dependency = + entry.dependency && + typeof entry.dependency === 'object' && + isNonEmptyString(entry.dependency.name) && + isNonEmptyString(entry.dependency.version) + ? { name: entry.dependency.name.trim(), version: entry.dependency.version.trim() } + : undefined; + + let npmSpec = isNonEmptyString(entry.npm) ? entry.npm.trim() : undefined; + if (dependency) { + npmSpec = formatNpmInstallSpec(dependency.name, dependency.version); + } else if (npmSpec) { + dependency = parseNpmSpecToDependency(npmSpec) ?? undefined; + } + + if (!npmSpec) return null; + + return { + id: entry.id.trim(), + name: entry.name, + version: typeof entry.version === 'string' ? entry.version : '0.0.0', + description: entry.description, + author: entry.author, + authorUri: entry.authorUri, + themeUri: entry.themeUri, + previewUrl: entry.previewUrl, + cover: entry.cover, + tags: Array.isArray(entry.tags) ? entry.tags : undefined, + dependency, + npm: npmSpec, + featured: entry.featured === true, + requires: entry.requires, + dir: typeof entry.dir === 'string' ? entry.dir.trim() : undefined, + }; +} + +/** Read `themes/package.json` registry lists (local ids + npm catalog refs). */ +function readThemesRegistryMeta(projectRoot) { + const pkgPath = path.join(path.resolve(projectRoot), THEMES_PACKAGE_REL); + if (!fs.existsSync(pkgPath)) { + return { local: [], npm: [] }; + } + + const raw = readJsonFile(pkgPath); + if (!raw?.reactpress || typeof raw.reactpress !== 'object') { + return { local: [], npm: [] }; + } + + const reactpress = raw.reactpress; + const localSource = reactpress.local ?? reactpress.bundled; + const npmSource = reactpress.npm ?? reactpress.catalog; + + const local = Array.isArray(localSource) + ? localSource.filter((id) => isNonEmptyString(id)).map((id) => id.trim()) + : []; + + const npm = Array.isArray(npmSource) ? npmSource : []; + + return { local, npm }; +} + +/** @deprecated Use readThemesRegistryMeta — kept for existing imports. */ +function readThemesPackageMeta(projectRoot) { + const { local, npm } = readThemesRegistryMeta(projectRoot); + const catalog = npm + .map((item) => { + if (isNonEmptyString(item)) return item.trim(); + if (item && typeof item === 'object' && isNonEmptyString(item.id)) return item.id.trim(); + return null; + }) + .filter(Boolean); + return { bundled: local, catalog, local, npm }; +} + +function readNpmEntryFromPackageDir(catalogDir, pkgPath) { + const raw = readJsonFile(pkgPath); + if (!raw) return null; + + const theme = + raw.reactpress && typeof raw.reactpress === 'object' && raw.reactpress.theme + ? raw.reactpress.theme + : null; + if (!theme || !isNonEmptyString(theme.id)) return null; + + const pkgVersion = typeof raw.version === 'string' ? raw.version : '0.0.0'; + const dependency = readThemeDependency(theme, raw); + const npmSpec = dependency ? formatNpmInstallSpec(dependency.name, dependency.version) : undefined; + if (!npmSpec) return null; + + return normalizeNpmCatalogEntry({ + id: theme.id, + dir: catalogDir, + name: + isNonEmptyString(theme.name) ? theme.name : isNonEmptyString(raw.description) ? raw.description : theme.id, + version: isNonEmptyString(theme.version) ? theme.version : dependency.version ?? pkgVersion, + description: + isNonEmptyString(theme.description) ? theme.description : isNonEmptyString(raw.description) ? raw.description : undefined, + author: isNonEmptyString(theme.author) ? theme.author : raw.author, + authorUri: isNonEmptyString(theme.authorUri) ? theme.authorUri : undefined, + themeUri: + isNonEmptyString(theme.themeUri) ? theme.themeUri : isNonEmptyString(raw.homepage) ? raw.homepage : undefined, + previewUrl: isNonEmptyString(theme.previewUrl) ? theme.previewUrl.trim() : undefined, + cover: isNonEmptyString(theme.cover) ? theme.cover.trim() : undefined, + tags: Array.isArray(theme.tags) ? theme.tags : undefined, + dependency, + npm: npmSpec, + featured: theme.featured === true, + requires: isNonEmptyString(theme.requires) ? theme.requires : undefined, + }); +} + +function readInlineNpmEntry(item) { + if (!item || typeof item !== 'object') return null; + const dependency = + readPackageDependencies(item) ?? + (item.dependency && + typeof item.dependency === 'object' && + isNonEmptyString(item.dependency.name) && + isNonEmptyString(item.dependency.version) + ? { name: item.dependency.name.trim(), version: item.dependency.version.trim() } + : isNonEmptyString(item.npm) + ? parseNpmSpecToDependency(item.npm) + : undefined); + if (!dependency && !isNonEmptyString(item.npm)) return null; + return normalizeNpmCatalogEntry({ + id: item.id, + name: item.name, + version: item.version, + description: item.description, + author: item.author, + authorUri: item.authorUri, + themeUri: item.themeUri, + previewUrl: item.previewUrl, + cover: item.cover, + tags: item.tags, + dependency, + npm: dependency ? formatNpmInstallSpec(dependency.name, dependency.version) : item.npm, + featured: item.featured, + requires: item.requires, + }); +} + +/** Local theme ids registered in themes/package.json with theme.json on disk. */ +function readLocalThemeSources(projectRoot) { + const root = path.resolve(projectRoot); + const { local } = readThemesRegistryMeta(root); + const templates = themesRoot(root); + const sources = []; + + for (const id of local) { + const themeJson = path.join(templates, id, 'theme.json'); + if (!fs.existsSync(themeJson)) continue; + const manifest = readJsonFile(themeJson); + if (!manifest?.id) continue; + sources.push({ + kind: 'local', + id: manifest.id, + dir: id, + manifest, + }); + } + + return sources; +} + +/** npm catalog entries — anchor dirs (themes/{anchor}/package.json) are canonical. */ +function readNpmThemeSources(projectRoot) { + const root = path.resolve(projectRoot); + const { npm } = readThemesRegistryMeta(root); + const templates = themesRoot(root); + const byId = new Map(); + + for (const item of npm) { + if (isNonEmptyString(item)) { + const dir = item.trim(); + const entry = readNpmEntryFromPackageDir(dir, path.join(templates, dir, 'package.json')); + if (entry) byId.set(entry.id, { kind: 'npm', ...entry }); + continue; + } + + const inline = readInlineNpmEntry(item); + if (inline) byId.set(inline.id, { kind: 'npm', ...inline }); + } + + return [...byId.values()]; +} + +/** Combined registry view used by CLI and server. */ +function readThemeSources(projectRoot) { + return { + local: readLocalThemeSources(projectRoot), + npm: readNpmThemeSources(projectRoot), + }; +} + +function validateLocalThemes(projectRoot) { + const root = path.resolve(projectRoot); + const { local } = readThemesRegistryMeta(root); + const missing = []; + const templates = themesRoot(root); + + for (const id of local) { + const themeJson = path.join(templates, id, 'theme.json'); + if (!fs.existsSync(themeJson)) { + missing.push(id); + } + } + return { local, missing }; +} + +/** @deprecated Use validateLocalThemes */ +function validateBundledThemes(projectRoot) { + const { local, missing } = validateLocalThemes(projectRoot); + return { bundled: local, missing }; +} + +function validateNpmThemes(projectRoot) { + const root = path.resolve(projectRoot); + const { npm } = readThemesRegistryMeta(root); + const missing = []; + const templates = themesRoot(root); + + for (const item of npm) { + if (isNonEmptyString(item)) { + const dir = item.trim(); + const pkgPath = path.join(templates, dir, 'package.json'); + const themeJson = path.join(templates, dir, 'theme.json'); + if (fs.existsSync(themeJson)) { + missing.push(`${dir}/ must not contain theme.json (npm anchor vs local theme)`); + } + const entry = readNpmEntryFromPackageDir(dir, pkgPath); + if (!entry) missing.push(`${dir}/package.json (dependencies + reactpress.theme)`); + continue; + } + if (!readInlineNpmEntry(item)) { + missing.push(`inline npm entry (id + dependencies or npm required): ${JSON.stringify(item)}`); + } + } + + return { npm, missing }; +} + +/** @deprecated Use validateNpmThemes */ +function validateCatalogThemes(projectRoot) { + const { npm, missing } = validateNpmThemes(projectRoot); + const catalog = npm + .map((item) => (isNonEmptyString(item) ? item.trim() : null)) + .filter(Boolean); + return { catalog, missing }; +} + +module.exports = { + readThemesRegistryMeta, + readThemesPackageMeta, + readLocalThemeSources, + readNpmThemeSources, + readThemeSources, + readNpmEntryFromPackageDir, + readInlineNpmEntry, + formatNpmInstallSpec, + parseNpmSpecToDependency, + readPackageDependencies, + readThemeDependency, + resolveThemeNpmSpec, + normalizeNpmCatalogEntry, + isValidNpmCatalogEntry, + validateLocalThemes, + validateNpmThemes, + validateBundledThemes, + validateCatalogThemes, +}; diff --git a/cli/src/lib/theme-warmup.ts b/cli/src/lib/theme-warmup.ts new file mode 100644 index 00000000..12a9442d --- /dev/null +++ b/cli/src/lib/theme-warmup.ts @@ -0,0 +1,172 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const http = require('http'); +const { readActiveThemeManifest, resolveThemeDirectory } = require('./theme-runtime'); +const { loadClientSiteUrl } = require('./http'); + +/** Placeholder for dynamic segments — only triggers page bundle compilation in dev. */ +const WARMUP_PARAM = '__reactpress_dev_warmup__'; + +function pageFileToRoute(pageFile) { + let route = String(pageFile) + .replace(/^pages\//, '') + .replace(/\.(tsx|ts|jsx|js)$/, ''); + if (route === 'index') return '/'; + route = route.replace(/\/index$/, ''); + route = route.replace(/\[([^\]]+)\]/g, WARMUP_PARAM); + return `/${route}`; +} + +/** Dynamic SSR routes need real API data — warmup only compiles static visitor pages. */ +function isWarmupSafeRoute(route) { + if (!route || typeof route !== 'string') return false; + if (route.includes(WARMUP_PARAM)) return false; + if (route.startsWith('/admin')) return false; + return true; +} + +function collectWarmupRoutes(themeDir) { + const themeJsonPath = path.join(themeDir, 'theme.json'); + const routes = new Set(['/']); + let fromThemeJson = false; + + if (fs.existsSync(path.join(themeDir, 'app'))) { + routes.add('/'); + } + + if (fs.existsSync(themeJsonPath)) { + try { + const manifest = JSON.parse(fs.readFileSync(themeJsonPath, 'utf8')); + const templates = manifest?.reactpress?.templates; + if (templates && typeof templates === 'object') { + fromThemeJson = true; + for (const file of Object.values(templates)) { + if (typeof file !== 'string') continue; + const route = pageFileToRoute(file); + if (isWarmupSafeRoute(route)) routes.add(route); + } + } + } catch { + // fall through to pages scan + } + } + + if (!fromThemeJson) { + const pagesDir = path.join(themeDir, 'pages'); + if (fs.existsSync(pagesDir)) { + walkPages(pagesDir, pagesDir).forEach((file) => { + const rel = path.relative(themeDir, file); + if (rel.startsWith(`pages${path.sep}admin${path.sep}`) || rel.includes(`${path.sep}admin${path.sep}`)) { + return; + } + const route = pageFileToRoute(rel); + if (isWarmupSafeRoute(route)) routes.add(route); + }); + } + } + + routes.add('/404'); + return [...routes].filter(isWarmupSafeRoute); +} + +function walkPages(pagesDir, currentDir, files = []) { + for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) { + const fullPath = path.join(currentDir, entry.name); + if (entry.isDirectory()) { + if (entry.name.startsWith('_') || entry.name === 'api' || entry.name === 'admin') continue; + walkPages(pagesDir, fullPath, files); + continue; + } + if (/\.(tsx|ts|jsx|js)$/.test(entry.name)) { + if (entry.name.startsWith('_')) continue; + files.push(fullPath); + } + } + return files; +} + +function fetchRoute(baseUrl, routePath) { + return new Promise((resolve) => { + const normalizedBase = String(baseUrl || 'http://127.0.0.1:3001').replace(/\/$/, ''); + const url = `${normalizedBase}${routePath.startsWith('/') ? routePath : `/${routePath}`}`; + const req = http.get(url, { timeout: 120_000 }, (res) => { + res.resume(); + resolve(res.statusCode >= 200 && res.statusCode < 500); + }); + req.on('error', () => resolve(false)); + req.on('timeout', () => { + req.destroy(); + resolve(false); + }); + }); +} + +async function mapWithConcurrency(items, concurrency, fn) { + if (!items.length) return []; + const limit = Math.max(1, Math.min(concurrency, items.length)); + const results = new Array(items.length); + let index = 0; + + async function worker() { + while (index < items.length) { + const i = index; + index += 1; + results[i] = await fn(items[i], i); + } + } + + await Promise.all(Array.from({ length: limit }, () => worker())); + return results; +} + +/** + * SSR-hit theme routes on the internal dev port so Next.js compiles page chunks + * before the browser performs client-side navigation (avoids webpack module mismatch). + */ +async function warmupThemeDevRoutes(projectRoot) { + const { activeTheme } = readActiveThemeManifest(projectRoot); + const themeDir = resolveThemeDirectory(projectRoot, activeTheme); + if (!themeDir) return { ok: false, routes: [] }; + + const baseUrl = loadClientSiteUrl(projectRoot); + const routes = collectWarmupRoutes(themeDir); + const concurrency = Math.max( + 1, + parseInt(process.env.REACTPRESS_THEME_WARMUP_CONCURRENCY || '6', 10) || 6, + ); + await mapWithConcurrency(routes, concurrency, (route) => fetchRoute(baseUrl, route)); + return { ok: true, routes, themeId: activeTheme }; +} + +function shouldBlockOnThemeWarmup() { + return process.env.REACTPRESS_THEME_WARMUP === '1'; +} + +/** Fire-and-forget SSR compile — off by default (saves ~10–20s Next compile after banner). */ +function warmupThemeDevRoutesInBackground(projectRoot) { + if (process.env.REACTPRESS_SKIP_THEME_WARMUP !== '0') return; + if (process.env.REACTPRESS_THEME_WARMUP !== '1') return; + warmupThemeDevRoutes(projectRoot).catch(() => { + // non-fatal — first browser navigation will compile anyway + }); +} + +/** SSR-hit homepage so first visitor load after theme switch is fast. */ +async function warmupThemeHomepage(projectRoot, baseUrl) { + const url = (baseUrl || loadClientSiteUrl(projectRoot)).replace(/\/$/, ''); + await fetchRoute(url, '/'); + return { ok: true }; +} + +module.exports = { + WARMUP_PARAM, + pageFileToRoute, + isWarmupSafeRoute, + collectWarmupRoutes, + mapWithConcurrency, + shouldBlockOnThemeWarmup, + warmupThemeDevRoutes, + warmupThemeDevRoutesInBackground, + warmupThemeHomepage, +}; diff --git a/cli/src/lib/toolkit-build.ts b/cli/src/lib/toolkit-build.ts new file mode 100644 index 00000000..35b8b334 --- /dev/null +++ b/cli/src/lib/toolkit-build.ts @@ -0,0 +1,54 @@ +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); + +const SOURCE_EXT = /\.(ts|tsx|js|json)$/; + +function newestSourceMtime(dir, depth = 0) { + if (!fs.existsSync(dir)) return 0; + let max = 0; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name === 'dist') continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (depth < 12) max = Math.max(max, newestSourceMtime(full, depth + 1)); + continue; + } + if (!SOURCE_EXT.test(entry.name)) continue; + max = Math.max(max, fs.statSync(full).mtimeMs); + } + return max; +} + +/** + * Whether `pnpm run build` in toolkit/ is needed before dev. + * Skips when dist is newer than all toolkit/src sources (unless forced). + */ +function shouldBuildToolkit(projectRoot) { + if (process.env.REACTPRESS_FORCE_TOOLKIT_BUILD === '1') return true; + if (process.env.REACTPRESS_SKIP_TOOLKIT_BUILD === '1') return false; + + const toolkitDir = path.join(path.resolve(projectRoot), 'toolkit'); + const distEntry = path.join(toolkitDir, 'dist', 'index.js'); + if (!fs.existsSync(distEntry)) return true; + + const srcDir = path.join(toolkitDir, 'src'); + if (!fs.existsSync(srcDir)) return false; + + const distMtime = fs.statSync(distEntry).mtimeMs; + const localesDir = path.join(toolkitDir, 'src', 'config', 'locales'); + const localesDist = path.join(toolkitDir, 'dist', 'config', 'locales'); + if (fs.existsSync(localesDir)) { + const localesMtime = newestSourceMtime(localesDir); + if (!fs.existsSync(localesDist) || localesMtime > fs.statSync(localesDist).mtimeMs) { + return true; + } + } + + return newestSourceMtime(srcDir) > distMtime; +} + +module.exports = { + shouldBuildToolkit, + newestSourceMtime, +}; diff --git a/cli/src/types/config.ts b/cli/src/types/config.ts new file mode 100644 index 00000000..243fe3b2 --- /dev/null +++ b/cli/src/types/config.ts @@ -0,0 +1,64 @@ +/** ReactPress 4.x 数据库模式 */ +export type DatabaseMode = 'embedded-docker' | 'external' | 'embedded-sqlite'; + +export type DatabaseType = 'mysql' | 'sqlite'; + +export interface ReactPressConfig { + version: number; + database: { + mode: DatabaseMode; + containerName?: string; + host?: string; + port?: number; + user?: string; + password?: string; + database?: string; + /** SQLite 文件路径(相对项目根或绝对路径) */ + sqlitePath?: string; + }; + server: { + port: number; + clientUrl?: string; + serverUrl?: string; + apiPrefix?: string; + siteUrl?: string; + }; +} + +export interface EnvMap { + [key: string]: string; +} + +export interface MysqlCredentials { + host: string; + port: number; + user: string; + password: string; + database: string; +} + +export interface SqliteCredentials { + database: string; +} + +export interface DatabaseProfile { + type: DatabaseType; + mode: DatabaseMode; + mysql?: MysqlCredentials; + sqlite?: SqliteCredentials; +} + +export interface DatabaseEnsureResult { + ok: boolean; + message?: string; +} + +export interface ServerStatus { + running: boolean; + pid?: number; + port?: number; + url?: string; + databaseReady: boolean; + databaseMode: DatabaseMode; + databaseType: DatabaseType; +} diff --git a/cli/src/ui/banner-startup.ts b/cli/src/ui/banner-startup.ts new file mode 100644 index 00000000..3d8fb33d --- /dev/null +++ b/cli/src/ui/banner-startup.ts @@ -0,0 +1,139 @@ +// @ts-nocheck +const { + printBanner, + composeBannerLines, + deriveSystemState, + resolveLayout, + bannerColumns, +} = require('./banner'); +const { getCliVersion } = require('../lib/paths'); +const { fetchContextStatus, resolveDbType, resolveServiceChecks } = require('../lib/context-status'); + +const CARD_MIN = 68; +const FRAME_MS = 55; +const MIN_ANIMATION_MS = 2000; + +function isInteractiveTTY() { + return Boolean(process.stdout.isTTY && process.env.TERM !== 'dumb'); +} + +function mergeStartupComponents(serviceIds, checked) { + const byId = Object.fromEntries((checked || []).map((c) => [c.id, c.ok])); + return serviceIds.map((id) => { + if (byId[id] === undefined) return { id, ok: 'pending' }; + return { id, ok: byId[id] }; + }); +} + +function paintLines(lines, prevLineCount) { + if (prevLineCount > 0) { + process.stdout.write(`\x1b[${prevLineCount}A`); + } + for (const line of lines) { + process.stdout.write('\x1b[2K'); + process.stdout.write(`${line}\n`); + } +} + +async function runStartupBanner(projectRoot, project) { + const status = await fetchContextStatus(projectRoot); + printBanner({ + projectRoot, + project, + status, + systemState: deriveSystemState(project, status), + }); + return status; +} + +async function runAnimatedStartupBanner(projectRoot, project) { + const dbType = await resolveDbType(projectRoot); + const serviceIds = resolveServiceChecks(dbType); + const version = getCliVersion(); + const cols = bannerColumns(); + const { showAscii, width } = resolveLayout(cols); + + let total = 0; + let completed = 0; + let checked = []; + let frame = 0; + let prevLineCount = 0; + let timer = null; + let statusResult = null; + const animStart = Date.now(); + + function renderFrame(ready = false) { + const components = mergeStartupComponents(serviceIds, checked); + const bannerOpts = { + projectRoot, + project, + status: { components }, + systemState: ready ? deriveSystemState(project, { components }) : 'pending', + }; + const startup = { frame, completed, total, ready }; + return composeBannerLines(version, bannerOpts, { showAscii, width, startup }); + } + + function paint(ready = false) { + const lines = renderFrame(ready); + paintLines(lines, prevLineCount); + prevLineCount = lines.length; + } + + const fetchPromise = fetchContextStatus(projectRoot, { + onProgress({ phase, total: nextTotal, id, ok }) { + if (phase === 'ready') total = nextTotal; + if (phase === 'done' && id && !String(id).startsWith('__')) { + completed += 1; + checked = [...checked.filter((c) => c.id !== id), { id, ok: Boolean(ok) }]; + } + }, + }).then((result) => { + statusResult = result; + return result; + }); + + timer = setInterval(() => { + frame += 1; + paint(false); + }, FRAME_MS); + paint(false); + + const status = await fetchPromise; + if (timer) { + const remain = Math.max(0, MIN_ANIMATION_MS - (Date.now() - animStart)); + if (remain > 0) { + await new Promise((resolve) => setTimeout(resolve, remain)); + } + clearInterval(timer); + timer = null; + } + + completed = total; + + const systemState = deriveSystemState(project, status); + const finalLines = composeBannerLines( + version, + { projectRoot, project, status, systemState }, + { showAscii, width }, + ); + paintLines(finalLines, prevLineCount); + + return statusResult || status; +} + +async function refreshBannerWithStartup(projectRoot, project, { animated = true } = {}) { + const cols = bannerColumns(); + if (!animated || !isInteractiveTTY() || cols < CARD_MIN) { + return runStartupBanner(projectRoot, project); + } + return runAnimatedStartupBanner(projectRoot, project); +} + +module.exports = { + refreshBannerWithStartup, + runAnimatedStartupBanner, + runStartupBanner, + mergeStartupComponents, + isInteractiveTTY, +}; diff --git a/cli/src/ui/banner.ts b/cli/src/ui/banner.ts new file mode 100644 index 00000000..38b3593b --- /dev/null +++ b/cli/src/ui/banner.ts @@ -0,0 +1,555 @@ +// @ts-nocheck +const os = require('os'); +const chalk = require('chalk'); +const { + brand, + icon, + palette, + visibleLength, + padRight, + gradientText, + macTrafficLights, + systemStatusBadge, + statusPill, + pulseBar, +} = require('./theme'); +const { t } = require('../lib/i18n'); +const { getCliVersion } = require('../lib/paths'); + +/** Full ANSI Shadow logo — 81 cells wide. */ +const TECH_LOGO = [ + '██████╗ ███████╗ █████╗ ██████╗████████╗██████╗ ██████╗ ███████╗███████╗███████╗', + '██╔══██╗██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝██╔════╝', + '██████╔╝█████╗ ███████║██║ ██║ ██████╔╝██████╔╝█████╗ ███████╗███████╗', + '██╔══██╗██╔══╝ ██╔══██║██║ ██║ ██╔═══╝ ██╔══██╗██╔══╝ ╚════██║╚════██║', + '██║ ██║███████╗██║ ██║╚██████╗ ██║ ██║ ██║ ██║███████╗███████║███████║', + '╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝', +]; + +const LOGO_WIDTH = TECH_LOGO[0].length; +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'; +const REPO_DISPLAY = 'github.com/fecommunity/reactpress'; +const CARD_MIN = 68; +const ASCII_CARD_PAD = 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 homify(p) { + if (!p) return p; + const home = os.homedir(); + if (home && p.startsWith(home)) return '~' + p.slice(home.length); + return p; +} + +function truncateText(text, maxLen) { + const raw = String(text); + if (visibleLength(raw) <= maxLen) return raw; + if (maxLen <= 3) return '…'; + let out = '…'; + for (let i = raw.length - 1; i >= 0; i -= 1) { + const candidate = '…' + raw.slice(i); + if (visibleLength(candidate) <= maxLen) { + out = candidate; + break; + } + } + return out; +} + +function bannerColumns() { + const raw = Number(process.stdout.columns); + return raw >= 48 ? raw : 80; +} + +/** Core dev stack — Docker/Nginx are shown but do not affect the SYSTEM badge. */ +const CORE_SYSTEM_SERVICES = new Set(['sqlite', 'mysql', 'server', 'web']); + +function deriveSystemState(project, status) { + const type = project && project.type; + if (type !== 'monorepo' && type !== 'standalone') { + return 'pending'; + } + if (!status || !Array.isArray(status.components) || status.components.length === 0) { + return 'pending'; + } + const core = status.components.filter((c) => CORE_SYSTEM_SERVICES.has(c.id)); + if (core.length === 0) return 'pending'; + const okCount = core.filter((c) => c.ok).length; + if (okCount === core.length) return 'online'; + if (okCount === 0) return 'error'; + return 'partial'; +} + +function resolveSystemState(options) { + if (options && options.systemState) return options.systemState; + if (options && options.status) { + return deriveSystemState(options.project, options.status); + } + const type = options && options.project && options.project.type; + if (type === 'monorepo' || type === 'standalone') return 'pending'; + return 'pending'; +} + +function componentLabel(component) { + return t(`banner.service.${component.id}`).trim(); +} + +function computeLabelWidth(components) { + const labels = [ + t('banner.label.mode').trim(), + t('banner.label.path').trim(), + ...(components || []).map((c) => componentLabel(c)), + ]; + return Math.max(...labels.map((label) => visibleLength(label)), 4); +} + +function componentStatusPill(component) { + if (component.ok === 'pending') { + return statusPill('pending', { pending: t('banner.pulsePending') }); + } + if (component.id === 'sqlite' || component.id === 'mysql') { + return statusPill(component.ok, { + on: t('menu.statusReady'), + off: t('menu.statusNotReady'), + }); + } + if (component.id === 'docker') { + return statusPill(component.ok, { + on: t('menu.statusYes'), + off: t('menu.statusNo'), + }); + } + if (component.id === 'nginx') { + return statusPill(component.ok, { + on: t('banner.nginxRunning'), + off: t('banner.nginxStopped'), + }); + } + return statusPill(component.ok, { + on: t('menu.statusOn'), + off: t('menu.statusOff'), + }); +} + +function appendComponentStatusRows(lines, innerWidth, status, lw) { + if (!status || !Array.isArray(status.components)) return; + for (const component of status.components) { + lines.push( + cardRow(infoRow(componentLabel(component), componentStatusPill(component), lw), innerWidth), + ); + } +} + +function resolveLayout(cols) { + const showAscii = cols >= LOGO_WIDTH + ASCII_CARD_PAD; + const width = showAscii + ? Math.min(cols - 2, Math.max(LOGO_WIDTH + ASCII_CARD_PAD, 88)) + : Math.min(Math.max(CARD_MIN, cols - 4), 88); + return { showAscii, width }; +} + +function centerLine(content, innerWidth) { + const pad = Math.max(0, Math.floor((innerWidth - visibleLength(content)) / 2)); + return ' '.repeat(pad) + content; +} + +function renderLogoLines() { + return TECH_LOGO.map((line, i) => gradientText(line, LOGO_GRADIENTS[i])); +} + +function mixHexLocal(a, b, t) { + const parse = (h) => { + const s = String(h || '#000000').replace('#', ''); + return { + r: parseInt(s.substring(0, 2), 16), + g: parseInt(s.substring(2, 4), 16), + b: parseInt(s.substring(4, 6), 16), + }; + }; + const pa = parse(a); + const pb = parse(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); + const pad = (n) => n.toString(16).padStart(2, '0'); + return `#${pad(r)}${pad(g)}${pad(bl)}`; +} + +/** Animated logo — horizontal light streak sweeps across ASCII art. */ +function renderAnimatedLogoLines(frame = 0, revealRatio = 1) { + const beamSpeed = 3.6; + const beamGlow = 16; + const cycleLen = LOGO_WIDTH + beamGlow * 2; + const head = (frame * beamSpeed) % cycleLen; + const trail = (head - beamGlow * 0.75 + cycleLen) % cycleLen; + const streamHot = '#FFFFFF'; + const streamCore = palette.accent; + const streamTrail = palette.pink; + + function beamIntensity(col, rowIndex, beamHead) { + const pos = col + rowIndex * 2.2; + const raw = Math.abs(pos - beamHead); + const wrapped = Math.min(raw, cycleLen - raw); + if (wrapped >= beamGlow) return 0; + const t = 1 - wrapped / beamGlow; + return t * t; + } + + function paintChar(ch, ci, rowIndex, colors, lineLen) { + if (ch === ' ') return ch; + + const n = Math.max(lineLen - 1, 1); + const ratio = ci / n; + const base = mixHexLocal(colors[0], colors[1] || colors[0], ratio); + const dimBase = mixHexLocal(base, palette.surface, 0.85); + + const primary = beamIntensity(ci, rowIndex, head); + const secondary = beamIntensity(ci, rowIndex, trail) * 0.55; + const glow = Math.min(1, primary + secondary); + + if (glow <= 0.03) { + return chalk.hex(dimBase)(ch); + } + + let lit = dimBase; + if (primary > 0.65) { + lit = mixHexLocal(streamHot, streamCore, 1 - primary); + } else if (glow > 0.3) { + lit = mixHexLocal(mixHexLocal(dimBase, streamTrail, 0.15), streamCore, glow); + } else { + lit = mixHexLocal(dimBase, streamTrail, glow * 0.9); + } + + const c = chalk.hex(lit); + return glow > 0.45 ? c.bold(ch) : c(ch); + } + + return TECH_LOGO.map((line, rowIndex) => { + const rowThreshold = (rowIndex + 1) / TECH_LOGO.length; + if (revealRatio < rowThreshold - 0.06) { + const ghost = mixHexLocal(palette.border, palette.surface, 0.5); + return chalk.hex(ghost)( + [...line] + .map((ch) => (ch === ' ' ? ch : '░')) + .join(''), + ); + } + + const colors = LOGO_GRADIENTS[rowIndex]; + return [...line] + .map((ch, ci) => paintChar(ch, ci, rowIndex, colors, line.length)) + .join(''); + }); +} + +function startupPulseRow(innerWidth, { completed = 0, total = 0, ready = false, frame = 0 } = {}) { + const barWidth = Math.min(24, Math.max(8, innerWidth - 22)); + let filled = 0; + if (total > 0) { + filled = Math.round((completed / total) * barWidth); + if (!ready) filled = Math.min(barWidth, filled + (frame % 2)); + } else { + filled = 4 + (frame % Math.max(1, barWidth - 4)); + } + const label = brand.muted(padRight(t('banner.pulseLabel').trim(), 6)); + const bar = pulseBar(barWidth, filled); + const status = ready + ? brand.success(t('banner.pulseReady')) + : brand.warn(t('banner.pulsePending')); + return centerLine(`${label}${bar} ${status}`, innerWidth); +} + +function cardTop(width) { + return brand.border(` ╭${'─'.repeat(width - 2)}╮`); +} + +function cardBottom(width) { + return brand.border(` ╰${'─'.repeat(width - 2)}╯`); +} + +function cardRow(content, innerWidth) { + const padded = padRight(content, innerWidth); + return brand.border(' │ ') + padded + brand.border(' │'); +} + +function cardGap(innerWidth) { + return cardRow('', innerWidth); +} + +function titleBarRow(innerWidth, version, systemState) { + const lights = macTrafficLights() + brand.dim(' '); + const lightsW = visibleLength(lights); + const title = brand.dim(`reactpress · v${version}`); + const titleW = visibleLength(title); + const status = systemStatusBadge(systemState); + const statusW = visibleLength(status); + + const titleStart = Math.max(lightsW + 1, Math.floor((innerWidth - titleW) / 2)); + const padBeforeTitle = Math.max(0, titleStart - lightsW); + + let line = lights + ' '.repeat(padBeforeTitle) + title; + const gap = innerWidth - visibleLength(line) - statusW; + if (gap >= 2) { + line += ' '.repeat(gap) + status; + } else if (gap >= 0) { + line += ' '.repeat(gap); + } + return padRight(line, innerWidth); +} + +function repoRow(innerWidth) { + const link = + brand.dim(icon.link + ' ') + + hyperlink(REPO_URL, brand.dim.underline(REPO_DISPLAY)); + return centerLine(link, innerWidth); +} + +function headerRows(innerWidth, version, systemState) { + return [titleBarRow(innerWidth, version, systemState), repoRow(innerWidth)]; +} + +function wordmarkHero() { + const bars = brand.border('═══'); + const word = gradientText('REACTPRESS', [palette.pink, palette.primary, palette.accent], { + bold: true, + }); + return `${bars} ${word} ${bars}`; +} + +/** Compact wordmark with the same streaming-light sweep. */ +function renderAnimatedWordmark(frame = 0) { + const text = 'REACTPRESS'; + const beamSpeed = 3.2; + const beamGlow = 6; + const cycleLen = text.length + beamGlow * 2; + const head = (frame * beamSpeed) % cycleLen; + const streamHot = '#F0FDFF'; + + const bars = brand.border('═══'); + const painted = [...text] + .map((ch, ci) => { + const raw = Math.abs(ci + 0.5 - head); + const wrapped = Math.min(raw, cycleLen - raw); + const glow = wrapped >= beamGlow ? 0 : Math.pow(1 - wrapped / beamGlow, 2); + const base = mixHexLocal(palette.pink, palette.accent, ci / Math.max(text.length - 1, 1)); + const dimBase = mixHexLocal(base, palette.surface, 0.65); + if (glow <= 0.05) return chalk.hex(dimBase).bold(ch); + const lit = + glow > 0.75 + ? mixHexLocal(streamHot, palette.accent, 1 - glow) + : mixHexLocal(dimBase, palette.accent, glow); + const c = chalk.hex(lit); + return glow > 0.5 ? c.bold(ch) : c(ch); + }) + .join(''); + return `${bars} ${painted} ${bars}`; +} + +function taglineRow() { + return `${brand.accent('◇')} ${brand.accent(t('banner.tagline').trim())}`; +} + +function softRule(innerWidth) { + const w = Math.min(innerWidth - 4, Math.max(28, innerWidth - 16)); + return centerLine(brand.border('─'.repeat(w)), innerWidth); +} + +function modeChip(type) { + if (type === 'monorepo') { + return chalk.bgHex(palette.primary).hex('#0B1220').bold(` ${t('banner.mode.monorepo').trim()} `); + } + if (type === 'standalone') { + return chalk.bgHex(palette.accent).hex('#0B1220').bold(` ${t('banner.mode.standalone').trim()} `); + } + return chalk.bgHex(palette.gray).hex('#0B1220').bold(` ${t('banner.mode.uninitialized').trim()} `); +} + +function infoRow(label, value, lw) { + return brand.muted(padRight(label, lw)) + brand.border('│ ') + value; +} + +function commandRail() { + return brand.primary('》 ') + gradientText('init', [palette.primary, palette.accent]); +} + +function appendInfoRows(lines, innerWidth, options, lw) { + if (options.project) { + lines.push( + cardRow(infoRow(t('banner.label.mode').trim(), modeChip(options.project.type), lw), innerWidth), + ); + } + if (options.projectRoot) { + const pathValue = + brand.primary('▶') + + brand.dim(' ') + + brand.dim(truncateText(homify(options.projectRoot), innerWidth - lw - 8)); + lines.push(cardRow(infoRow(t('banner.label.path').trim(), pathValue, lw), innerWidth)); + } + appendComponentStatusRows(lines, innerWidth, options.status, lw); +} + +function composeBannerLines(version, options, { showAscii, width, startup } = {}) { + const innerWidth = width - 4; + const systemState = startup && !startup.ready ? 'pending' : resolveSystemState(options); + const components = (options.status && options.status.components) || []; + const lw = computeLabelWidth(components); + const lines = []; + + lines.push(''); + lines.push(cardTop(width)); + for (const row of headerRows(innerWidth, version, systemState)) { + lines.push(cardRow(row, innerWidth)); + } + lines.push(cardGap(innerWidth)); + + if (showAscii) { + const logoLines = + startup && !startup.ready + ? renderAnimatedLogoLines(startup.frame || 0, 1) + : renderLogoLines(); + for (const logoLine of logoLines) { + lines.push(cardRow(centerLine(logoLine, innerWidth), innerWidth)); + } + lines.push(cardGap(innerWidth)); + lines.push(cardRow(centerLine(taglineRow(), innerWidth), innerWidth)); + if (startup && !startup.ready) { + lines.push( + cardRow( + startupPulseRow(innerWidth, { + completed: startup.completed, + total: startup.total, + ready: startup.ready, + frame: startup.frame || 0, + }), + innerWidth, + ), + ); + } + } else { + const wordmark = + startup && !startup.ready ? renderAnimatedWordmark(startup.frame || 0) : wordmarkHero(); + lines.push(cardRow(centerLine(wordmark, innerWidth), innerWidth)); + lines.push(cardRow(centerLine(taglineRow(), innerWidth), innerWidth)); + if (startup && !startup.ready) { + lines.push( + cardRow( + startupPulseRow(innerWidth, { + completed: startup.completed, + total: startup.total, + ready: startup.ready, + frame: startup.frame || 0, + }), + innerWidth, + ), + ); + } + } + + lines.push(cardRow(softRule(innerWidth), innerWidth)); + appendInfoRows(lines, innerWidth, options, lw); + lines.push(cardBottom(width)); + lines.push(` ${commandRail()}`); + lines.push(''); + return lines; +} + +function printCardBanner(version, options, { showAscii, width, startup } = {}) { + const lines = composeBannerLines(version, options, { showAscii, width, startup }); + for (const line of lines) console.log(line); +} + +function printCompactServiceStatus(status) { + if (!status || !Array.isArray(status.components)) return; + const lw = computeLabelWidth(status.components); + for (const component of status.components) { + console.log( + ` ${brand.muted(padRight(componentLabel(component), lw))}${brand.border('│ ')}${componentStatusPill(component)}`, + ); + } +} + +function printCompactBanner(version, options) { + const systemState = resolveSystemState(options); + + console.log(''); + console.log( + ` ${macTrafficLights()} ${brand.dim(`reactpress · v${version}`)} ${systemStatusBadge(systemState)}`, + ); + console.log(` ${brand.muted(icon.link)} ${hyperlink(REPO_URL, brand.dim.underline(REPO_DISPLAY))}`); + console.log(''); + for (const logoLine of renderLogoLines()) { + console.log(` ${logoLine}`); + } + console.log(` ${centerLine(taglineRow(), bannerColumns() - 4)}`); + if (options.project) console.log(` ${modeChip(options.project.type)}`); + if (options.projectRoot) { + console.log(` ${brand.primary('▶')} ${brand.dim(homify(options.projectRoot))}`); + } + printCompactServiceStatus(options.status); + console.log(` ${commandRail()}`); + console.log(''); +} + +function printBanner(options = {}) { + const version = getCliVersion(); + const cols = bannerColumns(); + const { showAscii, width } = resolveLayout(cols); + + if (cols < CARD_MIN) { + printCompactBanner(version, options); + return; + } + + printCardBanner(version, options, { showAscii, width }); +} + +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.border(` ╭${horizontal}╮`); + const bottom = brand.border(` ╰${horizontal}╯`); + const body = lines.map((line) => { + const padded = padRight(line, innerWidth); + return brand.border(' │ ') + padded + brand.border(' │'); + }); + return [top, ...body, bottom]; +} + +module.exports = { + printBanner, + visibleLength, + padRight, + TECH_LOGO, + LOGO_GRADIENTS, + LOGO_WIDTH, + box, + commandRail, + renderLogoLines, + renderAnimatedLogoLines, + renderAnimatedWordmark, + composeBannerLines, + startupPulseRow, + resolveLayout, + bannerColumns, + titleBarRow, + macTrafficLights, + deriveSystemState, + resolveSystemState, + CORE_SYSTEM_SERVICES, +}; diff --git a/cli/src/ui/interactive.ts b/cli/src/ui/interactive.ts new file mode 100644 index 00000000..fbcade3f --- /dev/null +++ b/cli/src/ui/interactive.ts @@ -0,0 +1,461 @@ +// @ts-nocheck +const inquirer = require('inquirer'); +const ora = require('ora'); +const open = require('open'); +const { refreshBannerWithStartup } = require('./banner-startup'); +const { + brand, + label, + ok, + fail, + padRight, + visibleLength, +} = require('./theme'); +const { ensureOriginalCwd } = require('../lib/root'); +const { describeProject } = require('../lib/project-type'); +const { hasResolvableActiveTheme } = require('../lib/theme-runtime'); +const { ensureProjectEnvironment, initMonorepoProject } = require('../lib/bootstrap'); +const { runDev, runThemeDev, runWebDev, runLocalWebDev, runDesktopDev } = 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'); +const { runBuild, TARGETS } = require('../lib/build'); +const { loadClientSiteUrl } = require('../lib/http'); +const { t } = require('../lib/i18n'); + +const GROUP_PREFIX = '__group:'; + +function menuSection(title) { + return new inquirer.Separator(` ${brand.border('──')} ${brand.dim(String(title).toUpperCase())}`); +} + +function formatChoice(key, text, hint) { + const keyCol = key ? brand.primary(`${key}.`) : ' '; + const hintPart = hint ? brand.dim(` ${hint}`) : ''; + return `${keyCol} ${text}${hintPart}`; +} + +function choice(labelKey, value, hintKey) { + return { + _label: t(labelKey), + _hint: hintKey ? t(hintKey) : '', + value, + }; +} + +function toInquirerChoices(items, { start = 1, alignHints = true } = {}) { + const actionable = items.filter((item) => item._label); + const labelWidth = alignHints + ? Math.min(Math.max(...actionable.map((item) => visibleLength(item._label)), 0), 34) + : 0; + + let n = start - 1; + return items.map((item) => { + if (item instanceof inquirer.Separator) { + return item; + } + const shortLabel = item._short || stripAnsi(item._label || item.name || item.value); + if (item.noShortcut) { + return { + name: item._label, + value: item.value, + short: shortLabel, + }; + } + n += 1; + const key = n <= 9 ? String(n) : ''; + const labelText = labelWidth ? padRight(item._label, labelWidth) : item._label; + return { + name: formatChoice(key, labelText, item._hint), + value: item.value, + short: shortLabel, + }; + }); +} + +function stripAnsi(text) { + return String(text || '').replace(/\u001b\[[0-9;]*m/g, ''); +} + +function buildMenuGroups(project) { + const standalone = project.type === 'standalone'; + const monorepo = project.type === 'monorepo'; + const showTheme = hasResolvableActiveTheme(project.root); + + const run = { + key: 'run', + title: t('menu.section.run'), + items: [ + 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'), + ], + }; + + const extendItems = []; + if (project.hasDesktop) { + extendItems.push(choice('menu.devDesktop', 'dev:desktop', 'menu.hint.devDesktop')); + } + if (project.hasWeb) { + extendItems.push(choice('menu.devWeb', 'dev:web', 'menu.hint.devWeb')); + extendItems.push(choice('menu.devLocalWeb', 'dev:local-web', 'menu.hint.devLocalWeb')); + } + extendItems.push(choice('menu.initLocal', 'init:local', 'menu.hint.initLocal')); + extendItems.push(choice('menu.themeList', 'theme:list', 'menu.hint.themeList')); + if (monorepo && project.hasPluginsWorkspace) { + extendItems.push(choice('menu.pluginList', 'plugin:list', 'menu.hint.pluginList')); + } + + const lifecycleItems = [choice('menu.devApi', 'dev:api', 'menu.hint.devApi')]; + if (showTheme) { + lifecycleItems.push(choice('menu.devClient', 'dev:client', 'menu.hint.devClient')); + } + lifecycleItems.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') + ); + + const buildItems = [choice('menu.build', 'build', 'menu.hint.build')]; + if (monorepo) { + buildItems.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) { + buildItems.push( + choice('menu.dockerUp', 'docker:up', 'menu.hint.dockerUp'), + choice('menu.dockerStop', 'docker:stop', 'menu.hint.dockerStop') + ); + } + if (monorepo) { + buildItems.push(choice('menu.publish', 'publish', 'menu.hint.publish')); + } + + const toolItems = [ + 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.dbBackup', 'db:backup', 'menu.hint.dbBackup'), + choice('menu.openAdmin', 'open:admin', 'menu.hint.openAdmin'), + ]; + + const groups = [run]; + if (extendItems.length > 0) { + groups.push({ key: 'extend', title: t('menu.section.extend'), items: extendItems }); + } + groups.push( + { key: 'lifecycle', title: t('menu.section.lifecycle'), items: lifecycleItems }, + { key: 'build', title: t('menu.section.build'), items: buildItems }, + { key: 'tools', title: t('menu.section.tools'), items: toolItems } + ); + return groups; +} + +function getTopLevelMenu(project) { + const groups = buildMenuGroups(project); + const runGroup = groups[0]; + const subGroups = groups.slice(1); + + const items = [menuSection(t('menu.section.quick')), ...runGroup.items]; + + if (subGroups.length > 0) { + items.push(menuSection(t('menu.section.explore'))); + for (const group of subGroups) { + items.push({ + _label: t(`menu.group.${group.key}`), + _hint: t(`menu.groupHint.${group.key}`), + value: `${GROUP_PREFIX}${group.key}`, + }); + } + } + + items.push( + new inquirer.Separator(), + { _label: brand.dim(t('menu.exit')), value: 'exit', noShortcut: true } + ); + return toInquirerChoices(items, { alignHints: false }); +} + +function getSubMenu(group) { + const items = [ + menuSection(group.title), + ...group.items, + new inquirer.Separator(), + { _label: brand.dim(`← ${t('menu.backToMain')}`), value: '__back', noShortcut: true }, + ]; + return toInquirerChoices(items); +} + +function getMenuActions(project) { + const groups = buildMenuGroups(project); + const items = []; + for (const group of groups) { + items.push(menuSection(group.title), ...group.items); + } + items.push(new inquirer.Separator(), choice('menu.exit', 'exit')); + return toInquirerChoices(items); +} + +async function pickMenuAction(project) { + const groups = buildMenuGroups(project); + const groupMap = Object.fromEntries(groups.map((group) => [group.key, group])); + + while (true) { + const { action } = await inquirer.prompt([ + { + type: 'list', + name: 'action', + message: `${brand.primary('›')} ${t('menu.prompt')}`, + pageSize: 14, + loop: false, + choices: getTopLevelMenu(project), + }, + ]); + + if (!action || typeof action !== 'string') { + continue; + } + + if (!action.startsWith(GROUP_PREFIX)) { + return action; + } + + const groupKey = action.slice(GROUP_PREFIX.length); + const group = groupMap[groupKey]; + if (!group) { + continue; + } + + const { subAction } = await inquirer.prompt([ + { + type: 'list', + name: 'subAction', + message: `${brand.primary('›')} ${t('menu.subPrompt', { section: group.title })}`, + pageSize: 14, + loop: false, + choices: getSubMenu(group), + }, + ]); + + if (subAction !== '__back') { + return subAction; + } + } +} + +async function refreshInteractiveBanner(projectRoot, project, { animated = true } = {}) { + return refreshBannerWithStartup(projectRoot, project, { animated }); +} + +async function withSpinner(text, fn) { + const spinner = ora({ text, color: 'gray', 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': { + const result = await withSpinner(t('menu.initProject'), () => + ensureProjectEnvironment(projectRoot) + ); + console.log(ok(result.message || t('menu.done'))); + return true; + } + case 'status': + await printUnifiedStatus(projectRoot); + return true; + case 'doctor': { + const code = await runDoctor(projectRoot); + if (code !== 0) process.exit(code); + return true; + } + case 'dev:api': + await runApiDev(projectRoot); + return false; + case 'dev:client': + await runThemeDev(projectRoot); + return false; + case 'dev:desktop': + await runDesktopDev(projectRoot); + return false; + case 'dev:web': + await runWebDev(projectRoot); + return false; + case 'dev:local-web': + process.env.REACTPRESS_LOCAL_MODE = '1'; + process.env.REACTPRESS_SKIP_NGINX = '1'; + await runLocalWebDev(projectRoot); + return false; + case 'init:local': { + const { isMonorepoCheckout } = require('../lib/root'); + const { initProject } = require('../core/services/init'); + const result = await withSpinner(t('menu.initProject'), async () => { + if (isMonorepoCheckout(projectRoot)) { + return initMonorepoProject(projectRoot, { local: true }); + } + return initProject({ directory: projectRoot, force: false, local: true }); + }); + console.log(ok(result.message || t('menu.done'))); + return true; + } + case 'theme:list': + require('../lib/theme-cli').runThemeList(projectRoot); + return true; + case 'plugin:list': + require('../lib/plugin-cli').runPluginList(projectRoot); + return true; + case 'db:backup': + await withSpinner(t('cli.db.backup'), async () => { + await runDbBackup(projectRoot); + }); + return true; + case 'server:start': { + const code = await withSpinner(t('menu.startingApi'), () => + runLifecycleCommand('start', projectRoot) + ); + if (code !== 0) process.exit(code); + return true; + } + case 'server:stop': + await withSpinner(t('menu.stoppingApi'), async () => { + await runLifecycleCommand('stop', projectRoot); + }); + return true; + case 'server:restart': { + const code = await withSpinner(t('menu.restartingApi'), () => + runLifecycleCommand('restart', projectRoot) + ); + if (code !== 0) process.exit(code); + return true; + } + case 'build': { + const buildChoices = TARGETS.map((target) => ({ + name: + target === 'all' + ? t('menu.buildAll') + : t(`build.label.${target}`), + value: target, + })); + const { target } = await inquirer.prompt([ + { + type: 'list', + name: 'target', + message: t('menu.buildTarget'), + pageSize: 12, + choices: buildChoices, + }, + ]); + await runBuild(target, projectRoot); + return true; + } + case 'docker:start': + await runDockerCommand('start', projectRoot); + return false; + case 'docker:up': + await withSpinner(t('docker.starting'), () => runDockerCommand('up', projectRoot)); + return true; + case 'docker:stop': + 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); + console.log(label(t('menu.opening', { url }))); + await open(url); + return true; + } + case 'publish': { + const prev = process.argv.slice(); + process.argv = [process.argv[0], process.argv[1], '--publish']; + await require('../lib/publish').main(); + process.argv = prev; + return true; + } + case 'exit': + return false; + default: + return true; + } +} + +async function runInteractiveLoop() { + const projectRoot = ensureOriginalCwd(); + const project = describeProject(projectRoot); + + await refreshInteractiveBanner(projectRoot, project, { animated: true }); + console.log(` ${brand.dim(t('menu.shortcuts'))}`); + console.log(''); + + let loop = true; + while (loop) { + const action = await pickMenuAction(project); + + if (action === 'exit') { + console.log(brand.muted(t('menu.goodbye'))); + break; + } + + try { + const stay = await runMenuAction(action, projectRoot, project); + if (!stay) break; + + if (action !== 'status' && action !== 'doctor') { + console.log(''); + await refreshInteractiveBanner(projectRoot, project, { animated: false }); + } + } catch (err) { + console.error(fail(err.message || err)); + const { retry } = await inquirer.prompt([ + { + type: 'confirm', + name: 'retry', + message: t('menu.retry'), + default: true, + }, + ]); + loop = retry; + if (loop) { + console.log(''); + await refreshInteractiveBanner(projectRoot, project, { animated: false }); + } + } + } +} + +module.exports = { + runInteractiveLoop, + runMenuAction, + getMenuActions, + buildMenuGroups, + pickMenuAction, +}; diff --git a/cli/src/ui/theme.ts b/cli/src/ui/theme.ts new file mode 100644 index 00000000..37faed44 --- /dev/null +++ b/cli/src/ui/theme.ts @@ -0,0 +1,328 @@ +// @ts-nocheck +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', + border: '#4A4580', + surface: '#12152A', + macRed: '#FF5F57', + macYellow: '#FEBC2E', + macGreen: '#28C840', + macGray: '#5C5C5C', +}; + +const brand = { + 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), + border: chalk.hex(palette.border), + 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}`; +} + +/** Single-line status probe progress: bar + percent + optional service label. */ +function statusProgressLine({ completed, total, label, barWidth = 24, filled } = {}) { + const safeTotal = Math.max(1, total || 1); + const barFilled = + filled !== undefined ? filled : Math.round((completed / safeTotal) * barWidth); + const pct = Math.min(100, Math.round((completed / safeTotal) * 100)); + const bar = pulseBar(barWidth, barFilled); + const labelPart = label ? brand.dim(` ${label}`) : ''; + return `${bar} ${brand.muted(`${pct}%`)}${labelPart}`; +} + +/** + * macOS Terminal-style traffic-light window controls (red · yellow · green). + */ +function macTrafficLights() { + return ( + chalk.hex(palette.macRed)('●') + + brand.dim(' ') + + chalk.hex(palette.macYellow)('●') + + brand.dim(' ') + + chalk.hex(palette.macGreen)('●') + ); +} + +/** + * Unified status badge — color encodes health (online / partial / error / pending). + * @param {'online'|'partial'|'error'|'pending'} state + */ +function systemStatusBadge(state = 'pending') { + const { t } = require('../lib/i18n'); + const words = { + online: 'banner.systemOnline', + partial: 'banner.systemPartial', + error: 'banner.systemError', + pending: 'banner.systemPending', + }; + const painters = { + online: brand.success, + partial: brand.warn, + error: brand.error, + pending: brand.muted, + }; + const key = words[state] || words.pending; + const paint = painters[state] || painters.pending; + return paint(t(key).trim()); +} + +/** + * Three-light status indicator used in status panels. + */ +function statusLights(state = 'online') { + if (state === 'offline') { + return `${brand.muted('●')} ${brand.muted('●')} ${brand.muted('●')}`; + } + if (state === 'degraded') { + return `${brand.warn('●')} ${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 = String(h || '#000000').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 `${icon.arrow} ${brand.primary(text)}`; +} + +function ok(text) { + return `${icon.ok} ${brand.success(text)}`; +} + +function fail(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} `); +} + +/** Compact pill badge for banner feature highlights. */ +function badge(text, color = palette.accent) { + return chalk.bgHex(palette.surface).hex(color).bold(` ${text} `); +} + +function kv(key, value, { keyWidth = 10, valueColor = (s) => s } = {}) { + return `${brand.muted(padRight(key, keyWidth))} ${valueColor(value)}`; +} + +/** + * 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 ruleLen = Math.min(40, Math.max(12, w - visibleLength(title) - 6)); + const rule = brand.dim('─'.repeat(ruleLen)); + return ` ${brand.dim(title)} ${rule}`; +} + +module.exports = { + palette, + brand, + icon, + label, + ok, + fail, + warn, + info, + chip, + badge, + kv, + statusPill, + sectionHeader, + visibleLength, + padRight, + padLeft, + terminalWidth, + divider, + gradientText, + pulseBar, + statusProgressLine, + statusLights, + macTrafficLights, + systemStatusBadge, +}; diff --git a/cli/templates/config.local.json b/cli/templates/config.local.json new file mode 100644 index 00000000..5d3bcd19 --- /dev/null +++ b/cli/templates/config.local.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "database": { + "mode": "embedded-sqlite", + "sqlitePath": ".reactpress/reactpress.db" + }, + "server": { + "port": 3002, + "clientUrl": "http://localhost:3001", + "serverUrl": "http://127.0.0.1:3002", + "apiPrefix": "/api" + } +} diff --git a/cli/templates/env.local.default b/cli/templates/env.local.default new file mode 100644 index 00000000..630d7934 --- /dev/null +++ b/cli/templates/env.local.default @@ -0,0 +1,10 @@ +# ReactPress — local SQLite mode (auto-generated) +DB_TYPE=sqlite +DB_DATABASE=.reactpress/reactpress.db +SERVER_PORT=3002 +SERVER_SITE_URL=http://127.0.0.1:3002 +CLIENT_SITE_URL=http://localhost:3001 +SERVER_API_PREFIX=/api +REACTPRESS_UPLOAD_DIR=uploads +ADMIN_USER=admin +ADMIN_PASSWD=admin diff --git a/cli/templates/theme-catalog.json b/cli/templates/theme-catalog.json new file mode 100644 index 00000000..eaa37aee --- /dev/null +++ b/cli/templates/theme-catalog.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "themes": [ + { + "id": "reactpress-theme-starter", + "name": "ReactPress Theme Starter", + "version": "1.0.0-beta.0", + "description": "官方 Next.js 15 主题 — Tailwind CSS、知识库、评论、深色模式,Lighthouse 95+。", + "author": "ReactPress", + "themeUri": "https://github.com/fecommunity/reactpress-theme-starter", + "previewUrl": "https://reactpress-theme-starter.vercel.app", + "tags": [ + "官方", + "Tailwind", + "App Router", + "Next.js 15" + ], + "dependency": { + "name": "@fecommunity/reactpress-theme-starter", + "version": "1.0.0-beta.0" + }, + "npm": "@fecommunity/reactpress-theme-starter@1.0.0-beta.0", + "featured": true, + "requires": ">=3.0.0" + } + ] +} diff --git a/cli/tests/admin-static.test.js b/cli/tests/admin-static.test.js new file mode 100644 index 00000000..ce05a927 --- /dev/null +++ b/cli/tests/admin-static.test.js @@ -0,0 +1,56 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + adminSourceIsNewerThanTheme, + ensureAdminStaticForTheme, +} = require('../out/core/services/admin-static'); + +const CLI_ROOT = path.join(__dirname, '..'); + +describe('admin-static upgrade sync', () => { + it('adminSourceIsNewerThanTheme detects stale theme admin bundle', () => { + const themeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'reactpress-admin-stale-')); + const publicAdmin = path.join(themeDir, 'public', 'admin'); + fs.mkdirSync(publicAdmin, { recursive: true }); + fs.writeFileSync( + path.join(publicAdmin, 'index.html'), + '', + 'utf8', + ); + + const bundledIndex = path.join(CLI_ROOT, 'admin-dist', 'index.html'); + if (!fs.existsSync(bundledIndex)) return; + + assert.equal(adminSourceIsNewerThanTheme(CLI_ROOT, themeDir), true); + fs.rmSync(themeDir, { recursive: true, force: true }); + }); + + it('ensureAdminStaticForTheme refreshes stale public/admin from bundled admin-dist', () => { + const bundledIndex = path.join(CLI_ROOT, 'admin-dist', 'index.html'); + if (!fs.existsSync(bundledIndex)) return; + + const themeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'reactpress-admin-resync-')); + const publicAdmin = path.join(themeDir, 'public', 'admin'); + fs.mkdirSync(publicAdmin, { recursive: true }); + fs.writeFileSync( + path.join(publicAdmin, 'index.html'), + '', + 'utf8', + ); + + const synced = ensureAdminStaticForTheme(CLI_ROOT, themeDir); + assert.equal(synced, true); + + const html = fs.readFileSync(path.join(publicAdmin, 'index.html'), 'utf8'); + const bundledHtml = fs.readFileSync(bundledIndex, 'utf8'); + const themeJs = html.match(/assets\/(index-[^"]+\.js)/)?.[1]; + const bundledJs = bundledHtml.match(/assets\/(index-[^"]+\.js)/)?.[1]; + assert.equal(themeJs, bundledJs); + + fs.rmSync(themeDir, { recursive: true, force: true }); + }); +}); diff --git a/cli/tests/banner.test.js b/cli/tests/banner.test.js new file mode 100644 index 00000000..b11852a4 --- /dev/null +++ b/cli/tests/banner.test.js @@ -0,0 +1,174 @@ +'use strict'; + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + printBanner, + TECH_LOGO, + LOGO_WIDTH, + commandRail, + renderLogoLines, + renderAnimatedLogoLines, + composeBannerLines, + deriveSystemState, + resolveSystemState, + resolveLayout, +} = require('../out/ui/banner'); +const { mergeStartupComponents } = require('../out/ui/banner-startup'); +const { systemStatusBadge } = require('../out/ui/theme'); + +const sampleComponents = [ + { id: 'mysql', ok: false }, + { id: 'server', ok: true }, + { id: 'docker', ok: false }, + { id: 'nginx', ok: false }, + { id: 'web', ok: true }, +]; + +describe('banner', () => { + it('printBanner renders service rows under PATH', () => { + const lines = []; + const origLog = console.log; + const prevCols = process.stdout.columns; + + console.log = (...args) => lines.push(args.join(' ')); + Object.defineProperty(process.stdout, 'columns', { value: 100, configurable: true }); + + try { + printBanner({ + projectRoot: '/tmp/demo-project', + project: { type: 'monorepo' }, + systemState: 'partial', + status: { components: sampleComponents }, + }); + } finally { + console.log = origLog; + Object.defineProperty(process.stdout, 'columns', { + value: prevCols, + configurable: true, + }); + } + + const output = lines.join('\n'); + assert.match(output, /██████╗/); + assert.match(output, /MySQL/); + assert.match(output, /Server/); + assert.match(output, /Docker/); + assert.match(output, /Nginx/); + assert.match(output, /Web/); + assert.doesNotMatch(output, /:3000/); + assert.doesNotMatch(output, /Toolkit/); + }); + + it('deriveSystemState ignores optional docker/nginx for SYSTEM badge', () => { + const project = { type: 'monorepo' }; + assert.equal( + deriveSystemState(project, { + components: [ + { id: 'sqlite', ok: true }, + { id: 'server', ok: true }, + { id: 'docker', ok: false }, + { id: 'nginx', ok: false }, + { id: 'web', ok: true }, + ], + }), + 'online', + ); + }); + + it('deriveSystemState maps core service checks to four states', () => { + const project = { type: 'monorepo' }; + assert.equal(deriveSystemState({ type: 'unknown' }, null), 'pending'); + assert.equal( + deriveSystemState(project, { + components: [ + { id: 'server', ok: true }, + { id: 'web', ok: true }, + ], + }), + 'online', + ); + assert.equal( + deriveSystemState(project, { + components: [ + { id: 'server', ok: true }, + { id: 'web', ok: false }, + ], + }), + 'partial', + ); + assert.equal( + deriveSystemState(project, { + components: [ + { id: 'server', ok: false }, + { id: 'web', ok: false }, + ], + }), + 'error', + ); + }); + + it('resolveSystemState derives from status when present', () => { + assert.equal( + resolveSystemState({ + project: { type: 'monorepo' }, + status: { + components: [ + { id: 'server', ok: true }, + { id: 'web', ok: true }, + ], + }, + }), + 'online', + ); + }); + + it('commandRail highlights init command', () => { + const rail = commandRail(); + assert.match(rail, /init/); + assert.doesNotMatch(rail, /start/); + }); + + it('renderAnimatedLogoLines applies streaming highlight across logo', () => { + const full = renderAnimatedLogoLines(30, 1); + assert.equal(full.length, TECH_LOGO.length); + assert.match(full[0].replace(/\u001b\[[0-9;]*m/g, ''), /^██████╗/); + assert.match(full[TECH_LOGO.length - 1].replace(/\u001b\[[0-9;]*m/g, ''), /╚═╝/); + }); + + it('composeBannerLines includes startup pulse row while probing', () => { + const { showAscii, width } = resolveLayout(100); + const lines = composeBannerLines( + '4.0.0', + { + projectRoot: '/tmp/demo', + project: { type: 'monorepo' }, + status: { + components: mergeStartupComponents(['mysql', 'server'], []), + }, + }, + { + showAscii, + width, + startup: { frame: 2, completed: 1, total: 3, ready: false }, + }, + ); + const output = lines.join('\n'); + assert.match(output, /INIT|初始化/); + assert.match(output, /▰/); + }); + + it('systemStatusBadge shows status text only', () => { + const plain = (value) => String(value).replace(/\u001b\[[0-9;]*m/g, ''); + assert.equal(plain(systemStatusBadge('online')), 'ONLINE'); + assert.equal(plain(systemStatusBadge('pending')), 'PENDING'); + assert.doesNotMatch(plain(systemStatusBadge('online')), /SYSTEM/); + }); + + it('mergeStartupComponents marks unchecked services as pending', () => { + const merged = mergeStartupComponents(['mysql', 'server'], [{ id: 'mysql', ok: true }]); + assert.equal(merged[0].ok, true); + assert.equal(merged[1].ok, 'pending'); + }); +}); diff --git a/cli/tests/build.test.js b/cli/tests/build.test.js index 5b35247e..e25ca549 100644 --- a/cli/tests/build.test.js +++ b/cli/tests/build.test.js @@ -1,6 +1,8 @@ +const fs = require('fs'); +const path = require('path'); const { describe, it } = require('node:test'); const assert = require('node:assert/strict'); -const { resolveBuildInvocation, TARGETS } = require('../lib/build'); +const { resolveBuildInvocation, TARGETS } = require('../out/lib/build'); const { createMonorepoFixture, createStandaloneProject, rmDir } = require('./helpers/tmp-project'); describe('lib/build', () => { @@ -15,10 +17,10 @@ describe('lib/build', () => { } }); - it('skips client build when client/ is missing', () => { + it('skips theme build when active theme is missing', () => { const root = createStandaloneProject(); try { - const inv = resolveBuildInvocation('build:client', root); + const inv = resolveBuildInvocation('build:theme', root); assert.equal(inv, null); } finally { rmDir(root); @@ -28,5 +30,22 @@ describe('lib/build', () => { it('exposes known targets', () => { assert.ok(TARGETS.includes('all')); assert.ok(TARGETS.includes('toolkit')); + assert.ok(TARGETS.includes('web')); + }); + + it('includes web in all steps when web/ exists', () => { + const root = createMonorepoFixture(); + try { + fs.mkdirSync(path.join(root, 'web')); + fs.writeFileSync( + path.join(root, 'web', 'package.json'), + JSON.stringify({ name: 'web', scripts: { build: 'echo build' } }) + ); + const { getBuildSteps } = require('../out/lib/build'); + const steps = getBuildSteps('all', root); + assert.ok(steps.some((s) => s.script === 'build:web')); + } finally { + rmDir(root); + } }); }); diff --git a/cli/tests/bundled-plugins.test.js b/cli/tests/bundled-plugins.test.js new file mode 100644 index 00000000..50a1315f --- /dev/null +++ b/cli/tests/bundled-plugins.test.js @@ -0,0 +1,55 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const CLI_ROOT = path.join(__dirname, '..'); + +describe('sync-bundled-plugins', () => { + it('copies plugin registry and built plugin trees into cli/plugins', () => { + const script = path.join(CLI_ROOT, 'scripts', 'sync-bundled-plugins.mjs'); + assert.ok(fs.existsSync(script)); + + const result = require('node:child_process').spawnSync(process.execPath, [script], { + cwd: CLI_ROOT, + encoding: 'utf8', + }); + assert.equal(result.status, 0, result.stderr || result.stdout); + + const bundled = path.join(CLI_ROOT, 'plugins'); + assert.ok(fs.existsSync(path.join(bundled, 'package.json'))); + for (const id of ['hello-world', 'seo', 'image-optimizer']) { + assert.ok( + fs.existsSync(path.join(bundled, id, 'plugin.json')), + `missing bundled plugin ${id}/plugin.json`, + ); + assert.ok( + fs.existsSync(path.join(bundled, id, 'dist', 'index.js')), + `missing bundled plugin ${id}/dist/index.js`, + ); + } + }); +}); + +describe('ensureBundledPlugins', () => { + it('seeds default plugins into an empty project from the CLI bundle', () => { + const bundledRegistry = path.join(CLI_ROOT, 'plugins', 'package.json'); + if (!fs.existsSync(bundledRegistry)) { + require('node:child_process').spawnSync(process.execPath, [ + path.join(CLI_ROOT, 'scripts', 'sync-bundled-plugins.mjs'), + ], { cwd: CLI_ROOT, stdio: 'inherit' }); + } + + const { ensureBundledPlugins } = require('../out/core/services/local-site'); + const siteRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'reactpress-plugins-seed-')); + try { + assert.equal(ensureBundledPlugins(siteRoot), true); + assert.ok(fs.existsSync(path.join(siteRoot, 'plugins', 'hello-world', 'plugin.json'))); + assert.ok(fs.existsSync(path.join(siteRoot, 'plugins', 'seo', 'plugin.json'))); + assert.equal(ensureBundledPlugins(siteRoot), false); + } finally { + fs.rmSync(siteRoot, { recursive: true, force: true }); + } + }); +}); diff --git a/cli/tests/bundled-server-sync.test.js b/cli/tests/bundled-server-sync.test.js new file mode 100644 index 00000000..c4ce0a1c --- /dev/null +++ b/cli/tests/bundled-server-sync.test.js @@ -0,0 +1,53 @@ +'use strict'; + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const CLI_ROOT = path.join(__dirname, '..'); + +describe('bundled server sync', () => { + it('has server dist and toolkit entry for publish', () => { + assert.ok(fs.existsSync(path.join(CLI_ROOT, 'server', 'dist', 'main.js'))); + assert.ok(fs.existsSync(path.join(CLI_ROOT, 'toolkit', 'dist', 'index.js'))); + assert.ok(fs.existsSync(path.join(CLI_ROOT, 'server', 'package.json'))); + }); + + it('bundled server entry exports main() for reactpress-server.js', () => { + const mainPath = path.join(CLI_ROOT, 'server', 'dist', 'main.js'); + const source = fs.readFileSync(mainPath, 'utf8'); + assert.match( + source, + /exports\.main\s*=|Object\.defineProperty\(exports,\s*["']main["']/, + 'dist/main.js must export main()', + ); + }); + + it('keeps README in English and blocks legacy README sync', () => { + const readme = fs.readFileSync(path.join(CLI_ROOT, 'README.md'), 'utf8'); + assert.match(readme, /official \*\*ReactPress CLI\*\*/i); + assert.doesNotMatch(readme, /[\u4e00-\u9fff]/, 'cli/README.md must stay in English'); + + const syncSrc = fs.readFileSync(path.join(CLI_ROOT, 'scripts', 'sync-bundled-core.mjs'), 'utf8'); + assert.match( + syncSrc, + /for \(const file of \['LICENSE'\]\)/, + 'sync-bundled-core must only copy LICENSE, not README.md', + ); + }); + + it('declares prune and npmignore rules for heavyweight paths', () => { + const npmignore = fs.readFileSync(path.join(CLI_ROOT, '.npmignore'), 'utf8'); + assert.match(npmignore, /server\/node_modules/); + assert.match(npmignore, /toolkit\/node_modules/); + assert.match(npmignore, /server\/public\/uploads/); + + const pruneSrc = fs.readFileSync( + path.join(CLI_ROOT, 'scripts', 'prune-bundled-pack.mjs'), + 'utf8', + ); + assert.match(pruneSrc, /server\/node_modules/); + assert.match(pruneSrc, /prune-bundled-pack/); + }); +}); diff --git a/cli/tests/cli-init.test.js b/cli/tests/cli-init.test.js new file mode 100644 index 00000000..6d8dd66c --- /dev/null +++ b/cli/tests/cli-init.test.js @@ -0,0 +1,113 @@ +'use strict'; + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const { execFileSync } = require('node:child_process'); +const fs = require('fs'); +const path = require('node:path'); + +const CLI_BIN = path.join(__dirname, '..', 'out', 'bin', 'reactpress.js'); + +function runCli(args, { env } = {}) { + return execFileSync(process.execPath, [CLI_BIN, ...args], { + encoding: 'utf8', + env: { ...process.env, TERM: 'dumb', ...env }, + }); +} + +function runCliFail(args, { env } = {}) { + try { + runCli(args, { env }); + return null; + } catch (err) { + return err; + } +} + +describe('reactpress init-only CLI', () => { + it('bare reactpress shows help', () => { + const out = runCli([]); + assert.match(out, /reactpress/i); + assert.match(out, /Zero deps/i); + assert.match(out, /init/i); + assert.match(out, /doctor/i); + assert.match(out, /Usage:/i); + }); + + it('--help documents init and doctor commands', () => { + const out = runCli(['--help']); + assert.match(out, /reactpress/i); + assert.match(out, /Zero deps/i); + assert.match(out, /init/i); + assert.match(out, /doctor/i); + assert.doesNotMatch(out, /reactpress docker/i); + assert.doesNotMatch(out, /reactpress nginx/i); + assert.doesNotMatch(out, /reactpress dev/i); + assert.doesNotMatch(out, /reactpress start/i); + }); + + it('rejects legacy subcommands', () => { + const err = runCliFail(['start']); + assert.ok(err); + assert.match(String(err.stderr || err.stdout || ''), /init/i); + }); + + it('runs doctor and prints diagnostics', () => { + const { createStandaloneProject, rmDir } = require('./helpers/tmp-project'); + const root = createStandaloneProject(); + try { + const err = runCliFail(['doctor', root], { + env: { REACTPRESS_LOCAL_MODE: '1', REACTPRESS_SKIP_NGINX: '1' }, + }); + assert.ok(err); + const out = String(err.stdout || err.stderr || ''); + assert.match(out, /ReactPress Doctor/i); + assert.match(out, /Node\.js/i); + assert.match(out, /Admin console/i); + } finally { + rmDir(root); + } + }); + + it('doctor logs lists project log files', () => { + const { createStandaloneProject, rmDir } = require('./helpers/tmp-project'); + const root = createStandaloneProject(); + try { + const logDir = path.join(root, '.reactpress', 'logs', 'server', 'error'); + fs.mkdirSync(logDir, { recursive: true }); + fs.writeFileSync(path.join(logDir, 'error.log.-2026-07-11.log'), '[ERROR] test\n', 'utf8'); + + const out = runCli(['doctor', 'logs', root, '--tail', '5'], { + env: { REACTPRESS_LOCAL_MODE: '1', REACTPRESS_SKIP_NGINX: '1' }, + }); + assert.match(out, /ReactPress Logs/i); + assert.match(out, /\[ERROR\] test/); + } finally { + rmDir(root); + } + }); + + it('logs command lists project log files', () => { + const { createStandaloneProject, rmDir } = require('./helpers/tmp-project'); + const root = createStandaloneProject(); + try { + const logDir = path.join(root, '.reactpress', 'logs', 'server', 'error'); + fs.mkdirSync(logDir, { recursive: true }); + fs.writeFileSync(path.join(logDir, 'error.log.-2026-07-11.log'), '[ERROR] via-logs\n', 'utf8'); + + const out = runCli(['logs', root, '--tail', '5'], { + env: { REACTPRESS_LOCAL_MODE: '1', REACTPRESS_SKIP_NGINX: '1' }, + }); + assert.match(out, /ReactPress Logs/i); + assert.match(out, /\[ERROR\] via-logs/); + } finally { + rmDir(root); + } + }); + + it('--help documents logs and stop commands', () => { + const out = runCli(['--help']); + assert.match(out, /reactpress logs/i); + assert.match(out, /reactpress stop/i); + }); +}); diff --git a/cli/tests/cli-version.test.js b/cli/tests/cli-version.test.js new file mode 100644 index 00000000..1037a77b --- /dev/null +++ b/cli/tests/cli-version.test.js @@ -0,0 +1,27 @@ +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, '..'); + +describe('CLI version resolution', () => { + it('reads version from package root (not out/package.json)', () => { + const { getCliVersion, getCliPackageRoot } = require('../out/lib/paths'); + const root = getCliPackageRoot(); + const expected = JSON.parse( + fs.readFileSync(path.join(CLI_ROOT, 'package.json'), 'utf8'), + ).version; + assert.equal(getCliVersion(), expected); + assert.equal( + fs.existsSync(path.join(root, 'package.json')), + true, + 'package root must contain package.json', + ); + assert.equal( + fs.existsSync(path.join(root, 'out', 'package.json')), + false, + 'compiled out/ must not rely on out/package.json', + ); + }); +}); diff --git a/cli/tests/context-status.test.js b/cli/tests/context-status.test.js new file mode 100644 index 00000000..1f14f27d --- /dev/null +++ b/cli/tests/context-status.test.js @@ -0,0 +1,51 @@ +'use strict'; + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const { + resolveServiceChecks, + resolveDbType, + parseEnvFile, +} = require('../out/lib/context-status'); + +describe('context-status', () => { + it('parseEnvFile reads DB_TYPE from .env', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rp-status-')); + try { + fs.writeFileSync(path.join(dir, '.env'), 'DB_TYPE=sqlite\nDB_DATABASE=data/app.db\n'); + const env = parseEnvFile(dir); + assert.equal(env.DB_TYPE, 'sqlite'); + assert.equal(env.DB_DATABASE, 'data/app.db'); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it('resolveDbType prefers sqlite from .env when config is missing', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rp-status-')); + try { + fs.writeFileSync(path.join(dir, '.env'), 'DB_TYPE=sqlite\n'); + assert.equal(await resolveDbType(dir), 'sqlite'); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it('resolveDbType defaults to mysql without sqlite hints', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rp-status-')); + try { + assert.equal(await resolveDbType(dir), 'mysql'); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it('resolveServiceChecks includes sqlite or mysql plus core services', () => { + assert.deepEqual(resolveServiceChecks('sqlite'), ['sqlite', 'server', 'web']); + assert.deepEqual(resolveServiceChecks('mysql'), ['mysql', 'server', 'web']); + }); +}); diff --git a/cli/tests/database-fallback.test.js b/cli/tests/database-fallback.test.js new file mode 100644 index 00000000..73c95808 --- /dev/null +++ b/cli/tests/database-fallback.test.js @@ -0,0 +1,80 @@ +const { describe, it, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +describe('database sqlite fallback', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rp-db-fallback-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('promotes embedded-docker to sqlite when docker is unavailable', async () => { + const reactpressDir = path.join(tmpDir, '.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(tmpDir, '.env'), + [ + 'DB_HOST=127.0.0.1', + 'DB_PORT=3307', + 'DB_USER=reactpress', + 'DB_PASSWD=reactpress', + 'DB_DATABASE=reactpress', + 'SERVER_PORT=3002', + ].join('\n'), + ); + + const { ensureDatabase } = require('../out/core/services/database'); + const { loadConfig } = require('../out/core/services/config'); + const { isDockerAvailable } = require('../out/core/services/exec'); + + if (isDockerAvailable()) { + return; + } + + const config = await loadConfig(tmpDir); + const result = await ensureDatabase(tmpDir, config); + assert.equal(result.ok, true); + + const next = await loadConfig(tmpDir); + assert.equal(next.database.mode, 'embedded-sqlite'); + + const env = fs.readFileSync(path.join(tmpDir, '.env'), 'utf8'); + assert.match(env, /^DB_TYPE=sqlite/m); + }); +}); + +describe('database-mode', () => { + it('detects sqlite mode from config.json', () => { + const { readSqliteModeFromProject } = require('../out/lib/database-mode'); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rp-db-mode-')); + try { + fs.mkdirSync(path.join(tmpDir, '.reactpress'), { recursive: true }); + fs.writeFileSync( + path.join(tmpDir, '.reactpress/config.json'), + JSON.stringify({ database: { mode: 'embedded-sqlite' } }), + ); + assert.equal(readSqliteModeFromProject(tmpDir), true); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); +}); diff --git a/cli/tests/dev-auto-local.test.js b/cli/tests/dev-auto-local.test.js new file mode 100644 index 00000000..8a952b92 --- /dev/null +++ b/cli/tests/dev-auto-local.test.js @@ -0,0 +1,46 @@ +const { describe, it, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); + +describe('dev auto local fallback', () => { + const envBackup = { ...process.env }; + + beforeEach(() => { + delete process.env.REACTPRESS_LOCAL_MODE; + delete process.env.REACTPRESS_SKIP_NGINX; + delete process.env.REACTPRESS_FORCE_MYSQL; + delete process.env.REACTPRESS_DESKTOP_LOCAL; + }); + + afterEach(() => { + process.env = { ...envBackup }; + }); + + it('enables local mode when docker is unavailable', async () => { + const { applyAutoLocalDevFallback } = require('../out/lib/dev'); + const { checkDocker } = require('../out/lib/doctor'); + const docker = checkDocker(); + if (docker.ok) { + // Cannot simulate missing docker in CI — skip when docker is running. + return; + } + + const enabled = await applyAutoLocalDevFallback(process.cwd(), { needsLocalApi: true }); + assert.equal(enabled, true); + assert.equal(process.env.REACTPRESS_LOCAL_MODE, '1'); + assert.equal(process.env.REACTPRESS_SKIP_NGINX, '1'); + }); + + it('respects explicit local mode and force mysql flag', async () => { + const { applyAutoLocalDevFallback } = require('../out/lib/dev'); + + process.env.REACTPRESS_LOCAL_MODE = '1'; + const alreadyLocal = await applyAutoLocalDevFallback(process.cwd(), { needsLocalApi: true }); + assert.equal(alreadyLocal, false); + + delete process.env.REACTPRESS_LOCAL_MODE; + process.env.REACTPRESS_FORCE_MYSQL = '1'; + const forced = await applyAutoLocalDevFallback(process.cwd(), { needsLocalApi: true }); + assert.equal(forced, false); + assert.equal(process.env.REACTPRESS_LOCAL_MODE, undefined); + }); +}); diff --git a/cli/tests/docker.test.js b/cli/tests/docker.test.js index 06142777..53d5912c 100644 --- a/cli/tests/docker.test.js +++ b/cli/tests/docker.test.js @@ -1,7 +1,7 @@ const { describe, it } = require('node:test'); const assert = require('node:assert/strict'); const path = require('path'); -const { resolveComposeContext } = require('../lib/docker'); +const { resolveComposeContext } = require('../out/lib/docker'); const { createStandaloneProject, createMonorepoFixture, rmDir } = require('./helpers/tmp-project'); describe('lib/docker', () => { diff --git a/cli/tests/doctor.test.js b/cli/tests/doctor.test.js new file mode 100644 index 00000000..bc2020a8 --- /dev/null +++ b/cli/tests/doctor.test.js @@ -0,0 +1,139 @@ +'use strict'; + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); +const { + checkDockerForProject, + resolveProjectProfile, +} = require('../out/lib/doctor'); +const { + getProjectServerLogDir, + listLogFiles, + readTailLines, + filterLines, + measureLogDir, + resolveLogSource, +} = require('../out/lib/project-logs'); +const { createStandaloneProject, rmDir } = require('./helpers/tmp-project'); + +function createSqliteProject() { + const root = createStandaloneProject(); + const configPath = path.join(root, '.reactpress', 'config.json'); + fs.writeFileSync( + configPath, + JSON.stringify( + { + version: 1, + database: { mode: 'embedded-sqlite', sqlitePath: 'data/reactpress.db' }, + server: { + port: 3002, + clientUrl: 'http://localhost:3001', + serverUrl: 'http://localhost:3002', + }, + }, + null, + 2 + ) + ); + const envPath = path.join(root, '.env'); + const env = fs.readFileSync(envPath, 'utf8').replace(/^DB_HOST=.*$/m, 'DB_TYPE=sqlite'); + fs.writeFileSync(envPath, env.includes('DB_TYPE=sqlite') ? env : `${env}DB_TYPE=sqlite\n`); + return root; +} + +describe('lib/doctor', () => { + it('skips Docker when project uses embedded-sqlite', async () => { + const root = createSqliteProject(); + try { + const profile = await resolveProjectProfile(root); + assert.equal(profile.localMode, true); + assert.equal(profile.requiresDocker, false); + + const docker = checkDockerForProject(root, profile); + assert.equal(docker.ok, true); + assert.match(docker.message, /SQLite local mode|无需 Docker/i); + } finally { + rmDir(root); + } + }); + + it('requires Docker for embedded-docker projects', async () => { + const root = createStandaloneProject(); + try { + const profile = await resolveProjectProfile(root); + assert.equal(profile.localMode, false); + assert.equal(profile.requiresDocker, true); + } finally { + rmDir(root); + } + }); + + it('reads project log files from .reactpress/logs/server', () => { + const root = createStandaloneProject(); + try { + const logDir = path.join(root, '.reactpress', 'logs', 'server', 'error'); + fs.mkdirSync(logDir, { recursive: true }); + const logFile = path.join(logDir, 'error.log.-2026-07-11.log'); + fs.writeFileSync( + logFile, + ['line-one', '[ERROR] boom', 'line-three'].join('\n'), + 'utf8' + ); + + assert.equal(getProjectServerLogDir(root), path.join(root, '.reactpress', 'logs', 'server')); + const files = listLogFiles(getProjectServerLogDir(root), 'error'); + assert.equal(files.length, 1); + assert.equal(readTailLines(logFile, 2).join('|'), '[ERROR] boom|line-three'); + assert.deepEqual(filterLines(readTailLines(logFile, 10), 'ERROR'), ['[ERROR] boom']); + } finally { + rmDir(root); + } + }); + + it('prefers non-empty bundled server logs when project log dir is empty', () => { + const root = createStandaloneProject(); + try { + fs.mkdirSync(path.join(root, 'server', 'src'), { recursive: true }); + fs.writeFileSync(path.join(root, 'server', 'src', 'main.ts'), '// stub'); + + const emptyDir = path.join(root, '.reactpress', 'logs', 'server', 'error'); + fs.mkdirSync(emptyDir, { recursive: true }); + fs.writeFileSync(path.join(emptyDir, 'error.log.-2026-07-11.log'), '', 'utf8'); + + const bundledDir = path.join(root, 'server', 'logs', 'error'); + fs.mkdirSync(bundledDir, { recursive: true }); + fs.writeFileSync( + path.join(bundledDir, 'error.log.-2026-07-11.log'), + '[ERROR] bundled\n', + 'utf8' + ); + + assert.equal(getProjectServerLogDir(root), path.join(root, 'server', 'logs')); + assert.equal(measureLogDir(path.join(root, '.reactpress', 'logs', 'server')), 0); + const files = listLogFiles(getProjectServerLogDir(root), 'error'); + assert.equal(files.length, 1); + assert.equal(readTailLines(files[0].path, 5).join('|'), '[ERROR] bundled'); + } finally { + rmDir(root); + } + }); + + it('lists pm2 process logs from the project log directory', () => { + const root = createStandaloneProject(); + try { + const logDir = path.join(root, '.reactpress', 'logs', 'server'); + fs.mkdirSync(logDir, { recursive: true }); + fs.writeFileSync(path.join(logDir, 'pm2-out.log'), 'API ready on 3002\n', 'utf8'); + fs.writeFileSync(path.join(logDir, 'pm2-error.log'), '[Nest] bootstrap failed\n', 'utf8'); + + const files = listLogFiles(logDir, 'process', root); + assert.equal(files.length, 2); + assert.equal(resolveLogSource(root, 'process'), 'process'); + assert.equal(readTailLines(files.find((f) => f.source === 'pm2-out').path, 5)[0], 'API ready on 3002'); + } finally { + rmDir(root); + } + }); +}); diff --git a/cli/tests/http.test.js b/cli/tests/http.test.js index 8b8d7d04..5b0c6343 100644 --- a/cli/tests/http.test.js +++ b/cli/tests/http.test.js @@ -3,9 +3,11 @@ const assert = require('node:assert/strict'); const { loadServerSiteUrl, loadClientSiteUrl, + loadAdminConsoleUrl, getApiPrefix, getHealthUrl, -} = require('../lib/http'); + normalizeProbeUrl, +} = require('../out/lib/http'); const { createStandaloneProject, rmDir } = require('./helpers/tmp-project'); describe('lib/http', () => { @@ -20,4 +22,30 @@ describe('lib/http', () => { rmDir(root); } }); + + it('normalizes localhost probes to IPv4 loopback', () => { + assert.equal( + normalizeProbeUrl('http://localhost:3002/api/health'), + 'http://127.0.0.1:3002/api/health', + ); + assert.equal( + normalizeProbeUrl('http://[::1]:3001/'), + 'http://127.0.0.1:3001/', + ); + }); + + it('resolves admin console URL for local zero-dependency mode', () => { + const root = createStandaloneProject(); + const prevLocal = process.env.REACTPRESS_LOCAL_MODE; + const prevNginx = process.env.REACTPRESS_SKIP_NGINX; + try { + process.env.REACTPRESS_LOCAL_MODE = '1'; + process.env.REACTPRESS_SKIP_NGINX = '1'; + assert.equal(loadAdminConsoleUrl(root), 'http://localhost:3001/admin/'); + } finally { + process.env.REACTPRESS_LOCAL_MODE = prevLocal; + process.env.REACTPRESS_SKIP_NGINX = prevNginx; + rmDir(root); + } + }); }); diff --git a/cli/tests/i18n.test.js b/cli/tests/i18n.test.js index d6e511d9..b09e73e4 100644 --- a/cli/tests/i18n.test.js +++ b/cli/tests/i18n.test.js @@ -1,6 +1,6 @@ const { describe, it } = require('node:test'); const assert = require('node:assert/strict'); -const { t, setLocale, getLocale } = require('../lib/i18n'); +const { t, setLocale, getLocale } = require('../out/lib/i18n'); describe('lib/i18n', () => { it('translates known keys in en and zh', () => { @@ -17,4 +17,10 @@ describe('lib/i18n', () => { setLocale('en'); assert.match(t('menu.opening', { url: 'http://x' }), /http:\/\/x/); }); + + it('does not throw when key or vars are missing', () => { + setLocale('en'); + assert.equal(t(undefined), ''); + assert.equal(t('dev.timingReady', { summary: undefined }), 'Ready in '); + }); }); diff --git a/cli/tests/nginx.test.js b/cli/tests/nginx.test.js index 540b6bb1..f9f4702c 100644 --- a/cli/tests/nginx.test.js +++ b/cli/tests/nginx.test.js @@ -6,7 +6,8 @@ const { resolveNginxConfigPath, resolveNginxComposeContext, ensureNginxConfig, -} = require('../lib/nginx'); + renderDevNginxConfig, +} = require('../out/lib/nginx'); const { createStandaloneProject, createMonorepoFixture, rmDir } = require('./helpers/tmp-project'); describe('lib/nginx', () => { @@ -39,7 +40,44 @@ describe('lib/nginx', () => { assert.equal(created, true); assert.equal(configPath, target); assert.ok(fs.existsSync(target)); - assert.ok(fs.readFileSync(target, 'utf8').includes('host.docker.internal')); + const content = fs.readFileSync(target, 'utf8'); + assert.ok(content.includes('host.docker.internal')); + assert.ok(content.includes('host.docker.internal:3000/admin/')); + assert.ok(!content.includes(':5173')); + assert.ok(!content.includes('expires 1y')); + } finally { + rmDir(root); + } + }); + + it('renderDevNginxConfig uses local API port by default', () => { + const content = renderDevNginxConfig({ + adminPort: 3000, + visitorPort: 3001, + apiPort: 3002, + }); + assert.ok(content.includes('host.docker.internal:3002')); + }); + + it('ensureNginxConfig refreshes stale prod nginx (13001 → env ports)', () => { + const root = createMonorepoFixture(); + try { + const configPath = path.join(root, 'nginx.conf'); + fs.writeFileSync( + configPath, + 'location / { proxy_pass http://host.docker.internal:13001; }\nlocation /api { proxy_pass http://host.docker.internal:13002; }\n', + ); + fs.writeFileSync( + path.join(root, '.env'), + 'CLIENT_SITE_URL=http://localhost:3001\nSERVER_SITE_URL=http://localhost:3002\n', + ); + const { changed } = ensureNginxConfig(root, { prod: true }); + assert.equal(changed, true); + const content = fs.readFileSync(configPath, 'utf8'); + assert.ok(content.includes('host.docker.internal:3001')); + assert.ok(content.includes('host.docker.internal:3002')); + assert.ok(!content.includes('13001')); + assert.ok(!content.includes('13002')); } finally { rmDir(root); } diff --git a/cli/tests/pack-size.test.js b/cli/tests/pack-size.test.js new file mode 100644 index 00000000..4462786c --- /dev/null +++ b/cli/tests/pack-size.test.js @@ -0,0 +1,52 @@ +'use strict'; + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const { execFileSync, spawnSync } = require('node:child_process'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +const CLI_ROOT = path.join(__dirname, '..'); +const MAX_PACK_BYTES = 10 * 1024 * 1024; + +function packTarball() { + const prepack = spawnSync('npm', ['run', 'prepack'], { cwd: CLI_ROOT, encoding: 'utf8' }); + assert.equal(prepack.status, 0, prepack.stderr || prepack.stdout); + + const output = execFileSync('npm', ['pack', '--ignore-scripts'], { + cwd: CLI_ROOT, + encoding: 'utf8', + maxBuffer: 20 * 1024 * 1024, + }); + const filename = output.trim().split('\n').pop().trim(); + const tarball = path.join(CLI_ROOT, filename); + assert.ok(fs.existsSync(tarball), `missing tarball: ${filename}`); + return { tarball, filename }; +} + +describe('npm pack size', () => { + it('pruned tarball is under 10MB and excludes bundled node_modules', () => { + const { tarball, filename } = packTarball(); + try { + assert.equal(fs.existsSync(path.join(CLI_ROOT, 'server', 'node_modules')), false); + assert.equal(fs.existsSync(path.join(CLI_ROOT, 'toolkit', 'node_modules')), false); + + const size = fs.statSync(tarball).size; + assert.ok( + size < MAX_PACK_BYTES, + `tarball ${filename} is ${(size / 1024 / 1024).toFixed(2)}MB (limit 10MB)`, + ); + + const list = execFileSync('tar', ['-tzf', tarball], { encoding: 'utf8' }); + assert.doesNotMatch(list, /server\/node_modules\//); + assert.doesNotMatch(list, /toolkit\/node_modules\//); + assert.doesNotMatch(list, /server\/public\/uploads\//); + assert.doesNotMatch(list, /server\/\.reactpress\//); + assert.doesNotMatch(list, /\.tsbuildinfo$/); + assert.doesNotMatch(list, /\.map$/); + } finally { + fs.rmSync(tarball, { force: true }); + } + }); +}); diff --git a/cli/tests/parity-pack.test.js b/cli/tests/parity-pack.test.js index 9754d0a8..40d6f8c5 100644 --- a/cli/tests/parity-pack.test.js +++ b/cli/tests/parity-pack.test.js @@ -10,12 +10,12 @@ 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', + 'out/lib/root.js', + 'out/lib/publish.js', + 'out/lib/project-type.js', + 'out/ui/interactive.js', + 'out/ui/banner.js', + 'out/ui/theme.js', 'dist/index.js', 'templates/env.default', 'templates/config.default.json', @@ -36,8 +36,10 @@ describe('publish/local file parity', () => { const top = required.split('/')[0]; assert.ok( declared.has(top) || declared.has(required), - `package.json files[] missing "${top}" (needed for ${required})` + `package.json files[] missing "${top}" (needed for ${required})`, ); } + assert.ok(declared.has('toolkit'), 'package.json files[] must ship bundled toolkit'); + assert.ok(declared.has('plugins'), 'package.json files[] must ship bundled plugins'); }); }); diff --git a/cli/tests/pm2-runtime.test.js b/cli/tests/pm2-runtime.test.js new file mode 100644 index 00000000..1a26a5a7 --- /dev/null +++ b/cli/tests/pm2-runtime.test.js @@ -0,0 +1,49 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + PM2_API_APP, + PM2_CLIENT_APP, + isPm2AppOnline, + getPm2AppPid, + getPm2AppLogPaths, +} = require('../out/lib/pm2-runtime'); + +describe('lib/pm2-runtime', () => { + it('exports internal app names', () => { + assert.equal(PM2_API_APP, 'reactpress-api'); + assert.equal(PM2_CLIENT_APP, 'reactpress-client'); + }); + + it('isPm2AppOnline returns a boolean', () => { + const root = '/tmp/reactpress-no-pm2-project'; + assert.equal(typeof isPm2AppOnline(root, PM2_API_APP), 'boolean'); + }); + + it('exports pm2 log path helpers', () => { + const paths = getPm2AppLogPaths('/tmp/reactpress-no-pm2-project'); + assert.match(paths.out, /\.log$/); + assert.match(paths.err, /\.log$/); + }); +}); + +describe('initLocalProject idempotent', () => { + it('returns ok when config already exists', async () => { + const fs = require('fs-extra'); + const os = require('os'); + const path = require('path'); + const { initLocalProject } = require('../out/core/services/local-site'); + + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'reactpress-init-idempotent-')); + try { + const first = await initLocalProject(root); + assert.equal(first.ok, true); + + const second = await initLocalProject(root); + assert.equal(second.ok, true); + assert.match(second.message, /already initialized|已初始化/i); + } finally { + fs.removeSync(root); + } + }); +}); diff --git a/cli/tests/ports.test.js b/cli/tests/ports.test.js new file mode 100644 index 00000000..eb9c6252 --- /dev/null +++ b/cli/tests/ports.test.js @@ -0,0 +1,67 @@ +const { describe, it, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); + +describe('dev stack ports', () => { + const envBackup = { ...process.env }; + + beforeEach(() => { + delete process.env.REACTPRESS_INSTANCE; + delete process.env.REACTPRESS_DEV_INSTANCE; + delete process.env.SERVER_PORT; + delete process.env.WEB_ADMIN_PORT; + delete process.env.CLIENT_PORT; + delete process.env.REACTPRESS_PREVIEW_PORT; + }); + + afterEach(() => { + process.env = { ...envBackup }; + }); + + it('defaults to standard dev ports', () => { + const { resolveDevStackPorts } = require('../out/lib/ports'); + const ports = resolveDevStackPorts(process.cwd()); + assert.equal(ports.admin, 3000); + assert.equal(ports.visitor, 3001); + assert.equal(ports.api, 3002); + assert.equal(ports.preview, 3003); + }); + + it('offsets ports by REACTPRESS_INSTANCE', () => { + process.env.REACTPRESS_INSTANCE = '2'; + const { resolveDevStackPorts } = require('../out/lib/ports'); + const ports = resolveDevStackPorts(process.cwd()); + assert.equal(ports.admin, 3020); + assert.equal(ports.visitor, 3021); + assert.equal(ports.api, 3022); + assert.equal(ports.preview, 3023); + }); + + it('process.env overrides instance offset', () => { + process.env.REACTPRESS_INSTANCE = '1'; + process.env.SERVER_PORT = '3999'; + const { resolveDevStackPorts } = require('../out/lib/ports'); + const ports = resolveDevStackPorts(process.cwd()); + assert.equal(ports.api, 3999); + assert.equal(ports.admin, 3010); + }); + + it('applyDevStackPortsToEnv sets SERVER_PORT and proxy target', () => { + const { applyDevStackPortsToEnv } = require('../out/lib/ports'); + applyDevStackPortsToEnv({ + admin: 3010, + visitor: 3011, + api: 3012, + preview: 3013, + }); + assert.equal(process.env.SERVER_PORT, '3012'); + assert.equal(process.env.REACTPRESS_LOCAL_API_PORT, '3012'); + assert.equal(process.env.VITE_DEV_API_PROXY_TARGET, 'http://127.0.0.1:3012'); + }); + + it('devSessionSuffix includes instance id', () => { + const { devSessionSuffix } = require('../out/lib/ports'); + assert.equal(devSessionSuffix(), ''); + process.env.REACTPRESS_INSTANCE = '3'; + assert.equal(devSessionSuffix(), '-instance-3'); + }); +}); diff --git a/cli/tests/project-type.test.js b/cli/tests/project-type.test.js index d4982ad9..3606d626 100644 --- a/cli/tests/project-type.test.js +++ b/cli/tests/project-type.test.js @@ -4,7 +4,7 @@ const { detectProjectType, describeProject, hasClient, -} = require('../lib/project-type'); +} = require('../out/lib/project-type'); const { createStandaloneProject, createMonorepoFixture, rmDir } = require('./helpers/tmp-project'); describe('lib/project-type', () => { diff --git a/cli/tests/publish-version.test.js b/cli/tests/publish-version.test.js index e1051a9d..1eb4eafe 100644 --- a/cli/tests/publish-version.test.js +++ b/cli/tests/publish-version.test.js @@ -20,7 +20,7 @@ function incrementVersion(version, type) { case 'beta': { const match = version.match(/^(.*)-beta\.(\d+)$/); if (match) return `${match[1]}-beta.${parseInt(match[2], 10) + 1}`; - return `${version}-beta.1`; + return `${base}-beta.0`; } default: return version; @@ -36,7 +36,11 @@ describe('publish version bump', () => { assert.equal(incrementVersion('3.0', 'patch'), '3.0.1'); }); - it('bumps beta', () => { + it('bumps beta from stable', () => { + assert.equal(incrementVersion('4.0.0', 'beta'), '4.0.0-beta.0'); + }); + + it('bumps beta prerelease', () => { assert.equal(incrementVersion('3.0.0-beta.1', 'beta'), '3.0.0-beta.2'); }); }); diff --git a/cli/tests/remote-dev.test.js b/cli/tests/remote-dev.test.js new file mode 100644 index 00000000..d26810d5 --- /dev/null +++ b/cli/tests/remote-dev.test.js @@ -0,0 +1,98 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { renderDevNginxConfig } = require('../out/lib/nginx'); +const { + normalizeRemoteOrigin, + resolveRemoteThemeApiBase, + parseOriginSpec, + resolveDevApiOrigins, + applyDevApiOriginsToEnv, +} = require('../out/lib/remote-dev'); + +describe('lib/remote-dev', () => { + it('normalizes bare hostnames to https origins', () => { + assert.equal(normalizeRemoteOrigin('api.gaoredu.com'), 'https://api.gaoredu.com'); + assert.equal(normalizeRemoteOrigin('https://api.gaoredu.com/'), 'https://api.gaoredu.com'); + assert.equal(normalizeRemoteOrigin(''), null); + }); + + it('parseOriginSpec supports local, remote, and URL', () => { + assert.deepEqual(parseOriginSpec('local', 'https://api.gaoredu.com'), { url: null }); + assert.deepEqual(parseOriginSpec('remote', 'https://api.gaoredu.com'), { + url: 'https://api.gaoredu.com', + }); + assert.deepEqual(parseOriginSpec('remote', null), { error: 'REMOTE_DEFAULT_REQUIRED' }); + assert.deepEqual(parseOriginSpec('api.gaoredu.com', null), { url: 'https://api.gaoredu.com' }); + }); + + it('resolveDevApiOrigins splits admin and client', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rp-origins-')); + try { + const mixed = resolveDevApiOrigins(root, { + remoteOrigin: 'https://api.gaoredu.com', + adminOrigin: 'local', + clientOrigin: 'remote', + }); + assert.equal(mixed.admin, null); + assert.equal(mixed.client, 'https://api.gaoredu.com'); + assert.equal(mixed.needsLocalApi, true); + + const both = resolveDevApiOrigins(root, { + remoteOrigin: 'api.gaoredu.com', + }); + assert.equal(both.admin, 'https://api.gaoredu.com'); + assert.equal(both.client, 'https://api.gaoredu.com'); + assert.equal(both.needsLocalApi, false); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + it('applyDevApiOriginsToEnv sets per-side env keys', () => { + const keys = [ + 'REACTPRESS_DEV_REMOTE_ORIGIN', + 'REACTPRESS_DEV_ADMIN_API_ORIGIN', + 'REACTPRESS_DEV_CLIENT_API_ORIGIN', + ]; + const prev = Object.fromEntries(keys.map((k) => [k, process.env[k]])); + try { + applyDevApiOriginsToEnv({ + remoteDefault: 'https://api.gaoredu.com', + admin: null, + client: 'https://api.gaoredu.com', + }); + assert.equal(process.env.REACTPRESS_DEV_REMOTE_ORIGIN, 'https://api.gaoredu.com'); + assert.equal(process.env.REACTPRESS_DEV_ADMIN_API_ORIGIN, undefined); + assert.equal(process.env.REACTPRESS_DEV_CLIENT_API_ORIGIN, 'https://api.gaoredu.com'); + } finally { + for (const key of keys) { + if (prev[key] === undefined) delete process.env[key]; + else process.env[key] = prev[key]; + } + } + }); + + it('builds theme API base with /api suffix', () => { + assert.equal( + resolveRemoteThemeApiBase('https://api.gaoredu.com'), + 'https://api.gaoredu.com/api', + ); + }); +}); + +describe('renderDevNginxConfig client /api', () => { + it('proxies /api to remote upstream when clientApiOrigin is set', () => { + const content = renderDevNginxConfig({ + adminPort: 3000, + visitorPort: 3001, + apiPort: 3002, + clientApiOrigin: 'https://api.gaoredu.com', + }); + assert.ok(content.includes('proxy_pass https://api.gaoredu.com')); + assert.ok(content.includes('proxy_set_header Host api.gaoredu.com')); + assert.ok(!content.includes('host.docker.internal:3002')); + }); +}); diff --git a/cli/tests/root.test.js b/cli/tests/root.test.js index 7b9dc672..c1a8c167 100644 --- a/cli/tests/root.test.js +++ b/cli/tests/root.test.js @@ -6,7 +6,7 @@ const { isProjectRoot, isPublishedCliRoot, getMonorepoRoot, -} = require('../lib/root'); +} = require('../out/lib/root'); const { createStandaloneProject, createMonorepoFixture, rmDir } = require('./helpers/tmp-project'); describe('lib/root', () => { diff --git a/cli/tests/server-bundle.test.js b/cli/tests/server-bundle.test.js new file mode 100644 index 00000000..195c641e --- /dev/null +++ b/cli/tests/server-bundle.test.js @@ -0,0 +1,36 @@ +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, '..'); + +describe('server-bundle', () => { + it('ensureBundledServerDeps skips monorepo server projects', async () => { + const { ensureBundledServerDeps } = require('../out/lib/server-bundle'); + const repoRoot = path.join(CLI_ROOT, '..'); + const result = await ensureBundledServerDeps(repoRoot); + assert.equal(result.ok, true); + }); + + it('isBundledServerReady requires toolkit dist and server runtime modules', () => { + const { isBundledServerReady } = require('../out/lib/server-bundle'); + const ready = isBundledServerReady(); + const hasToolkit = fs.existsSync(path.join(CLI_ROOT, 'toolkit', 'dist', 'index.js')); + const hasServerMain = fs.existsSync(path.join(CLI_ROOT, 'server', 'dist', 'main.js')); + if (hasToolkit && hasServerMain) { + assert.equal(typeof ready, 'boolean'); + } else { + assert.equal(ready, false); + } + }); +}); + +describe('install-bundled-runtime script', () => { + it('exits without error in monorepo checkout', () => { + const { spawnSync } = require('child_process'); + const script = path.join(CLI_ROOT, 'scripts', 'install-bundled-runtime.mjs'); + const result = spawnSync(process.execPath, [script], { cwd: CLI_ROOT, encoding: 'utf8' }); + assert.equal(result.status, 0, result.stderr || result.stdout); + }); +}); diff --git a/cli/tests/sqlite-path.test.js b/cli/tests/sqlite-path.test.js new file mode 100644 index 00000000..fa95ead8 --- /dev/null +++ b/cli/tests/sqlite-path.test.js @@ -0,0 +1,36 @@ +const { describe, it, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +describe('sqlite paths', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rp-sqlite-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('defaults to .reactpress/reactpress.db', async () => { + const { getProjectPaths } = require('../out/core/utils/paths'); + const paths = getProjectPaths(tmpDir); + assert.equal(paths.sqlitePath, path.join(tmpDir, '.reactpress', 'reactpress.db')); + }); + + it('migrates legacy data/reactpress.db on resolve', async () => { + const legacyDir = path.join(tmpDir, 'data'); + fs.mkdirSync(legacyDir, { recursive: true }); + fs.writeFileSync(path.join(legacyDir, 'reactpress.db'), 'legacy'); + + const { resolveSqlitePath } = require('../out/core/services/database/sqlite'); + const resolved = await resolveSqlitePath(tmpDir); + const expected = path.join(tmpDir, '.reactpress', 'reactpress.db'); + + assert.equal(resolved, expected); + assert.equal(fs.readFileSync(expected, 'utf8'), 'legacy'); + }); +}); diff --git a/cli/tests/theme-api-proxy-patch.test.js b/cli/tests/theme-api-proxy-patch.test.js new file mode 100644 index 00000000..9f43e554 --- /dev/null +++ b/cli/tests/theme-api-proxy-patch.test.js @@ -0,0 +1,90 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + PATCH_MARKER, + ENCODING_PATCH_MARKER, + patchThemeApiProxyRoute, + patchThemeApiRouteBodyRead, + patchThemeApiProxyEncodingHeaders, + resolveApiRoutePath, + resolveApiProxyPath, +} = require('../out/lib/theme-api-proxy-patch'); + +const STARTER_ROUTE = path.join( + __dirname, + '../../.reactpress/runtime/reactpress-theme-starter/app/api/[[...path]]/route.ts', +); + +describe('lib/theme-api-proxy-patch', () => { + it('resolveApiRoutePath finds App Router catch-all route', () => { + const themeDir = path.dirname(path.dirname(path.dirname(path.dirname(STARTER_ROUTE)))); + assert.ok(resolveApiRoutePath(themeDir)); + }); + + it('resolveApiProxyPath finds theme-starter proxy module', () => { + const themeDir = path.dirname(path.dirname(path.dirname(path.dirname(STARTER_ROUTE)))); + assert.ok(resolveApiProxyPath(themeDir)); + }); + + it('patchThemeApiProxyRoute fixes body read before proxy branch', () => { + const themeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'reactpress-api-proxy-')); + const routeDir = path.join(themeDir, 'app', 'api', '[[...path]]'); + fs.mkdirSync(routeDir, { recursive: true }); + + const buggy = `async function handleRequest(request: Request) { + const keyword = new URL(request.url).searchParams.get('keyword')?.trim() ?? '' + const body = await readJsonBody(request) + + let response: Response + if (isMockApiEnabled()) { + const mockResponse = matchMockRoute(request.method, path, { keyword, body }) + response = mockResponse ?? Response.json({ success: true, data: null }) + } else { + response = await proxyToUpstreamApi(request, path) + } +} +`; + + fs.writeFileSync(path.join(routeDir, 'route.ts'), buggy, 'utf8'); + + assert.equal(patchThemeApiProxyRoute(themeDir), true); + const patched = fs.readFileSync(path.join(routeDir, 'route.ts'), 'utf8'); + assert.match(patched, /if \(isMockApiEnabled\(\)\) \{\n const body = await readJsonBody\(request\)/); + assert.ok(!patched.includes('const body = await readJsonBody(request)\n\n let response')); + assert.ok(fs.existsSync(path.join(themeDir, PATCH_MARKER))); + assert.equal(patchThemeApiRouteBodyRead(themeDir), false); + }); + + it('patchThemeApiProxyEncodingHeaders strips stale Content-Encoding after fetch()', () => { + const themeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'reactpress-api-proxy-enc-')); + const proxyDir = path.join(themeDir, 'lib', 'mock-api'); + fs.mkdirSync(proxyDir, { recursive: true }); + + const buggy = `export async function proxyToUpstreamApi() { + const upstream = await fetch(target) + return new Response(upstream.body, { + status: upstream.status, + headers: upstream.headers, + }) +} +`; + + fs.writeFileSync(path.join(proxyDir, 'proxy.ts'), buggy, 'utf8'); + + assert.equal(patchThemeApiProxyEncodingHeaders(themeDir), true); + const patched = fs.readFileSync(path.join(proxyDir, 'proxy.ts'), 'utf8'); + assert.ok(patched.includes(ENCODING_PATCH_MARKER)); + assert.ok(patched.includes("responseHeaders.delete('content-encoding')")); + assert.equal(patchThemeApiProxyEncodingHeaders(themeDir), false); + }); + + it('leaves already-fixed route.ts unchanged', () => { + if (!fs.existsSync(STARTER_ROUTE)) return; + const themeDir = path.dirname(path.dirname(path.dirname(path.dirname(STARTER_ROUTE)))); + assert.equal(patchThemeApiRouteBodyRead(themeDir), false); + }); +}); diff --git a/cli/tests/theme-catalog.test.js b/cli/tests/theme-catalog.test.js new file mode 100644 index 00000000..35d5af1e --- /dev/null +++ b/cli/tests/theme-catalog.test.js @@ -0,0 +1,13 @@ +const path = require('path'); +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +describe('lib/theme-catalog (re-export)', () => { + it('re-exports theme-registry API', () => { + const catalog = require('../out/lib/theme-catalog'); + const registry = require('../out/lib/theme-registry'); + assert.equal(catalog.OFFICIAL_THEME_STARTER_ID, registry.OFFICIAL_THEME_STARTER_ID); + assert.equal(typeof catalog.readThemeCatalog, 'function'); + assert.equal(typeof catalog.validateBundledThemes, 'function'); + }); +}); diff --git a/cli/tests/theme-dev-watch.test.js b/cli/tests/theme-dev-watch.test.js new file mode 100644 index 00000000..bc3330a2 --- /dev/null +++ b/cli/tests/theme-dev-watch.test.js @@ -0,0 +1,75 @@ +const fs = require('fs'); +const path = require('path'); +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + hasThemePackages, + hasResolvableActiveTheme, + readActiveThemeManifest, + resolveThemeDirectory, + readManifestSignature, +} = require('../out/lib/theme-runtime'); +const { createStandaloneProject, rmDir } = require('./helpers/tmp-project'); + +const HELLO_WORLD = path.join(__dirname, '../../themes/hello-world'); + +describe('theme dev watcher prerequisites', () => { + it('detects theme packages even when active theme is missing on disk', () => { + const root = createStandaloneProject(); + try { + const runtimeHello = path.join(root, '.reactpress', 'runtime', 'hello-world'); + fs.mkdirSync(path.dirname(runtimeHello), { recursive: true }); + fs.cpSync(HELLO_WORLD, runtimeHello, { recursive: true }); + + const manifestPath = path.join(root, '.reactpress', 'active-theme.json'); + fs.writeFileSync( + manifestPath, + JSON.stringify({ activeTheme: 'missing-theme', themeDir: null }, null, 2), + ); + + assert.equal(hasThemePackages(root), true); + assert.equal(hasResolvableActiveTheme(root), false); + assert.equal(readManifestSignature(root), ''); + assert.ok(resolveThemeDirectory(root, 'hello-world')); + } finally { + rmDir(root); + } + }); + + it('manifest signature updates when active theme becomes resolvable', () => { + const root = createStandaloneProject(); + try { + const runtimeHello = path.join(root, '.reactpress', 'runtime', 'hello-world'); + fs.mkdirSync(path.dirname(runtimeHello), { recursive: true }); + fs.cpSync(HELLO_WORLD, runtimeHello, { recursive: true }); + + const manifestPath = path.join(root, '.reactpress', 'active-theme.json'); + fs.writeFileSync( + manifestPath, + JSON.stringify({ activeTheme: 'missing-theme' }, null, 2), + ); + assert.equal(readManifestSignature(root), ''); + + fs.writeFileSync( + manifestPath, + JSON.stringify( + { + activeTheme: 'hello-world', + themeDir: '.reactpress/runtime/hello-world', + updatedAt: new Date().toISOString(), + }, + null, + 2, + ), + ); + + const signature = readManifestSignature(root); + assert.match(signature, /^hello-world:/); + assert.equal(readActiveThemeManifest(root).activeTheme, 'hello-world'); + assert.equal(hasResolvableActiveTheme(root), true); + } finally { + rmDir(root); + } + }); +}); diff --git a/cli/tests/theme-install.test.js b/cli/tests/theme-install.test.js new file mode 100644 index 00000000..576a727b --- /dev/null +++ b/cli/tests/theme-install.test.js @@ -0,0 +1,63 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { spawnSync } = require('child_process'); +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + parseNpmSpec, + resolveThemeIdentity, + installThemeFromNpm, +} = require('../out/lib/theme-install'); +const { readThemeLock } = require('../out/lib/theme-lock'); +const { createStandaloneProject, rmDir } = require('./helpers/tmp-project'); + +const HELLO_WORLD_THEME = path.join(__dirname, '../../themes/hello-world'); + +describe('lib/theme-install', () => { + it('parseNpmSpec accepts npm specs and tarball paths', () => { + assert.deepEqual(parseNpmSpec('@fecommunity/reactpress-template-hello-world@3.0.4'), { + kind: 'npm', + spec: '@fecommunity/reactpress-template-hello-world@3.0.4', + }); + assert.equal(parseNpmSpec('').error, 'EMPTY_SPEC'); + }); + + it('resolveThemeIdentity reads theme.json id', () => { + const identity = resolveThemeIdentity(HELLO_WORLD_THEME); + assert.equal(identity?.themeId, 'hello-world'); + assert.equal(identity?.manifest?.name, 'Hello World'); + }); + + it('installThemeFromNpm materializes a local npm pack into runtime', async () => { + const root = createStandaloneProject(); + const packDir = fs.mkdtempSync(path.join(os.tmpdir(), 'reactpress-pack-')); + try { + const packResult = spawnSync( + 'npm', + ['pack', HELLO_WORLD_THEME, '--pack-destination', packDir], + { encoding: 'utf8', shell: process.platform === 'win32' }, + ); + assert.equal(packResult.status, 0, packResult.stderr || packResult.stdout); + + const tarball = fs + .readdirSync(packDir) + .find((name) => name.endsWith('.tgz')); + assert.ok(tarball, 'npm pack should produce a tarball'); + + const result = await installThemeFromNpm(root, path.join(packDir, tarball), { + skipDependencies: true, + }); + assert.equal(result.themeId, 'hello-world'); + assert.ok(fs.existsSync(path.join(root, '.reactpress', 'runtime', 'hello-world', 'theme.json'))); + + const lock = readThemeLock(root); + assert.equal(lock.themes['hello-world']?.source, 'npm'); + assert.match(lock.themes['hello-world']?.spec ?? '', /\.tgz$/); + } finally { + rmDir(root); + rmDir(packDir); + } + }); +}); diff --git a/cli/tests/theme-paths.test.js b/cli/tests/theme-paths.test.js new file mode 100644 index 00000000..08e9f3fe --- /dev/null +++ b/cli/tests/theme-paths.test.js @@ -0,0 +1,27 @@ +const path = require('path'); +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + THEME_RUNTIME_REL, + THEMES_CATALOG_REL, + PREVIEW_POOL_PORTS, + PREVIEW_PROXY_PORT, + getPreviewBackendPorts, + themesRoot, +} = require('../out/lib/theme-paths'); + +describe('lib/theme-paths', () => { + it('exports stable relative paths', () => { + assert.equal(THEME_RUNTIME_REL, '.reactpress/runtime'); + assert.equal(THEMES_CATALOG_REL, 'themes/catalog.json'); + assert.deepEqual(PREVIEW_POOL_PORTS, [3003]); + assert.equal(PREVIEW_PROXY_PORT, 3003); + assert.deepEqual(getPreviewBackendPorts(), [3004, 3005, 3006]); + }); + + it('themesRoot resolves under project root', () => { + const root = path.join(__dirname, '../..'); + assert.equal(themesRoot(root), path.join(root, 'themes')); + }); +}); diff --git a/cli/tests/theme-preview-frame.test.js b/cli/tests/theme-preview-frame.test.js new file mode 100644 index 00000000..33d2786a --- /dev/null +++ b/cli/tests/theme-preview-frame.test.js @@ -0,0 +1,93 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + ensurePreviewFrameAllowed, + stripBakedFrameOptionsFromBuild, + shouldHonorThemePreviewFrame, + PATCH_MARKER, +} = require('../out/lib/theme-preview-frame'); + +describe('lib/theme-preview-frame', () => { + it('patches next.config.js to skip X-Frame-Options during admin preview', () => { + const themeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'reactpress-frame-')); + try { + fs.writeFileSync( + path.join(themeDir, 'next.config.js'), + `module.exports = { + async headers() { + return [{ + source: '/:path*', + headers: [ + { key: 'X-Content-Type-Options', value: 'nosniff' }, + { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, + ], + }]; + }, +};`, + ); + + assert.equal(ensurePreviewFrameAllowed(themeDir), true); + assert.ok(fs.existsSync(path.join(themeDir, PATCH_MARKER))); + + const patched = fs.readFileSync(path.join(themeDir, 'next.config.js'), 'utf8'); + assert.match(patched, /REACTPRESS_HONOR_PREVIEW === '1'/); + assert.match(patched, /\?\s*\[\]\s*:\s*\[\{ key: 'X-Frame-Options'/); + + assert.equal(ensurePreviewFrameAllowed(themeDir), true); + } finally { + fs.rmSync(themeDir, { recursive: true, force: true }); + } + }); + + it('strips baked X-Frame-Options from routes-manifest.json', () => { + const themeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'reactpress-frame-manifest-')); + try { + const distDir = '.next-preview'; + const manifestDir = path.join(themeDir, distDir); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync( + path.join(manifestDir, 'routes-manifest.json'), + `${JSON.stringify( + { + headers: [ + { + source: '/:path*', + headers: [ + { key: 'X-Content-Type-Options', value: 'nosniff' }, + { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, + ], + }, + ], + }, + null, + 2, + )}\n`, + ); + + assert.equal(stripBakedFrameOptionsFromBuild(themeDir, distDir), true); + const parsed = JSON.parse( + fs.readFileSync(path.join(manifestDir, 'routes-manifest.json'), 'utf8'), + ); + const keys = parsed.headers[0].headers.map((h) => h.key); + assert.deepEqual(keys, ['X-Content-Type-Options']); + assert.equal(stripBakedFrameOptionsFromBuild(themeDir, distDir), false); + } finally { + fs.rmSync(themeDir, { recursive: true, force: true }); + } + }); + + it('detects desktop local preview frame mode', () => { + const prev = process.env.REACTPRESS_DESKTOP_LOCAL; + process.env.REACTPRESS_DESKTOP_LOCAL = '1'; + try { + assert.equal(shouldHonorThemePreviewFrame(), true); + } finally { + if (prev === undefined) delete process.env.REACTPRESS_DESKTOP_LOCAL; + else process.env.REACTPRESS_DESKTOP_LOCAL = prev; + } + }); +}); diff --git a/cli/tests/theme-preview-pool.test.js b/cli/tests/theme-preview-pool.test.js new file mode 100644 index 00000000..13b7ee9a --- /dev/null +++ b/cli/tests/theme-preview-pool.test.js @@ -0,0 +1,90 @@ +const path = require('path'); +const fs = require('fs'); +const os = require('os'); +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { resolvePreviewThemeLaunchPlan } = require('../out/lib/theme-preview-pool'); + +const HELLO_WORLD = path.join(__dirname, '../../themes/hello-world'); +const STARTER = path.join(__dirname, '../../.reactpress/runtime/reactpress-theme-starter'); + +describe('lib/theme-preview-pool launch plan', () => { + it('prefers dev script for hello-world outside integrated desktop dev', () => { + const prev = process.env.REACTPRESS_DESKTOP_LOCAL; + delete process.env.REACTPRESS_DESKTOP_LOCAL; + delete process.env.REACTPRESS_DESKTOP_SITE_ROOT; + try { + const plan = resolvePreviewThemeLaunchPlan(HELLO_WORLD, 3003); + assert.equal(plan.mode, 'dev'); + assert.equal(plan.cmd, 'pnpm'); + assert.deepEqual(plan.args, ['run', 'dev', '--', '--port', '3003']); + } finally { + if (prev === undefined) delete process.env.REACTPRESS_DESKTOP_LOCAL; + else process.env.REACTPRESS_DESKTOP_LOCAL = prev; + } + }); + + it('uses production next start for hello-world in dev:web:local stack', () => { + const prevLocal = process.env.REACTPRESS_DESKTOP_LOCAL; + const prevSite = process.env.REACTPRESS_DESKTOP_SITE_ROOT; + process.env.REACTPRESS_DESKTOP_LOCAL = '1'; + try { + const plan = resolvePreviewThemeLaunchPlan(HELLO_WORLD, 3004); + assert.equal(plan.mode, 'production'); + assert.equal(plan.cmd, process.execPath); + assert.match(plan.args.join(' '), /next start -p 3004/); + } finally { + if (prevLocal === undefined) delete process.env.REACTPRESS_DESKTOP_LOCAL; + else process.env.REACTPRESS_DESKTOP_LOCAL = prevLocal; + if (prevSite === undefined) delete process.env.REACTPRESS_DESKTOP_SITE_ROOT; + else process.env.REACTPRESS_DESKTOP_SITE_ROOT = prevSite; + } + }); + + it('uses shared next when packaged theme has no local node_modules', () => { + const prevLocal = process.env.REACTPRESS_DESKTOP_LOCAL; + const prevPath = process.env.NODE_PATH; + const prevRoot = process.env.REACTPRESS_MONOREPO_ROOT; + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rp-theme-packaged-')); + process.env.REACTPRESS_DESKTOP_LOCAL = '1'; + process.env.NODE_PATH = path.join(HELLO_WORLD, 'node_modules'); + process.env.REACTPRESS_MONOREPO_ROOT = path.join(__dirname, '../..'); + fs.copyFileSync(path.join(HELLO_WORLD, 'package.json'), path.join(tmpDir, 'package.json')); + fs.copyFileSync(path.join(HELLO_WORLD, 'server.js'), path.join(tmpDir, 'server.js')); + try { + const plan = resolvePreviewThemeLaunchPlan(tmpDir, 3005); + assert.equal(plan.mode, 'production'); + assert.equal(plan.cmd, process.execPath); + assert.match(plan.args.join(' '), /next start -p 3005/); + assert.doesNotMatch(plan.args.join(' '), /server\.js/); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + if (prevLocal === undefined) delete process.env.REACTPRESS_DESKTOP_LOCAL; + else process.env.REACTPRESS_DESKTOP_LOCAL = prevLocal; + if (prevPath === undefined) delete process.env.NODE_PATH; + else process.env.NODE_PATH = prevPath; + if (prevRoot === undefined) delete process.env.REACTPRESS_MONOREPO_ROOT; + else process.env.REACTPRESS_MONOREPO_ROOT = prevRoot; + } + }); + + it('uses production launch for App Router reactpress-theme-starter (fast switch when pre-built)', () => { + const plan = resolvePreviewThemeLaunchPlan(STARTER, 3003); + assert.equal(plan.mode, 'production'); + assert.equal(plan.cmd, process.execPath); + assert.match(plan.args.join(' '), /next/); + }); + + it('allows admin iframe when REACTPRESS_DESKTOP_LOCAL is set', () => { + const { shouldHonorThemePreviewFrame } = require('../out/lib/theme-preview-pool'); + const prev = process.env.REACTPRESS_DESKTOP_LOCAL; + process.env.REACTPRESS_DESKTOP_LOCAL = '1'; + try { + assert.equal(shouldHonorThemePreviewFrame(), true); + } finally { + if (prev === undefined) delete process.env.REACTPRESS_DESKTOP_LOCAL; + else process.env.REACTPRESS_DESKTOP_LOCAL = prev; + } + }); +}); diff --git a/cli/tests/theme-registry.test.js b/cli/tests/theme-registry.test.js new file mode 100644 index 00000000..f19d2c45 --- /dev/null +++ b/cli/tests/theme-registry.test.js @@ -0,0 +1,89 @@ +const path = require('path'); +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + OFFICIAL_THEME_STARTER_ID, + OFFICIAL_THEME_STARTER_SPEC, + readThemeCatalog, + readThemesPackageMeta, + readThemesRegistryMeta, + readThemeSources, + resolveCatalogInstallSpec, + validateLocalThemes, + validateNpmThemes, + validateBundledThemes, + validateCatalogThemes, + catalogEntryToManifest, +} = require('../out/lib/theme-registry'); + +const REPO_ROOT = path.join(__dirname, '../..'); + +describe('lib/theme-registry', () => { + it('reads official theme-starter from themes/theme-starter/package.json anchor', () => { + const catalog = readThemeCatalog(REPO_ROOT); + const starter = catalog.themes.find((entry) => entry.id === OFFICIAL_THEME_STARTER_ID); + assert.ok(starter); + assert.equal(starter.npm, OFFICIAL_THEME_STARTER_SPEC); + assert.deepEqual(starter.dependency, { + name: '@fecommunity/reactpress-theme-starter', + version: '1.0.0-beta.0', + }); + assert.equal(starter.featured, true); + assert.equal(starter.dir, 'theme-starter'); + assert.equal(starter.previewUrl, 'https://reactpress-theme-starter.vercel.app'); + assert.equal(catalog.source, 'themes/package.json'); + }); + + it('resolveCatalogInstallSpec maps catalog id to npm spec', () => { + assert.equal( + resolveCatalogInstallSpec(REPO_ROOT, OFFICIAL_THEME_STARTER_ID), + OFFICIAL_THEME_STARTER_SPEC, + ); + }); + + it('readThemesRegistryMeta lists local ids and npm anchor dirs', () => { + const meta = readThemesRegistryMeta(REPO_ROOT); + assert.ok(meta.local.includes('hello-world')); + assert.ok(meta.npm.includes('theme-starter')); + }); + + it('readThemesPackageMeta keeps bundled/catalog aliases for legacy callers', () => { + const meta = readThemesPackageMeta(REPO_ROOT); + assert.ok(meta.bundled.includes('hello-world')); + assert.ok(meta.local.includes('hello-world')); + }); + + it('readThemeSources exposes local and npm kinds', () => { + const sources = readThemeSources(REPO_ROOT); + assert.ok(sources.local.some((entry) => entry.id === 'hello-world' && entry.kind === 'local')); + assert.ok( + sources.npm.some((entry) => entry.id === OFFICIAL_THEME_STARTER_ID && entry.kind === 'npm'), + ); + }); + + it('catalogEntryToManifest maps catalog metadata', () => { + const entry = readThemeCatalog(REPO_ROOT).themes[0]; + const manifest = catalogEntryToManifest(entry); + assert.equal(manifest.id, entry.id); + assert.equal(manifest.name, entry.name); + }); + + it('validateLocalThemes reports missing template dirs', () => { + const { local, missing } = validateLocalThemes(REPO_ROOT); + assert.ok(local.includes('hello-world')); + assert.equal(missing.length, 0); + }); + + it('validateNpmThemes accepts theme-starter anchor package.json', () => { + const { missing } = validateNpmThemes(REPO_ROOT); + assert.equal(missing.length, 0); + }); + + it('validateBundledThemes and validateCatalogThemes remain as aliases', () => { + const bundled = validateBundledThemes(REPO_ROOT); + const catalog = validateCatalogThemes(REPO_ROOT); + assert.equal(bundled.missing.length, 0); + assert.equal(catalog.missing.length, 0); + }); +}); diff --git a/cli/tests/theme-warmup.test.js b/cli/tests/theme-warmup.test.js new file mode 100644 index 00000000..d27a5e14 --- /dev/null +++ b/cli/tests/theme-warmup.test.js @@ -0,0 +1,55 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('path'); +const { pageFileToRoute, collectWarmupRoutes, isWarmupSafeRoute } = require('../out/lib/theme-warmup'); +const { createMonorepoFixture, rmDir } = require('./helpers/tmp-project'); + +describe('lib/theme-warmup', () => { + it('filters dynamic and admin routes from warmup', () => { + assert.equal(isWarmupSafeRoute('/'), true); + assert.equal(isWarmupSafeRoute('/archives'), true); + assert.equal(isWarmupSafeRoute('/tag/__reactpress_dev_warmup__'), false); + assert.equal(isWarmupSafeRoute('/admin/article'), false); + }); + + it('maps template files to warmup routes', () => { + assert.equal(pageFileToRoute('pages/index.tsx'), '/'); + assert.equal(pageFileToRoute('pages/about.tsx'), '/about'); + assert.equal(pageFileToRoute('pages/tag/[tag].tsx'), '/tag/__reactpress_dev_warmup__'); + assert.equal(pageFileToRoute('pages/category/[category].tsx'), '/category/__reactpress_dev_warmup__'); + assert.equal(pageFileToRoute('pages/article/[id].tsx'), '/article/__reactpress_dev_warmup__'); + }); + + it('collects routes from theme.json templates', () => { + const root = createMonorepoFixture(); + try { + const themeDir = path.join(root, 'themes', 'demo-theme'); + const pagesDir = path.join(themeDir, 'pages'); + require('fs').mkdirSync(path.join(pagesDir, 'tag'), { recursive: true }); + require('fs').writeFileSync( + path.join(themeDir, 'theme.json'), + JSON.stringify({ + id: 'demo-theme', + reactpress: { + templates: { + home: 'pages/index.tsx', + 'archive-tag': 'pages/tag/[tag].tsx', + search: 'pages/search.tsx', + }, + }, + }), + ); + require('fs').writeFileSync(path.join(pagesDir, 'index.tsx'), ''); + require('fs').writeFileSync(path.join(pagesDir, 'search.tsx'), ''); + require('fs').writeFileSync(path.join(pagesDir, 'tag', '[tag].tsx'), ''); + + const routes = collectWarmupRoutes(themeDir); + assert.ok(routes.includes('/')); + assert.ok(routes.includes('/search')); + assert.ok(!routes.some((r) => r.includes('__reactpress_dev_warmup__'))); + assert.ok(routes.includes('/404')); + } finally { + rmDir(root); + } + }); +}); diff --git a/cli/tests/toolkit-build.test.js b/cli/tests/toolkit-build.test.js new file mode 100644 index 00000000..2e330dce --- /dev/null +++ b/cli/tests/toolkit-build.test.js @@ -0,0 +1,50 @@ +const fs = require('fs'); +const path = require('path'); +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const { shouldBuildToolkit } = require('../out/lib/toolkit-build'); +const { createMonorepoFixture, rmDir } = require('./helpers/tmp-project'); + +describe('lib/toolkit-build', () => { + it('requires build when dist is missing', () => { + const root = createMonorepoFixture(); + try { + assert.equal(shouldBuildToolkit(root), true); + } finally { + rmDir(root); + } + }); + + it('skips build when dist is newer than src', () => { + const root = createMonorepoFixture(); + try { + const toolkitDir = path.join(root, 'toolkit'); + const distDir = path.join(toolkitDir, 'dist'); + fs.mkdirSync(distDir, { recursive: true }); + fs.writeFileSync(path.join(distDir, 'index.js'), 'module.exports = {};\n'); + const srcDir = path.join(toolkitDir, 'src'); + fs.mkdirSync(srcDir, { recursive: true }); + fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export {};\n'); + const past = new Date(Date.now() - 60_000); + fs.utimesSync(path.join(srcDir, 'index.ts'), past, past); + const future = new Date(Date.now() + 60_000); + fs.utimesSync(path.join(distDir, 'index.js'), future, future); + assert.equal(shouldBuildToolkit(root), false); + } finally { + rmDir(root); + } + }); + + it('honors REACTPRESS_SKIP_TOOLKIT_BUILD', () => { + const root = createMonorepoFixture(); + const prev = process.env.REACTPRESS_SKIP_TOOLKIT_BUILD; + process.env.REACTPRESS_SKIP_TOOLKIT_BUILD = '1'; + try { + assert.equal(shouldBuildToolkit(root), false); + } finally { + if (prev === undefined) delete process.env.REACTPRESS_SKIP_TOOLKIT_BUILD; + else process.env.REACTPRESS_SKIP_TOOLKIT_BUILD = prev; + rmDir(root); + } + }); +}); diff --git a/cli/tsconfig.json b/cli/tsconfig.json new file mode 100644 index 00000000..7b0f13c8 --- /dev/null +++ b/cli/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node", + "moduleDetection": "force", + "outDir": "out", + "rootDir": "src", + "strict": false, + "noImplicitAny": false, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "out", "dist", "server", "tests"] +} diff --git a/cli/ui/banner.js b/cli/ui/banner.js deleted file mode 100644 index c4fa9f77..00000000 --- a/cli/ui/banner.js +++ /dev/null @@ -1,441 +0,0 @@ -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'); - -/** - * "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('▌▍▎')} ${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.muted('↗')} ${hyperlink(REPO_URL, brand.accent.underline(REPO_URL))}` - ); - console.log(''); -} - -/** - * 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 deleted file mode 100644 index 282a2b08..00000000 --- a/cli/ui/interactive.js +++ /dev/null @@ -1,380 +0,0 @@ -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, - 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, loadServerSiteUrl, isHttpResponding } = require('../lib/http'); -const { isDockerRunning } = require('../lib/docker'); -const { t } = require('../lib/i18n'); - -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(), - 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 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': { - const result = await withSpinner(t('menu.initProject'), () => - ensureProjectEnvironment(projectRoot) - ); - console.log(ok(result.message || t('menu.done'))); - return true; - } - case 'status': - await printUnifiedStatus(projectRoot); - return true; - case 'doctor': { - const code = await runDoctor(projectRoot); - if (code !== 0) process.exit(code); - return true; - } - case 'dev:api': - await runApiDev(projectRoot); - return false; - case 'dev:client': - await runNodeScript(getClientBin(), [], { cwd: projectRoot }); - return false; - case 'server:start': { - const code = await withSpinner(t('menu.startingApi'), () => - runLifecycleCommand('start', projectRoot) - ); - if (code !== 0) process.exit(code); - return true; - } - case 'server:stop': - await withSpinner(t('menu.stoppingApi'), async () => { - await runLifecycleCommand('stop', projectRoot); - }); - return true; - case 'server:restart': { - const code = await withSpinner(t('menu.restartingApi'), () => - runLifecycleCommand('restart', projectRoot) - ); - if (code !== 0) process.exit(code); - return true; - } - case 'build': { - const buildChoices = TARGETS.map((target) => ({ - name: - target === 'all' - ? t('menu.buildAll') - : t(`build.label.${target}`), - value: target, - })); - const { target } = await inquirer.prompt([ - { - type: 'list', - name: 'target', - message: t('menu.buildTarget'), - pageSize: 12, - choices: buildChoices, - }, - ]); - await runBuild(target, projectRoot); - return true; - } - case 'docker:start': - await runDockerCommand('start', projectRoot); - return false; - case 'docker:up': - await withSpinner(t('docker.starting'), () => runDockerCommand('up', projectRoot)); - return true; - case 'docker:stop': - 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); - console.log(label(t('menu.opening', { url }))); - await open(url); - return true; - } - case 'publish': { - const prev = process.argv.slice(); - process.argv = [process.argv[0], process.argv[1], '--publish']; - await require('../lib/publish').main(); - process.argv = prev; - return true; - } - case 'exit': - return false; - default: - return true; - } -} - -async function runInteractiveLoop() { - const projectRoot = ensureOriginalCwd(); - const project = describeProject(projectRoot); - - printBanner({ projectRoot, project }); - await printContextStatus(projectRoot); - console.log(` ${brand.dim(t('menu.shortcuts'))}`); - console.log(''); - - let loop = true; - while (loop) { - const { action } = await inquirer.prompt([ - { - type: 'list', - name: 'action', - message: `${brand.primary(t('menu.actionPrefix'))} ${brand.dim('›')}`, - pageSize: 20, - loop: false, - choices: getMenuActions(project), - }, - ]); - - if (action === 'exit') { - console.log(brand.muted(t('menu.goodbye'))); - break; - } - - try { - const stay = await runMenuAction(action, projectRoot, project); - if (!stay) break; - - if (action !== 'status' && action !== 'doctor') { - console.log(''); - await printContextStatus(projectRoot); - } - } catch (err) { - console.error(fail(err.message || err)); - const { retry } = await inquirer.prompt([ - { - type: 'confirm', - name: 'retry', - message: t('menu.retry'), - default: true, - }, - ]); - loop = retry; - if (loop) { - console.log(''); - await printContextStatus(projectRoot); - } - } - } -} - -module.exports = { runInteractiveLoop, runMenuAction, getMenuActions }; diff --git a/cli/ui/theme.js b/cli/ui/theme.js deleted file mode 100644 index 7b06f7ab..00000000 --- a/cli/ui/theme.js +++ /dev/null @@ -1,265 +0,0 @@ -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(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 `${icon.arrow} ${brand.primary(text)}`; -} - -function ok(text) { - return `${icon.ok} ${brand.success(text)}`; -} - -function fail(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)}`; -} - -/** - * 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/client/Dockerfile b/client/Dockerfile deleted file mode 100644 index 2e70ebfc..00000000 --- a/client/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -# Use Node.js 18 as the base image -FROM node:18-alpine - -# Set working directory -WORKDIR /app - -# Install pnpm globally -RUN npm install -g pnpm - -# Copy ALL files from the project root -COPY . . - -# Debug: Show what files were copied -RUN echo "=== Files in /app ===" && ls -la -RUN echo "=== pnpm-lock.yaml exists? ===" && test -f pnpm-lock.yaml && echo "YES" || echo "NO" -RUN echo "=== pnpm-workspace.yaml exists? ===" && test -f pnpm-workspace.yaml && echo "YES" || echo "NO" -RUN echo "=== client/package.json exists? ===" && test -f client/package.json && echo "YES" || echo "NO" - -# Install dependencies - ALWAYS use --no-frozen-lockfile to avoid issues -RUN pnpm install --no-frozen-lockfile - -# Build the client application -WORKDIR /app/client -RUN pnpm run build - -# Expose port -EXPOSE 3001 - -# Create a non-root user -RUN addgroup -g 1001 -S nodejs && \ - adduser -S nextjs -u 1001 - -# Change ownership of the app directory -RUN chown -R nextjs:nodejs /app -USER nextjs - -# Start the application -CMD ["pnpm", "run", "start"] \ No newline at end of file diff --git a/client/README.md b/client/README.md deleted file mode 100644 index 154717ef..00000000 --- a/client/README.md +++ /dev/null @@ -1,341 +0,0 @@ -# @fecommunity/reactpress-client - -ReactPress Client - Next.js 14 frontend for ReactPress CMS with modern UI and responsive design. - -[![NPM Version](https://img.shields.io/npm/v/@fecommunity/reactpress-client.svg)](https://www.npmjs.com/package/@fecommunity/reactpress-client) -[![License](https://img.shields.io/npm/l/@fecommunity/reactpress-client.svg)](https://github.com/fecommunity/reactpress/blob/master/client/LICENSE) -[![Node Version](https://img.shields.io/node/v/@fecommunity/reactpress-client.svg)](https://nodejs.org) -[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) -[![Next.js](https://img.shields.io/badge/Next.js-14-black)](https://nextjs.org/) - -## Overview - -ReactPress Client is a responsive frontend application built with Next.js 14 that serves as the user interface for the ReactPress CMS platform. It provides a clean design, intuitive navigation, and content management capabilities. - -The client is designed with a component-based architecture that promotes reusability and maintainability. It integrates with the ReactPress backend through the [ReactPress Toolkit](../toolkit), providing type-safe API interactions. - -## Quick Start - -### Installation & Setup - -```bash -# Regular startup -npx @fecommunity/reactpress-client - -# PM2 startup for production -npx @fecommunity/reactpress-client --pm2 -``` - -## Features - -- ⚡ **App Router Architecture** with Server Components for optimal SSR -- 🎨 **Theme System** with light/dark mode switching -- 🌍 **Internationalization** - Supports Chinese and English languages -- 🌙 **Theme Switching** with system preference detection -- ✍️ **Markdown Editor** with live preview -- 📊 **Analytics Dashboard** with metrics and visualizations -- 🔍 **Search** with filtering -- 🖼️ **Media Management** with drag-and-drop upload -- 📱 **PWA Support** with offline capabilities -- ♿ **Accessibility Compliance** - WCAG 2.1 AA standards -- 🚀 **Performance Optimized** - Code splitting, image optimization, and caching - -## Requirements - -- Node.js >= 18.20.4 -- npm or pnpm package manager -- ReactPress Server running (for API connectivity) - -## Usage Scenarios - -### Standalone Client -Perfect for: -- Connecting to remote ReactPress API -- Headless CMS implementation -- Custom deployment scenarios -- Microfrontend architecture - -### Full ReactPress Stack -Use with ReactPress API for complete CMS solution: -```bash -# Start API first -pnpm exec reactpress-cli start - -# In another terminal, start client -npx @fecommunity/reactpress-client -``` - -## Core Components - -ReactPress Client includes a comprehensive set of UI components: - -- **Admin Dashboard** - Content management interface with role-based access -- **Article Editor** - Advanced markdown editor with media embedding -- **Comment System** - Moderation tools with spam detection -- **Media Library** - File management -- **User Management** - Account and profile settings with 2FA -- **Analytics Views** - Data visualization components with export capabilities -- **Theme Switcher** - Light/dark mode toggle with system preference detection -- **Language Selector** - Internationalization controls with RTL support - -## PM2 Support - -ReactPress client supports PM2 process management for production deployments: - -```bash -# Start with PM2 -npx @fecommunity/reactpress-client --pm2 -``` - -PM2 features: -- Automatic process restart on crash -- Memory monitoring -- Log management with rotation -- Process management -- Health checks - -## Configuration - -The client connects to the ReactPress server via environment variables: - -```env -# Server API URL -SERVER_API_URL=https://api.yourdomain.com - -# Client URL -CLIENT_URL=https://yourdomain.com -CLIENT_PORT=3001 - -# Analytics -GOOGLE_ANALYTICS_ID=your_ga_id - -# Security -NEXT_PUBLIC_CRYPTO_KEY=your_encryption_key -``` - -## Development - -```bash -# Clone repository -git clone https://github.com/fecommunity/reactpress.git -cd reactpress/client - -# Install dependencies -pnpm install - -# Start development server with hot reload -pnpm run dev - -# Start with PM2 (development) -pnpm run pm2 - -# Build for production -pnpm run build - -# Start production server -pnpm run start -``` - -## Project Structure - -``` -client/ -├── app/ # Next.js 14 App Router -│ ├── (admin)/ # Admin dashboard routes -│ ├── (public)/ # Public facing routes -│ └── api/ # API routes -├── components/ # Reusable UI components -├── lib/ # Business logic and utilities -├── providers/ # React context providers -├── hooks/ # Custom React hooks -├── styles/ # Global styles and design tokens -├── public/ # Static assets -└── bin/ # CLI entry points -``` - -## Environment Variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `SERVER_API_URL` | ReactPress server API URL | `http://localhost:3002` | -| `CLIENT_URL` | Client site URL | `http://localhost:3001` | -| `CLIENT_PORT` | Client port | `3001` | -| `NEXT_PUBLIC_GA_ID` | Google Analytics ID | - | -| `NEXT_PUBLIC_SITE_TITLE` | Site title | `ReactPress` | -| `NEXT_PUBLIC_CRYPTO_KEY` | Encryption key for sensitive data | - | - -## CLI Commands - -```bash -# Show help -npx @fecommunity/reactpress-client --help - -# Start client -npx @fecommunity/reactpress-client - -# Start with PM2 -npx @fecommunity/reactpress-client --pm2 - -# Specify port -npx @fecommunity/reactpress-client --port 3001 - -# Enable verbose logging -npx @fecommunity/reactpress-client --verbose -``` - -## Integration with ReactPress Toolkit - -The client seamlessly integrates with the ReactPress Toolkit for API interactions: - -```typescript -import { api, types } from '@fecommunity/reactpress-toolkit'; - -// Fetch articles with proper typing -const articles: types.IArticle[] = await api.article.findAll(); - -// Create new article -const newArticle = await api.article.create({ - title: 'My New Article', - content: 'Article content here...', - // ... other properties -}); -``` - -The toolkit provides: -- Strongly-typed API clients for all modules -- TypeScript definitions for all data models -- Utility functions for common operations -- Built-in authentication and error handling -- Automatic retry mechanisms for failed requests - -## Theme Customization - -ReactPress Client supports advanced theme customization: - -### Design Token System -```typescript -// Custom theme tokens -const customTokens = { - colors: { - primary: '#0070f3', - secondary: '#7928ca', - background: '#ffffff', - text: '#000000' - }, - typography: { - fontFamily: 'Inter, sans-serif', - fontSize: { - small: '12px', - medium: '16px', - large: '20px' - } - } -}; -``` - -### Component-Level Customization -```typescript -// Extend existing components -import { Button } from '@fecommunity/reactpress-components'; - -const CustomButton = styled(Button)` - background-color: ${props => props.theme.colors.primary}; - border-radius: 8px; - padding: 12px 24px; -`; -``` - -## Performance Optimization - -- **App Router Architecture** - Server Components for optimal SSR -- **Automatic Code Splitting** - Route-based code splitting -- **Image Optimization** - Next.js built-in image optimization with automatic format selection -- **Lazy Loading** - Component and route lazy loading -- **Caching Strategies** - HTTP caching and in-memory caching -- **Bundle Analysis** - Built-in bundle analysis tools - -## PWA Support - -ReactPress Client is a Progressive Web App with: -- Offline support with service workers -- Installable on devices with native app experience -- Push notifications (coming soon) -- App-like experience with splash screens - -## Testing - -```bash -# Run unit tests with Vitest -pnpm run test - -# Run integration tests with Playwright -pnpm run test:e2e - -# Run linting -pnpm run lint - -# Run formatting -pnpm run format - -# Run type checking -pnpm run type-check - -# Run bundle analysis -pnpm run analyze -``` - -## Templates - -ReactPress Client can be used with various professional templates: - -### Hello World Template -```bash -npx @fecommunity/reactpress-template-hello-world my-blog -``` - -### Twenty Twenty Five Template -```bash -npx @fecommunity/reactpress-template-twentytwentyfive my-blog -``` - -### Custom Templates -Create your own templates by extending the client with custom components and pages. - -## Deployment - -### Vercel Deployment (Recommended) - -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/fecommunity/reactpress) - -### Custom Deployment - -```bash -# Build for production -pnpm run build - -# Start production server -pnpm run start -``` - -## Support - -- 📖 [Documentation](https://github.com/fecommunity/reactpress) -- 🐛 [Issues](https://github.com/fecommunity/reactpress/issues) -- 💬 [Discussions](https://github.com/fecommunity/reactpress/discussions) -- 📧 [Support](mailto:support@reactpress.dev) - -## Contributing - -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) -5. Open a pull request - -## License - -MIT License - see [LICENSE](LICENSE) file for details. - ---- - -Built with ❤️ by [FECommunity](https://github.com/fecommunity) \ No newline at end of file diff --git a/client/bin/reactpress-client.js b/client/bin/reactpress-client.js deleted file mode 100755 index c045124c..00000000 --- a/client/bin/reactpress-client.js +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env node - -/** - * ReactPress Client CLI Entry Point - * This script allows starting the ReactPress client via npx - * Supports both regular and PM2 startup modes - */ - -const path = require('path'); -const fs = require('fs'); -const { spawn, spawnSync } = require('child_process'); - -// Capture the original working directory where npx was executed -// BUT prioritize the REACTPRESS_ORIGINAL_CWD environment variable if it exists -// This ensures consistency when running via pnpm dev from root directory -const originalCwd = process.env.REACTPRESS_ORIGINAL_CWD || process.cwd(); - -// Get command line arguments -const args = process.argv.slice(2); -const usePM2 = args.includes('--pm2'); -const showHelp = args.includes('--help') || args.includes('-h'); - -// Show help if requested -if (showHelp) { - console.log(` -ReactPress Client - Next.js-based frontend for ReactPress CMS - -Usage: - npx @fecommunity/reactpress-client [options] - -Options: - --pm2 Start client with PM2 process manager - --help, -h Show this help message - -Examples: - npx @fecommunity/reactpress-client # Start client normally - npx @fecommunity/reactpress-client --pm2 # Start client with PM2 - npx @fecommunity/reactpress-client --help # Show this help message - `); - process.exit(0); -} - -// Get the directory where this script is located -const binDir = __dirname; -const clientDir = path.join(binDir, '..'); -const nextDir = path.join(clientDir, '.next'); - -// Function to check if PM2 is installed -function isPM2Installed() { - try { - require.resolve('pm2'); - return true; - } catch (e) { - // Check if PM2 is installed globally - try { - spawnSync('pm2', ['--version'], { stdio: 'ignore' }); - return true; - } catch (e) { - return false; - } - } -} - -// Function to install PM2 -function installPM2() { - console.log('[ReactPress Client] Installing PM2...'); - const installResult = spawnSync('npm', ['install', 'pm2', '--no-save'], { - stdio: 'inherit', - cwd: clientDir - }); - - if (installResult.status !== 0) { - console.error('[ReactPress Client] Failed to install PM2'); - return false; - } - - return true; -} - -// Function to start with PM2 -function startWithPM2() { - // Check if PM2 is installed - if (!isPM2Installed()) { - // Try to install PM2 - if (!installPM2()) { - console.error('[ReactPress Client] Cannot start with PM2'); - process.exit(1); - } - } - - // Check if the client is built - if (!fs.existsSync(nextDir)) { - console.log('[ReactPress Client] Client not built yet. Building...'); - - // Try to build the client - const buildResult = spawnSync('npm', ['run', 'build'], { - stdio: 'inherit', - cwd: clientDir - }); - - if (buildResult.status !== 0) { - console.error('[ReactPress Client] Failed to build client'); - process.exit(1); - } - } - - console.log('[ReactPress Client] Starting with PM2...'); - - // Use PM2 to start the Next.js production server - let pm2Command = 'pm2'; - try { - // Try to resolve PM2 path - pm2Command = path.join(clientDir, 'node_modules', '.bin', 'pm2'); - if (!fs.existsSync(pm2Command)) { - pm2Command = 'pm2'; - } - } catch (e) { - pm2Command = 'pm2'; - } - - // Start with PM2 using direct command - const pm2 = spawn(pm2Command, ['start', 'npm', '--name', 'reactpress-client', '--', 'run', 'start'], { - stdio: 'inherit', - cwd: clientDir - }); - - pm2.on('close', (code) => { - console.log(`[ReactPress Client] PM2 process exited with code ${code}`); - process.exit(code); - }); - - pm2.on('error', (error) => { - console.error('[ReactPress Client] Failed to start with PM2:', error); - process.exit(1); - }); -} - -// Function to start with regular Node.js (npm start) -function startWithNode() { - // Check if the app is built - if (!fs.existsSync(nextDir)) { - console.log('[ReactPress Client] Client not built yet. Building...'); - - // Try to build the client - const buildResult = spawnSync('npm', ['run', 'build'], { - stdio: 'inherit', - cwd: clientDir - }); - - if (buildResult.status !== 0) { - console.error('[ReactPress Client] Failed to build client'); - process.exit(1); - } - } - - // ONLY set the environment variable if it's not already set - // This preserves the value set by set-env.js when running pnpm dev from root - if (!process.env.REACTPRESS_ORIGINAL_CWD) { - process.env.REACTPRESS_ORIGINAL_CWD = originalCwd; - } else { - console.log(`[ReactPress Client] Using existing REACTPRESS_ORIGINAL_CWD: ${process.env.REACTPRESS_ORIGINAL_CWD}`); - } - - // Change to the client directory - process.chdir(clientDir); - - // Start with npm start - console.log('[ReactPress Client] Starting with npm start...'); - const npmStart = spawn('npm', ['start'], { - stdio: 'inherit', - cwd: clientDir - }); - - npmStart.on('close', (code) => { - console.log(`[ReactPress Client] npm start process exited with code ${code}`); - process.exit(code); - }); - - npmStart.on('error', (error) => { - console.error('[ReactPress Client] Failed to start with npm start:', error); - process.exit(1); - }); -} - -// Main execution -if (usePM2) { - startWithPM2(); -} else { - startWithNode(); -} \ No newline at end of file diff --git a/client/next-env.d.ts b/client/next-env.d.ts deleted file mode 100644 index 4f11a03d..00000000 --- a/client/next-env.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/client/next-sitemap.js b/client/next-sitemap.js deleted file mode 100644 index 63aa6ab1..00000000 --- a/client/next-sitemap.js +++ /dev/null @@ -1,10 +0,0 @@ -const { config } = require('@fecommunity/reactpress-toolkit'); - -module.exports = { - siteUrl: config.CLIENT_SITE_URL, - generateRobotsTxt: true, - robotsTxtOptions: { - policies: [{ userAgent: '*', allow: '/', disallow: '/admin/' }], - }, - exclude: ['/admin', '/admin/**'], -}; diff --git a/client/next.config.js b/client/next.config.js deleted file mode 100644 index 4ff813f5..00000000 --- a/client/next.config.js +++ /dev/null @@ -1,67 +0,0 @@ -const path = require('path'); -const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); -const withPlugins = require('next-compose-plugins'); -const withLess = require('next-with-less'); -const withPWA = require('next-pwa'); -const { config } = require('@fecommunity/reactpress-toolkit'); -const antdVariablesFilePath = path.resolve(__dirname, './antd-custom.less'); - -const getServerApiUrl = () => { - if (config.SERVER_URL) { - return `${config.SERVER_SITE_URL}/api`; - } else { - return config.SERVER_API_URL || `${process.env.SERVER_SITE_URL}/api` || 'http://localhost:3002/api'; - } -}; - -/** @type {import('next').NextConfig} */ -const nextConfig = { - assetPrefix: config.CLIENT_ASSET_PREFIX || '/', - i18n: { - locales: config.locales && config.locales.length > 0 ? config.locales : ['zh', 'en'], - defaultLocale: config.defaultLocale || 'zh', - }, - env: { - SERVER_API_URL: getServerApiUrl(), - GITHUB_CLIENT_ID: config.GITHUB_CLIENT_ID, - }, - webpack: (config, { dev, isServer }) => { - config.resolve.plugins.push(new TsconfigPathsPlugin()); - return config; - }, - eslint: { - ignoreDuringBuilds: true, - }, - typescript: { - ignoreBuildErrors: true, - }, - compiler: { - removeConsole: { - exclude: ['error'], - }, - }, -}; - -module.exports = withPlugins( - [ - [ - withPWA, - { - pwa: { - disable: process.env.NODE_ENV !== 'production', - dest: '.next', - sw: 'service-worker.js', - }, - }, - ], - [ - withLess, - { - lessLoaderOptions: { - additionalData: (content) => `${content}\n\n@import '${antdVariablesFilePath}';`, - }, - }, - ], - ], - nextConfig -); \ No newline at end of file diff --git a/client/package.json b/client/package.json deleted file mode 100644 index 6f4e531d..00000000 --- a/client/package.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "name": "@fecommunity/reactpress-client", - "version": "3.7.0", - "bin": { - "reactpress-client": "./bin/reactpress-client.js" - }, - "files": [ - ".next/**/*", - "bin/**/*", - "public/**/*", - "next.config.js", - "server.js" - ], - "scripts": { - "prebuild": "rimraf .next", - "build": "next build", - "postbuild": "next-sitemap", - "dev": "node server.js", - "start": "cross-env NODE_ENV=production node server.js", - "pm2": "pm2 start npm --name @fecommunity/reactpress-client -- start" - }, - "dependencies": { - "@ant-design/compatible": "^1.1.0", - "@ant-design/cssinjs": "^1.22.0", - "@ant-design/icons": "^4.7.0", - "@ant-design/pro-layout": "7.19.11", - "@monaco-editor/react": "^4.6.0", - "@fecommunity/reactpress-toolkit": "workspace:*", - "fs-extra": "^10.0.0", - "antd": "^5.24.4", - "array-move": "^3.0.1", - "axios": "^0.23.0", - "classnames": "^2.3.1", - "copy-to-clipboard": "^3.3.1", - "date-fns": "^2.17.0", - "deep-equal": "^2.0.5", - "dotenv": "^17.2.3", - "highlight.js": "^9.18.5", - "less": "^4.1.2", - "less-vars-to-js": "^1.3.0", - "lodash-es": "^4.17.21", - "mime-types": "^2.1.26", - "next": "^12.3.4", - "next-compose-plugins": "^2.2.1", - "next-fonts": "^1.5.1", - "next-images": "^1.3.1", - "next-intl": "^1.5.1", - "next-page-transitions": "^1.0.0-beta.2", - "next-pwa": "^5.5.2", - "next-sitemap": "^1.6.102", - "next-with-less": "^2.0.5", - "nprogress": "^0.2.0", - "open": "^8.4.2", - "preact": "^10.5.14", - "qrcode-svg": "^1.1.0", - "react": "17.0.2", - "react-dom": "17.0.2", - "react-infinite-scroller": "^1.2.4", - "react-lazyload": "^2.6.5", - "react-sortable-hoc": "^2.0.0", - "react-spring": "^9.1.2", - "react-text-loop": "2.3.0", - "react-visibility-sensor": "^5.1.1", - "showdown": "^1.9.1", - "viewerjs": "^1.5.0", - "xml": "^1.0.1", - "echarts-for-react": "^3.0.2", - "echarts": "^5.6.0" - }, - "devDependencies": { - "@types/node": "17.0.22", - "@types/react": "17.0.42", - "@types/react-infinite-scroller": "^1.2.3", - "@typescript-eslint/eslint-plugin": "^5.21.0", - "@typescript-eslint/parser": "^5.21.0", - "cross-env": "^7.0.3", - "eslint": "8.11.0", - "eslint-config-next": "12.1.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.4", - "eslint-plugin-react-hooks": "^4.5.0", - "eslint-plugin-simple-import-sort": "^7.0.0", - "less-loader": "^10.2.0", - "rimraf": "^3.0.2", - "sass": "^1.49.9", - "tsconfig-paths-webpack-plugin": "^3.5.2", - "typescript": "4.6.2" - } -} diff --git a/client/pages/404.tsx b/client/pages/404.tsx deleted file mode 100644 index 5e5e5df3..00000000 --- a/client/pages/404.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -import { Error404 } from './_error'; - -function Error() { - return ; -} - -export default Error; diff --git a/client/pages/_app.tsx b/client/pages/_app.tsx deleted file mode 100644 index 18c4b309..00000000 --- a/client/pages/_app.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import '@/theme/index.scss'; -import 'highlight.js/styles/atom-one-dark.css'; -import 'viewerjs/dist/viewer.css'; - -import { NProgress } from '@components/NProgress'; -import { ConfigProvider, theme } from 'antd'; -import { IntlMessages, NextIntlProvider } from 'next-intl'; -import App from 'next/app'; -import { default as Router } from 'next/router'; - -import { Analytics } from '@/components/Analytics'; -import { FixAntdStyleTransition } from '@/components/FixAntdStyleTransition'; -import { ViewStatistics } from '@/components/ViewStatistics'; -import { GlobalContext, IGlobalContext } from '@/context/global'; -import { AppLayout } from '@/layout/AppLayout'; -import { CategoryProvider } from '@/providers/category'; -import { PageProvider } from '@/providers/page'; -import { SettingProvider } from '@/providers/setting'; -import { TagProvider } from '@/providers/tag'; -import { UserProvider } from '@/providers/user'; -import { safeJsonParse } from '@/utils/json'; -import { toLogin } from '@/utils/login'; - -Router.events.on('routeChangeComplete', () => { - setTimeout(() => { - if (document.documentElement.scrollTop > 0) { - window.scrollTo({ - top: 0, - behavior: 'smooth', - }); - } - }, 0); -}); - -class MyApp extends App { - state = { - locale: '', - user: null, - theme: null, - collapsed: false, - }; - - static getInitialProps = async ({ Component, ctx }) => { - const getPagePropsPromise = Component.getInitialProps ? Component.getInitialProps(ctx) : Promise.resolve({}); - const [pageProps, setting, tags, categories, pages] = await Promise.all([ - getPagePropsPromise, - SettingProvider.getSetting(), - TagProvider.getTags({ articleStatus: 'publish' }), - CategoryProvider.getCategory({ articleStatus: 'publish' }), - PageProvider.getAllPublisedPages(), - ]); - const i18n = safeJsonParse(setting.i18n); - const globalSetting = safeJsonParse(setting.globalSetting)?.[ctx?.locale]; - return { - pageProps, - setting, - tags, - categories, - pages: pages[0] || [], - i18n, - globalSetting, - locales: Object.keys(i18n), - }; - }; - - changeLocale = (key) => { - window.localStorage.setItem('locale', key); - this.setState({ locale: key }); - }; - - setUser = (user) => { - window.localStorage.setItem('user', JSON.stringify(user)); - this.setState({ user }); - }; - - removeUser = () => { - window.localStorage.setItem('user', ''); - this.setState({ user: null }); - window.location.reload(); - }; - - changeTheme = (theme: string) => { - this.setState({ theme }); - }; - - - getSetting = () => { - SettingProvider.getSetting().then((res) => { - this.setState({ setting: res }); - }); - }; - - isAdminPage = () => { - const isAdminPage = this.props?.router?.route?.startsWith('/admin'); - return isAdminPage; - } - - getUserFromStorage = () => { - const str = localStorage.getItem('user'); - const isAdminPage = this.isAdminPage(); - if (!isAdminPage) { - return; - } - if (str) { - const user = JSON.parse(str); - this.setUser(user); - UserProvider.checkAdmin(user); - } else { - toLogin(); - } - }; - - toggleCollapse = () => { - this.setState({ collapsed: !this.state.collapsed }); - }; - - componentDidMount() { - const userStr = window.localStorage.getItem('user'); - if (userStr) { - this.setState({ user: safeJsonParse(userStr) }); - } - this.getUserFromStorage(); - } - - render() { - const { Component, pageProps, i18n, globalSetting, locales, router, ...contextValue } = this.props; - const locale = this.state.locale || router.locale; - const { needLayoutFooter = true, hasBg = false } = pageProps; - const message = i18n[locale] || {}; - const algorithm = this.state.theme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm; - const isAdminPage = this.isAdminPage(); - const hasFooter = !isAdminPage && needLayoutFooter; - - return ( - - - - - - - - {!isAdminPage && } - - - - - - ); - } -} - -export default MyApp; diff --git a/client/pages/_document.tsx b/client/pages/_document.tsx deleted file mode 100644 index 4e515f7b..00000000 --- a/client/pages/_document.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs'; -import type { DocumentContext } from 'next/document'; -import Document, { Head, Html, Main, NextScript } from 'next/document'; - -const MyDocument = () => ( - - - -
- - - -); - -MyDocument.getInitialProps = async (ctx: DocumentContext) => { - const cache = createCache(); - const originalRenderPage = ctx.renderPage; - ctx.renderPage = () => - originalRenderPage({ - enhanceApp: (App) => (props) => ( - - - - ), - }); - - const initialProps = await Document.getInitialProps(ctx); - const style = extractStyle(cache, true); - return { - ...initialProps, - styles: ( - <> - {initialProps.styles} - - - - - )} - /> - - - - ); -}; - -export const GitHub = () => { - return ( - -
  • - - - -
  • -
    - ); -}; - -export const Comment = () => { - return ( - - } - > -
  • - -
  • -
    - ); -}; - -export const WeChat = () => { - return ( - } - > -
  • - -
  • -
    - ); -}; - -export const ContactInfo = () => { - return ( -
    -
      - - - - - - - - - - - - - -
    -
    - ); -}; - -const AboutUs = ({ setting, className = '', hasBg = false }: IProps) => { - const t = useTranslations(); - return ( - - - {t('aboutUs')} - - } - className={style.card} - > -
    - {setting?.systemFooterInfo && ( -
    - )} -
    -
    - - -
    -
    -
    -
    - ); -}; - -export default AboutUs; diff --git a/client/src/components/AdvanceSearch/index.module.scss b/client/src/components/AdvanceSearch/index.module.scss deleted file mode 100644 index f49fff68..00000000 --- a/client/src/components/AdvanceSearch/index.module.scss +++ /dev/null @@ -1,129 +0,0 @@ -.searchItem { - display: flex; - flex-direction: column; - cursor: pointer; - &:hover { - color: var(--primary-color); - } - .description { - color: var(--second-text-color); - } -} -.wrapper { - padding: 16px 24px !important; -} -.pop { - background-color: var(--bg-box) !important; -} -.searchWrapper { - background-color: var(--bg-box) !important; - .autoComplete { - width: 100%; - height: 48px !important; - * { - background-color: var(--bg-box) !important; - } - } - - .searchCategory, - .searchSubCategory { - background-color: var(--bg-box) !important; - > div { - margin: 0 !important; - div { - justify-content: center !important; - } - } - } - .searchSubCategory { - > div { - > div { - > div { - > div:last-child { - top: 0 !important; - content: ''; - border-width: 8px 8px 0px 8px; - border-style: solid; - border-color: #f44336 transparent transparent; - position: absolute; - left: 50%; - top: 0; - margin-left: -8px; - background: none; - width: 0px !important; - } - } - } - } - } - - .searchInput { - border-radius: 24px; - } - - a { - color: inherit; - text-decoration: none; - - &:hover { - color: var(--primary-color); - } - } - - .wrapper { - height: fit-content; - max-height: 70vh; - overflow: scroll; - } -} -.inner { - box-shadow: var(--box-shadow) !important; - - .ant-modal-header { - font-size: 1.5rem; - font-weight: 600; - } - - .ant-modal-close-x { - font-size: 18px; - } - - .result { - flex: 1; - overflow: auto; - } - - ul { - list-style: circle; - - li { - display: flex; - align-items: center; - - &:hover { - background: var(--bg-body); - } - - a { - display: inline-block; - width: 100%; - padding: 12px 8px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - } -} - -@media (min-width: 992px) { - .inner { - width: 768px !important; - } -} - -@media (max-width: 576px) { - .inner { - width: 92% !important; - } -} diff --git a/client/src/components/AdvanceSearch/index.tsx b/client/src/components/AdvanceSearch/index.tsx deleted file mode 100644 index 32f0b68d..00000000 --- a/client/src/components/AdvanceSearch/index.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { AutoComplete, Button, Input, Spin, Tabs } from 'antd'; -import React, { useContext, useEffect, useState } from 'react'; - -import { useAsyncLoading } from '@/hooks/useAsyncLoading'; -import { SearchProvider } from '@/providers/search'; -import { SearchOutlined } from '@ant-design/icons'; - -import { GlobalContext } from '@/context/global'; -import { ArticleProvider } from '@/providers/article'; -import { jsonp } from '@/utils/jsonp'; -import styles from './index.module.scss'; - -interface IProps { - globalSetting?: any; -} - -export const AdvanceSearch: React.FC = (props) => { - const { globalSetting } = useContext(GlobalContext); - const { subCategories = {}, categories } = globalSetting?.globalConfig?.navConfig || props.globalSetting || {}; - const [category, setCategory] = useState(categories?.[0]?.key); - const [subCategory, setSubCategory] = useState(subCategories?.[category]?.[0]?.key); - const [options, setOptions] = useState([]); - const [searchVal, setSearchVal] = useState(); - - useEffect(() => { - setSubCategory(subCategories?.[category]?.[0]?.key); - fetchSuggestions(searchVal); - }, [category]); - - const fetchLocalData = (keyword: string) => { - if (keyword?.length) { - return SearchProvider.searchArticles(keyword); - } else { - return ArticleProvider.getRecommend(); - } - }; - - const [searchArticles, loading] = useAsyncLoading(fetchLocalData); - - const fetchSuggestions = (keyword: string) => { - switch (category) { - case 'local': - return searchArticles(keyword).then((res) => { - const options = res - .filter((t) => t.status === 'publish') - .map((item) => ({ - label: item?.title, - value: item?.title, - description: item?.summary, - link: `/article/${item?.id}`, - data: item, - })); - setOptions(options); - }); - default: - return jsonp( - `https://suggestion.baidu.com/su`, - { - wd: keyword || '高热度网', - }, - (res) => { - const data = subCategories[category]?.find((item) => item.key === subCategory); - const options = (res?.s || []).map((item) => ({ - link: data?.url ? `${data.url}${item}` : null, - label: item, - value: item, - })); - setOptions(options); - } - ); - } - }; - - const onValueChange = (val) => { - setSearchVal(val); - fetchSuggestions(val); - }; - - const handleSearch = () => { - const data = subCategories[category]?.find((item) => item.key === subCategory); - const link = data?.url ? `${data.url}${searchVal || '高热度网'}` : null; - if (category === 'local' || !!searchVal) { - fetchSuggestions(searchVal); - } else { - window.open(link, '_blank'); - } - }; - - const optionRender = (record, info) => { - const { label, value, data: { link, description, id } = {} as any } = record; - - return ( -
    { - !!link && window.open(link, '_blank'); - e.stopPropagation(); - }} - key={info?.index} - > -
    {label}
    -

    -

    - ); - }; - - return ( -
    -
    -
    - { - setCategory(val); - }} - /> - fetchSuggestions(searchVal)} - notFoundContent={loading ? : null} - popupClassName={styles.pop} - > - } type="text" />} - /> - - { - setSubCategory(value); - fetchSuggestions(searchVal); - }} - /> -
    -
    - ); -}; diff --git a/client/src/components/Analytics/index.tsx b/client/src/components/Analytics/index.tsx deleted file mode 100644 index 739d9689..00000000 --- a/client/src/components/Analytics/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useContext, useEffect } from 'react'; - -import { GlobalContext } from '@/context/global'; - -export const Analytics = (props) => { - const { setting } = useContext(GlobalContext); - - useEffect(() => { - const googleAnalyticsId = setting.googleAnalyticsId; - - if (!googleAnalyticsId) { - return; - } - - // @ts-ignore - window.dataLayer = window.dataLayer || []; - function gtag() { - // @ts-ignore - window.dataLayer.push(arguments); // eslint-disable-line prefer-rest-params - } - // @ts-ignore - gtag('js', new Date()); - // @ts-ignore - gtag('config', googleAnalyticsId); - - const script = document.createElement('script'); - script.src = `https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsId}`; - script.async = true; - - if (document.body) { - document.body.appendChild(script); - } - }, [setting.googleAnalyticsId]); - - useEffect(() => { - const baiduAnalyticsId = setting.baiduAnalyticsId; - - if (!baiduAnalyticsId) { - return; - } - - const hm = document.createElement('script'); - hm.src = `https://hm.baidu.com/hm.js?${baiduAnalyticsId}`; - const s = document.getElementsByTagName('script')[0]; - s.parentNode.insertBefore(hm, s); - }, [setting.baiduAnalyticsId]); - - return props.children || null; -}; diff --git a/client/src/components/Animation/Opacity.tsx b/client/src/components/Animation/Opacity.tsx deleted file mode 100644 index 0537d667..00000000 --- a/client/src/components/Animation/Opacity.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -import { Spring, SpringProps } from './Spring'; - -export const Opacity: React.FC = (props) => { - const { from = {}, to = {}, ...rest } = props; - from.opacity = 0; - to.opacity = 1; - - return ; -}; diff --git a/client/src/components/Animation/Spring.tsx b/client/src/components/Animation/Spring.tsx deleted file mode 100644 index 6db5262c..00000000 --- a/client/src/components/Animation/Spring.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { useCallback, useEffect, useRef } from 'react'; -import { animated, useSpring } from 'react-spring'; -import VisibilitySensor from 'react-visibility-sensor'; - -import { elementInViewport } from '@/utils'; - -export interface SpringProps { - containerProps?: Record; - from?: Record; - to?: Record; -} - -export const Spring: React.FC = ({ containerProps = {}, from = {}, to = {}, children }) => { - const ref = useRef(); - const [styles, animation] = useSpring(() => ({ - ...from, - config: { mass: 10, tension: 400, friction: 40, precision: 0.00001, clamp: true }, - })); - const onViewportChange = useCallback( - (visible) => { - if (visible) { - animation.start(to); - } - }, - [animation, to] - ); - - useEffect(() => { - if (elementInViewport(ref.current)) { - animation.start(to); - } - }, [animation, to]); - - return ( - - - {children} - - - ); -}; diff --git a/client/src/components/Animation/Trail.tsx b/client/src/components/Animation/Trail.tsx deleted file mode 100644 index 49177aa9..00000000 --- a/client/src/components/Animation/Trail.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { animated, useTrail } from 'react-spring'; - -interface ListTrailProps { - length: number; - options: Record; - element?: string; - setItemContainerProps?: (index: number) => Record; - renderItem: (index: number) => React.ReactNode; -} - -export const ListTrail: React.FC = ({ - length, - options, - element = 'li', - setItemContainerProps = () => ({}), - renderItem, -}) => { - const C = animated[element]; - const trail = useTrail(length, { - config: { mass: 2, tension: 280, friction: 24, clamp: true }, - ...options, - }); - - return ( - <> - {trail.map((style, index) => { - return ( - - {renderItem(index)} - - ); - })} - - ); -}; diff --git a/client/src/components/Animation/Transition.tsx b/client/src/components/Animation/Transition.tsx deleted file mode 100644 index da7b353c..00000000 --- a/client/src/components/Animation/Transition.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { animated, useTransition } from 'react-spring'; - -type ConditionTransitionProps = { - visible: boolean; - options: Record; -}; - -export const ConditionTransition: React.FC = ({ visible, options, children }) => { - const transitions = useTransition(visible, { - config: { mass: 2, tension: 280, friction: 24, clamp: true }, - ...options, - }); - - return ( - <> - {transitions( - (style, item) => item && {children} - )} - - ); -}; diff --git a/client/src/components/ArticleCarousel/index.module.scss b/client/src/components/ArticleCarousel/index.module.scss deleted file mode 100644 index 133b2094..00000000 --- a/client/src/components/ArticleCarousel/index.module.scss +++ /dev/null @@ -1,85 +0,0 @@ -.wrapper { - background: var(--bg-second); - box-shadow: var(--box-shadow); - border-radius: var(--border-radius); - - > div { - font-size: 0 !important; - } - - .articleItem { - position: relative; - display: flex; - width: 100%; - height: 260px; - background-position: center; - background-size: cover; - background-repeat: no-repeat; - overflow: hidden; - border-radius: var(--border-radius); - - img { - width: 100%; - } - - .info { - position: absolute; - top: 0; - left: 0; - z-index: 1; - display: flex; - width: 100%; - height: 100%; - font-size: 1rem; - color: var(--font-color-base); - background-color: rgb(0 0 0 / 35%); - flex-direction: column; - justify-content: center; - align-items: center; - - h2 { - color: inherit; - text-align: center; - margin: 0 16px; - } - - .seperator { - margin: 0 4px; - } - - .meta { - text-align: right; - } - } - } - - @media (max-width: 768px) { - .articleItem { - height: 300px; - } - } - - @media (min-width: 768px) { - .container { - width: 768px; - } - } - - @media (min-width: 992px) { - .articleItem { - height: 340px; - } - } - - @media (min-width: 1200px) { - .articleItem { - height: 380px; - } - } - - @media (min-width: 1360px) { - .articleItem { - height: 460px; - } - } -} diff --git a/client/src/components/ArticleCarousel/index.tsx b/client/src/components/ArticleCarousel/index.tsx deleted file mode 100644 index 8f00e92a..00000000 --- a/client/src/components/ArticleCarousel/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Carousel } from 'antd'; -import Link from 'next/link'; -import { useTranslations } from 'next-intl'; -import React from 'react'; - -import { LocaleTime } from '@/components/LocaleTime'; - -import style from './index.module.scss'; - -interface IProps { - articles?: IArticle[]; -} - -export const ArticleCarousel: React.FC = ({ articles = [] }) => { - const t = useTranslations(); - return articles && articles.length ? ( -
    - - {(articles || []) - .filter((article) => article.cover) - .slice(0, 6) - .map((article) => { - return ( - - ); - })} - -
    - ) : null; -}; diff --git a/client/src/components/ArticleEditor/ArticleSettingDrawer/index.module.scss b/client/src/components/ArticleEditor/ArticleSettingDrawer/index.module.scss deleted file mode 100644 index b38c9083..00000000 --- a/client/src/components/ArticleEditor/ArticleSettingDrawer/index.module.scss +++ /dev/null @@ -1,38 +0,0 @@ -.formItem { - display: flex; - align-items: center; - - + .formItem { - margin-top: 16px; - } - - > span { - padding-right: 16px; - } - - > div { - flex: 1; - } -} - -.cover { - .preview { - display: flex; - justify-content: center; - align-items: center; - height: 180px; - margin-bottom: 16px; - color: #888; - background-color: #f5f5f5; - - img { - display: block; - max-width: 100%; - max-height: 180px; - } - } - - button { - margin-top: 16px; - } -} diff --git a/client/src/components/ArticleEditor/ArticleSettingDrawer/index.tsx b/client/src/components/ArticleEditor/ArticleSettingDrawer/index.tsx deleted file mode 100644 index fd0d2daf..00000000 --- a/client/src/components/ArticleEditor/ArticleSettingDrawer/index.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { Button, Drawer, Input, Select, Switch } from 'antd'; -import React, { useEffect, useReducer, useState } from 'react'; - -import { FileSelectDrawer } from '@/components/FileSelectDrawer'; -import { CategoryProvider } from '@/providers/category'; -import { TagProvider } from '@/providers/tag'; - -import style from './index.module.scss'; - -interface IProps { - visible: boolean; - article?: Partial; - onClose: () => void; - onChange?: (arg) => void; -} - -const FormItem = ({ label, content }) => { - return ( -
    - {label} -
    {content}
    -
    - ); -}; - -const initialArticleAttrs = { - summary: null, // 摘要 - password: null, // 密码 - isCommentable: true, // 评论 - isRecommended: true, // 推荐到首页 - category: null, // 分类 - tags: [], // 标签 - cover: null, // 封面 -}; -function reducer(state: typeof initialArticleAttrs = initialArticleAttrs, action) { - const payload = action.payload; - switch (action.type) { - case 'summary': - return { ...state, summary: payload }; - case 'password': - return { ...state, password: payload }; - case 'isCommentable': - return { ...state, isCommentable: payload }; - case 'isRecommended': - return { ...state, isRecommended: payload }; - case 'category': - return { ...state, category: payload }; - case 'tags': - return { ...state, tags: payload }; - case 'cover': - return { ...state, cover: payload }; - default: - return state; - } -} -export const ArticleSettingDrawer: React.FC = ({ article, visible, onClose, onChange }) => { - const [fileVisible, setFileVisible] = useState(false); - const [attrs, dispatch] = useReducer(reducer, article as typeof initialArticleAttrs); - const [categorys, setCategorys] = useState>([]); - const [tags, setTags] = useState>([]); - - useEffect(() => { - CategoryProvider.getCategory().then((res) => setCategorys(res)); - TagProvider.getTags().then((tags) => setTags(tags)); - }, []); - - const ok = () => { - onChange({ - ...attrs, - tags: (attrs.tags || []).join(','), - }); - }; - - return ( - - { - dispatch({ type: 'summary', payload: e.target.value }); - }} - /> - } - /> - { - dispatch({ type: 'password', payload: e.target.value }); - }} - /> - } - /> - { - dispatch({ type: 'isCommentable', payload: val }); - }} - /> - } - /> - { - dispatch({ type: 'isRecommended', payload: val }); - }} - /> - } - /> - { - dispatch({ type: 'category', payload: id }); - }} - style={{ width: '100%' }} - > - {categorys.map((t) => ( - - {t.label} - - ))} - - } - /> - t.id || t)} - onChange={(tags) => { - dispatch({ type: 'tags', payload: tags }); - }} - > - {tags.map((tag) => ( - - {tag.label} - - ))} - - } - /> - -
    setFileVisible(true)} className={style.preview}> - 预览图 -
    - { - dispatch({ type: 'cover', payload: e.target.value }); - }} - /> - -
    - } - /> - setFileVisible(false)} - onChange={(url) => { - dispatch({ type: 'cover', payload: url }); - }} - /> -
    - -
    - - ); -}; diff --git a/client/src/components/ArticleEditor/index.module.scss b/client/src/components/ArticleEditor/index.module.scss deleted file mode 100644 index 8a69d715..00000000 --- a/client/src/components/ArticleEditor/index.module.scss +++ /dev/null @@ -1,31 +0,0 @@ -.wrapper { - display: flex; - flex-direction: column; - height: 100vh; - background-color: var(--bg-box); - - .header { - z-index: 1000; - height: 64px; - background-color: var(--bg-secord); - - > div { - height: 100%; - } - - input { - padding-right: 0; - padding-left: 0; - border-top: 0; - border-left: 0; - border-radius: 0 !important; - box-shadow: none !important; - border-right: 0; - } - } - - .main { - flex: 1; - overflow: hidden; - } -} diff --git a/client/src/components/ArticleEditor/index.tsx b/client/src/components/ArticleEditor/index.tsx deleted file mode 100644 index ca25ec07..00000000 --- a/client/src/components/ArticleEditor/index.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import { CloseOutlined, EllipsisOutlined } from '@ant-design/icons'; -import { PageHeader } from '@ant-design/pro-layout'; -import { Editor as MDEditor } from '@components/Editor'; -import { Button, Dropdown, Input, Layout, Menu, message, Modal } from 'antd'; -import cls from 'classnames'; -import { default as Router } from 'next/router'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import Head from 'next/head'; - -import { useSetting } from '@/hooks/useSetting'; -import { useToggle } from '@/hooks/useToggle'; -import { useWarningOnExit } from '@/hooks/useWarningOnExit'; -import { ArticleProvider } from '@/providers/article'; -import { resolveUrl } from '@/utils'; - -import { ArticleSettingDrawer } from './ArticleSettingDrawer'; -import style from './index.module.scss'; - -interface IProps { - id?: string | number; - article?: IArticle; -} - -const REQUIRED_ARTICLE_ATTRS = [ - ['title', '请输入文章标题'], - ['content', '请输入文章内容'], -]; - -// 副作用:传给服务端的 category 需要是 id -const transformCategory = (article) => { - if (article.category && article.category.id) { - article.category = article.category.id; - } -}; -const transformTags = (article) => { - if (Array.isArray(article.tags)) { - try { - article.tags = (article.tags as ITag[]).map((t) => t.id).join(','); - } catch (e) { - console.log(e); - } - } -}; - -export const ArticleEditor: React.FC = ({ id: defaultId, article: defaultArticle = { title: '' } }) => { - const isCreate = !defaultId; // 一开始是否是新建 - const setting = useSetting(); - const [id, setId] = useState(defaultId); - const [article, setArticle] = useState>(defaultArticle); - const [settingDrawerVisible, toggleSettingDrawerVisible] = useToggle(false); - const [hasSaved, toggleHasSaved] = useToggle(false); - - const patchArticle = useMemo( - () => (key) => (value) => { - if (value.target) { - value = value.target.value; - } - setArticle((article) => { - article[key] = value; - return article; - }); - }, - [] - ); - - // 校验文章必要属性 - const check = useCallback(() => { - let canPublish = true; - let errorMsg = null; - REQUIRED_ARTICLE_ATTRS.forEach(([key, msg]) => { - if (!article[key]) { - errorMsg = msg; - canPublish = false; - } - }); - if (!canPublish) { - return Promise.reject(new Error(errorMsg)); - } - return Promise.resolve(); - }, [article]); - - // 打开发布抽屉 - const openSetting = useCallback(() => { - check() - .then(() => { - toggleSettingDrawerVisible(); - }) - .catch((err) => { - message.warning(err.message); - }); - }, [check, toggleSettingDrawerVisible]); - - const saveSetting = useCallback( - (setting) => { - toggleSettingDrawerVisible(); - Object.assign(article, setting); - }, - [article, toggleSettingDrawerVisible] - ); - - // 保存草稿或者发布线上 - const saveOrPublish = useCallback( - (patch = {}) => { - const data = { ...article, ...patch }; - return check() - .then(() => { - transformCategory(data); - transformTags(data); - const promise = !isCreate ? ArticleProvider.updateArticle(id, data) : ArticleProvider.addArticle(data); - return promise.then((res) => { - setId(res.id); - toggleHasSaved(true); - message.success(res.status === 'draft' ? '文章已保存为草稿' : '文章已发布'); - }); - }) - .catch((err) => { - message.warning(err.message); - return Promise.reject(err); - }); - }, - [article, isCreate, check, id, toggleHasSaved] - ); - - const saveDraft = useCallback(() => { - return saveOrPublish({ status: 'draft' }); - }, [saveOrPublish]); - - const publish = useCallback(() => { - return saveOrPublish({ status: 'publish' }); - }, [saveOrPublish]); - - // 预览文章 - const preview = useCallback(() => { - if (id) { - if (!setting.systemUrl) { - message.error('尚未配置前台地址,无法正确构建预览地址'); - return; - } - window.open(resolveUrl(setting.systemUrl, '/article/' + id)); - } else { - message.warning('请先保存'); - } - }, [id, setting.systemUrl]); - - const deleteArticle = useCallback(() => { - if (!id) { - return; - } - const handle = () => { - ArticleProvider.deleteArticle(id).then(() => { - toggleHasSaved(true); - message.success('文章删除成功'); - Router.push('/article'); - }); - }; - Modal.confirm({ - title: '确认删除?', - content: '删除内容后,无法恢复。', - onOk: handle, - okText: '确认', - cancelText: '取消', - transitionName: '', - maskTransitionName: '', - }); - }, [id, toggleHasSaved]); - - const goback = useCallback(() => { - Router.push('/admin/article'); - }, []); - - useEffect(() => { - if (isCreate && id) { - Router.replace('/admin/article/editor/' + id); - } - }, [id, isCreate]); - - useWarningOnExit(!hasSaved, () => window.confirm('确认关闭?如果有内容变更,请先保存!')); - - return ( -
    - - {id ? `编辑文章 ${article.title ? '-' + article.title : ''}` : '新建文章'} - -
    - } />} - style={{ - borderBottom: '1px solid rgb(235, 237, 240)', - }} - onBack={goback} - title={ - - } - extra={[ - , - - - 查看 - - - 设置 - - - - 保存草稿 - - - - 删除 - - - } - > - - , - ]} - /> -
    -
    - { - patchArticle('content')(value); - patchArticle('html')(html); - patchArticle('toc')(toc); - }} - /> -
    - -
    - ); -}; diff --git a/client/src/components/ArticleList/index.module.scss b/client/src/components/ArticleList/index.module.scss deleted file mode 100644 index 5d1c64fa..00000000 --- a/client/src/components/ArticleList/index.module.scss +++ /dev/null @@ -1,265 +0,0 @@ -.wrapper { - overflow: hidden; - border-radius: var(--border-radius); - box-shadow: var(--box-shadow); - margin-top: 1rem; -} - -.articleItem { - position: relative; - display: flex; - justify-content: space-between; - overflow: hidden; - padding: 1rem; - background-color: var(--bg-box); - border-radius: var(--border-radius); - - .info { - display: flex; - align-items: center; - } - - &:hover { - img { - transform: scale(1.1); - transition: all 0.2s ease-in; - } - } - - .antBadge { - margin-left: 4px; - &:hover { - opacity: 0.7; - } - .category { - color: var(--second-text-color); - } - } - - .coverWrapper { - position: relative; - height: 114px; - width: 200px; - margin: 0 10px 0 0; - flex-shrink: 0; - overflow: hidden; - display: flex; - justify-content: center; - align-items: center; - border-radius: 5px; - cursor: pointer; - - img { - width: 100%; - height: 100%; - object-fit: cover; - } - } - - @media (max-width: 992px) { - .coverWrapper { - width: 180px; - } - } - - .badge { - position: absolute; - top: 20px; - left: -1px; - width: 5px; - height: 25px; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); - background-color: var(--primary-color); - } - - .link { - display: inline-block; - height: 100%; - width: 100%; - } - - .articleWrapper { - flex: 1; - } - - & + .articleItem { - margin-top: 1rem; - } - - &::after { - position: absolute; - bottom: 0rem; - width: calc(100% - 32px); - height: 1px; - // background: var(--border-color); - content: ''; - } - - &:last-of-type { - &::after { - height: 0; - } - } - - &:hover { - header .title { - color: var(--primary-color); - } - } - - header { - display: flex; - align-items: flex-start; - - .title { - overflow: hidden; - font-size: 16px; - font-weight: 600; - line-height: 22px; - color: var(--main-text-color); - text-overflow: ellipsis; - font-synthesis: style; - - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - overflow: hidden; - text-overflow: ellipsis; - line-height: 1.2em; - max-height: 2.4em; - } - - .time, - .category { - color: #fff; - } - } - - main { - display: flex; - flex-wrap: nowrap; - height: calc(100% - 28px); - - .coverWrapper { - position: relative; - width: 120px; - max-height: 100px; - min-height: 80px; - margin-left: 1.5rem; - overflow: hidden; - border-radius: var(--border-radius); - flex: 0 0 auto; - - &:hover { - transform: scale(1.2); - } - - img { - position: absolute; - top: 50%; - left: 50%; - width: 100%; - height: 100%; - transform: translate3d(-50%, -50%, 0); - object-fit: cover; - } - } - - .contentWrapper { - flex: 1 1 auto; - display: flex; - flex-direction: column; - justify-content: space-between; - - .desc { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - overflow: hidden; - font-size: 14px; - color: var(--second-text-color); - text-overflow: ellipsis; - line-height: 1.2em; - max-height: 2.4em; - max-width: 100%; - width: calc(100% - 24px); - } - - .meta { - width: 100%; - margin-top: 0.8rem; - font-size: 14px; - line-height: 20px; - color: #8590a6; - display: flex; - justify-content: space-between; - white-space: nowrap; - - .separator { - margin: 0 8px; - } - - .number { - margin-left: 6px; - color: var(--second-text-color); - } - .time { - > * { - margin-left: 6px; - } - } - } - } - } -} - -@media (max-width: 658px) { - .articleItem { - .coverWrapper { - width: 140px; - height: 80px; - } - - > a { - flex-direction: column; - } - - .info { - display: none; - } - - header { - flex-direction: column; - align-items: flex-start; - - .info { - font-size: 0.8em; - - > div:first-of-type { - display: none; - } - } - .category { - display: none; - } - } - - main { - .contentWrapper { - .desc { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 1; - overflow: hidden; - text-overflow: ellipsis; - line-height: 1.2em; - max-height: 2.4em; - } - - .time { - display: none; - } - } - } - } -} diff --git a/client/src/components/ArticleList/index.tsx b/client/src/components/ArticleList/index.tsx deleted file mode 100644 index a305bc19..00000000 --- a/client/src/components/ArticleList/index.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/** - * ArticleList Component - * - * This component displays a list of articles in a card format. - * Each article card includes: - * - Cover image (with lazy loading) - * - Title - * - Category tag - * - Summary - * - Meta information (likes, views, publish date) - * - * Features: - * - Lazy loading for images - * - Responsive design - * - Category navigation - * - Article statistics display - */ - -import { EyeOutlined, FolderOutlined, HeartOutlined, HistoryOutlined } from '@ant-design/icons'; -import { Spin, Tag } from 'antd'; -import { useTranslations } from 'next-intl'; -import Link from 'next/link'; -import React, { useContext, useMemo } from 'react'; -import LazyLoad from 'react-lazyload'; -import LogoSvg from '../../assets/LogoSvg'; - -import { LocaleTime } from '@/components/LocaleTime'; -import { GlobalContext } from '@/context/global'; -import { getColorFromNumber } from '@/utils'; -import style from './index.module.scss'; - -interface Article { - id: string; - title: string; - cover?: string; - summary: string; - category?: { - value: string; - label: string; - }; - likes: number; - views: number; - publishAt: string; -} - -interface ArticleListProps { - articles: Article[]; - coverHeight?: number; - asRecommend?: boolean; -} - -/** - * ArticleCard Component - * Renders a single article card with all its details - */ -const ArticleCard: React.FC<{ article: Article; categoryIndex: number }> = ({ article, categoryIndex }) => { - return ( - - ); -}; - -/** - * Main ArticleList Component - * Renders a list of article cards with proper handling of empty state - */ -export const ArticleList: React.FC = ({ articles = [] }) => { - const t = useTranslations(); - const { categories } = useContext(GlobalContext); - - // Memoize the category indices to avoid recalculating on every render - const categoryIndices = useMemo(() => { - return articles.map(article => - categories?.findIndex((category) => category?.value === article?.category?.value) - ); - }, [articles, categories]); - - return ( -
    - {articles && articles.length ? ( - articles.map((article, index) => ( - - )) - ) : ( -
    {t('empty')}
    - )} -
    - ); -}; diff --git a/client/src/components/ArticleRecommend/index.module.scss b/client/src/components/ArticleRecommend/index.module.scss deleted file mode 100644 index c4a27426..00000000 --- a/client/src/components/ArticleRecommend/index.module.scss +++ /dev/null @@ -1,152 +0,0 @@ -.wrapper { - margin-bottom: 1.3rem; - overflow: hidden; - line-height: 1.29; - - &.inline { - background-color: var(--bg-box); - border-radius: var(--border-radius); - box-shadow: var(--box-shadow); - } - - .recommendIcon { - margin-right: 8px; - } - - .title { - padding: 1rem; - font-weight: bold; - color: var(--main-text-color); - border-bottom: 1px solid var(--border-color); - } - - ul.inlineWrapper { - padding: 0 1rem 1rem; - - .article { - display: flex; - justify-content: space-between; - width: 100%; - .seqId { - display: inline-block; - width: 40px; - border-radius: 4px; - background-color: var(--primary-color); - } - .articleTitle { - flex: 1; - width: 100%; - display: inline-block; - overflow: hidden; - text-overflow: ellipsis; - &::before { - background-color: var(--primary-color); - color: #fff; - content: attr(data-num); - display: inline-block; - font-size: 14px; - line-height: 18px; - margin-right: 5px; - text-align: center; - width: 18px; - } - } - .views { - display: inline-block; - width: 54px; - color: var(--second-text-color); - } - } - - li { - display: flex; - flex-wrap: nowrap; - align-items: stretch; - padding-top: 1rem; - color: var(--second-text-color); - - > div:last-of-type { - display: flex; - align-items: center; - } - - a { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: inherit; - - span:first-of-type { - color: var(--main-text-color); - } - - &:hover { - color: var(--primary-color); - - span:first-of-type { - color: inherit; - } - } - } - - p { - margin: 0; - } - - img { - display: inline-block; - width: 6.8rem; - height: 3.8rem; - margin-right: 0.8rem; - } - } - } -} - -.articleItem { - position: relative; - width: 100%; - padding: 0; - margin-right: 14px; - overflow: hidden; - background: var(--bg-second); - border-radius: 5px; - box-shadow: var(--box-shadow); - transition: transform 0.3s; - - &:hover { - transform: scale(1.04); - } - - img { - width: 100%; - height: 123px; - border-top-right-radius: 5px; - border-top-left-radius: 5px; - object-fit: cover; - object-fit: cover; - } - - .title { - min-width: 225px; - padding: 12px; - margin-bottom: 0; - overflow: hidden; - font-size: 16px; - font-weight: 600; - line-height: 22px; - color: var(--main-text-color); - text-overflow: ellipsis; - white-space: nowrap; - border: 0; - font-synthesis: style; - } - - .meta { - width: 100%; - padding: 0 12px 12px; - font-size: 14px; - line-height: 20px; - color: #8590a6; - } -} diff --git a/client/src/components/ArticleRecommend/index.tsx b/client/src/components/ArticleRecommend/index.tsx deleted file mode 100644 index 25f72083..00000000 --- a/client/src/components/ArticleRecommend/index.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { ArticleList } from '@components/ArticleList'; -import { Spin } from 'antd'; -import cls from 'classnames'; -import { useTranslations } from 'next-intl'; -import Link from 'next/link'; -import React, { useEffect, useState } from 'react'; - -import { useAsyncLoading } from '@/hooks/useAsyncLoading'; -import { ArticleProvider } from '@/providers/article'; -import { LikeOutlined, EyeOutlined } from '@ant-design/icons'; - -import style from './index.module.scss'; - -interface IProps { - articleId?: string; - mode?: 'inline' | 'vertical'; - needTitle?: boolean; -} - -export const ArticleRecommend: React.FC = ({ mode = 'vertical', articleId = null, needTitle = true }) => { - const t = useTranslations(); - const [getRecommend, loading] = useAsyncLoading(ArticleProvider.getRecommend, 150, true); - const [fetched, setFetched] = useState(''); - const [articles, setArticles] = useState([]); - - useEffect(() => { - if (fetched === articleId) return; - getRecommend(articleId).then((res) => { - const articles = res.slice(0, 6); - articles.sort((a, b) => b.views - a.views); - setArticles(articles); - setFetched(articleId); - }); - }, [articleId, getRecommend, fetched]); - - return ( -
    - {needTitle && ( -
    - - {t('recommendToReading')} -
    - )} - - - {loading ? ( -
    - ) : mode === 'inline' ? ( - articles.length <= 0 ? ( - loading ? ( -
    - ) : ( -
    {t('empty')}
    - ) - ) : ( - - ) - ) : ( - - )} -
    -
    - ); -}; diff --git a/client/src/components/Categories/index.module.scss b/client/src/components/Categories/index.module.scss deleted file mode 100644 index fa76f47b..00000000 --- a/client/src/components/Categories/index.module.scss +++ /dev/null @@ -1,58 +0,0 @@ -.wrapper { - margin-bottom: 1.3rem; - overflow: hidden; - line-height: 1.29; - background-color: var(--bg-box); - border-radius: var(--border-radius); - box-shadow: var(--box-shadow); - - .title { - padding: 1rem 1.3rem; - font-weight: bold; - color: var(--main-text-color); - border-bottom: 1px solid var(--border-color); - } - - .categoryIcon { - margin-right: 8px; - } - - ul { - padding: 1rem; - } - - li { - padding: 8px 7px; - line-height: 1.5em; - color: var(--second-text-color); - border-radius: var(--border-radius); - transition: all ease-in-out 0.2s; - - a { - display: inline-flex; - justify-content: space-between; - width: 100%; - - > span:first-of-type { - color: var(var(--main-text-color)); - } - } - - &:hover { - color: var(--primary-color); - - a > span:first-of-type { - color: inherit; - } - } - - &.active { - color: var(--bg); - background-color: var(--primary-color); - - a > span:first-of-type { - color: inherit; - } - } - } -} diff --git a/client/src/components/Categories/index.tsx b/client/src/components/Categories/index.tsx deleted file mode 100644 index 93c9baf2..00000000 --- a/client/src/components/Categories/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useTranslations } from 'next-intl'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; - -import { FolderOutlined } from '@ant-design/icons'; - -import style from './index.module.scss'; - -export const Categories = ({ categories = [] }) => { - const t = useTranslations(); - - return ( -
    -
    - - {t('categoryTitle')} -
    - -
    - ); -}; diff --git a/client/src/components/Comment/CommentAction/CommentAction.tsx b/client/src/components/Comment/CommentAction/CommentAction.tsx deleted file mode 100644 index c148b106..00000000 --- a/client/src/components/Comment/CommentAction/CommentAction.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { Divider, Input, message, Modal, notification, Popconfirm } from 'antd'; -import React, { useCallback, useState } from 'react'; - -import { useSetting } from '@/hooks/useSetting'; -import { CommentProvider } from '@/providers/comment'; -import { SettingProvider } from '@/providers/setting'; - -import style from './index.module.scss'; - -export const CommentAction = ({ comment, refresh }) => { - const setting = useSetting(); - const [replyContent, setReplyContent] = useState(null); - const [replyVisible, setReplyVisible] = useState(false); - - // 修改评论 - const updateComment = useCallback( - (comment, pass = false) => { - CommentProvider.updateComment(comment.id, { pass }).then(() => { - message.success(pass ? '评论已通过' : '评论已拒绝'); - refresh(); - }); - }, - [refresh] - ); - - const reply = useCallback(() => { - if (!replyContent) { - return; - } - const userInfo = JSON.parse(window.localStorage.getItem('user')); - const email = (userInfo && userInfo.mail) || (setting && setting.smtpFromUser); - const notify = () => { - notification.error({ - message: '回复评论失败', - description: '请前往系统设置完善 SMTP 设置,前往个人中心更新个人邮箱。', - }); - }; - - const handle = (email) => { - const data = { - name: userInfo.name, - email, - content: replyContent, - parentCommentId: comment.parentCommentId || comment.id, - hostId: comment.hostId, - isHostInPage: comment.isHostInPage, - replyUserName: comment.name, - replyUserEmail: comment.email, - url: comment.url, - createByAdmin: true, - }; - - CommentProvider.addComment(data) - .then(() => { - message.success('回复成功'); - setReplyContent(''); - refresh(); - }) - .catch(() => notify()); - }; - - if (!email) { - SettingProvider.getSetting() - .then((res) => { - if (res && res.smtpFromUser) { - handle(res.smtpFromUser); - } else { - notify(); - } - setReplyVisible(false); - }) - .catch(() => { - notify(); - setReplyVisible(false); - }); - } else { - handle(email); - setReplyVisible(false); - } - }, [ - replyContent, - comment.email, - comment.hostId, - comment.id, - comment.isHostInPage, - comment.name, - comment.parentCommentId, - comment.url, - refresh, - setting, - ]); - - // 删除评论 - const deleteComment = useCallback( - (id) => { - CommentProvider.deleteComment(id).then(() => { - message.success('评论删除成功'); - refresh(); - }); - }, - [refresh] - ); - - return ( -
    - - updateComment(comment, true)}>通过 - - updateComment(comment, false)}>拒绝 - - setReplyVisible(true)}>回复 - - deleteComment(comment.id)} - okText="确认" - cancelText="取消" - > - 删除 - - - setReplyVisible(false)} - transitionName={''} - maskTransitionName={''} - > - { - const val = e.target.value; - setReplyContent(val); - }} - > - -
    - ); -}; diff --git a/client/src/components/Comment/CommentAction/CommentArticle.tsx b/client/src/components/Comment/CommentAction/CommentArticle.tsx deleted file mode 100644 index c7d34545..00000000 --- a/client/src/components/Comment/CommentAction/CommentArticle.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Popover } from 'antd'; -import React from 'react'; - -import { useSetting } from '@/hooks/useSetting'; -import { resolveUrl } from '@/utils'; - -import style from './index.module.scss'; - -export const CommentArticle = ({ comment }) => { - const setting = useSetting(); - const { url: link } = comment; - const href = resolveUrl(setting?.systemUrl, link); - - return ( - } placement={'right'} mouseEnterDelay={0.5}> - - 文章 - - - ); -}; diff --git a/client/src/components/Comment/CommentAction/CommentContent.tsx b/client/src/components/Comment/CommentAction/CommentContent.tsx deleted file mode 100644 index 11cf73a9..00000000 --- a/client/src/components/Comment/CommentAction/CommentContent.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Button, Popover } from 'antd'; -import React from 'react'; - -export const CommentContent = ({ comment }) => { - return ( - - - } - > - - - - ); -}; diff --git a/client/src/components/Comment/CommentAction/CommentHTML.tsx b/client/src/components/Comment/CommentAction/CommentHTML.tsx deleted file mode 100644 index fd358130..00000000 --- a/client/src/components/Comment/CommentAction/CommentHTML.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Button, Popover } from 'antd'; -import React from 'react'; - -export const CommentHTML = ({ comment }) => { - return ( - - - } - > - - - - ); -}; diff --git a/client/src/components/Comment/CommentAction/CommentStatus.tsx b/client/src/components/Comment/CommentAction/CommentStatus.tsx deleted file mode 100644 index 9588928d..00000000 --- a/client/src/components/Comment/CommentAction/CommentStatus.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Badge } from 'antd'; -import React from 'react'; - -export const CommentStatus = ({ comment }) => { - return ; -}; diff --git a/client/src/components/Comment/CommentAction/index.module.scss b/client/src/components/Comment/CommentAction/index.module.scss deleted file mode 100644 index 7bf73382..00000000 --- a/client/src/components/Comment/CommentAction/index.module.scss +++ /dev/null @@ -1,7 +0,0 @@ -.action a { - color: #1890ff; -} - -.link { - color: #1890ff; -} diff --git a/client/src/components/Comment/CommentEditor/Emoji/emojis.ts b/client/src/components/Comment/CommentEditor/Emoji/emojis.ts deleted file mode 100644 index 75a2633c..00000000 --- a/client/src/components/Comment/CommentEditor/Emoji/emojis.ts +++ /dev/null @@ -1,152 +0,0 @@ -export const emojis = { - grinning: '😀', - smiley: '😃', - smile: '😄', - grin: '😁', - laughing: '😆', - satisfied: '😆', - sweat_smile: '😅', - joy: '😂', - wink: '😉', - blush: '😊', - innocent: '😇', - heart_eyes: '😍', - kissing_heart: '😘', - kissing: '😗', - kissing_closed_eyes: '😚', - kissing_smiling_eyes: '😙', - yum: '😋', - stuck_out_tongue: '😛', - stuck_out_tongue_winking_eye: '😜', - stuck_out_tongue_closed_eyes: '😝', - neutral_face: '😐', - expressionless: '😑', - no_mouth: '😶', - smirk: '😏', - unamused: '😒', - relieved: '😌', - pensive: '😔', - sleepy: '😪', - sleeping: '😴', - mask: '😷', - dizzy_face: '😵', - sunglasses: '😎', - confused: '😕', - worried: '😟', - open_mouth: '😮', - hushed: '😯', - astonished: '😲', - flushed: '😳', - frowning: '😦', - anguished: '😧', - fearful: '😨', - cold_sweat: '😰', - disappointed_relieved: '😥', - cry: '😢', - sob: '😭', - scream: '😱', - confounded: '😖', - persevere: '😣', - disappointed: '😞', - sweat: '😓', - weary: '😩', - tired_face: '😫', - rage: '😡', - pout: '😡', - angry: '😠', - smiling_imp: '😈', - smiley_cat: '😺', - smile_cat: '😸', - joy_cat: '😹', - heart_eyes_cat: '😻', - smirk_cat: '😼', - kissing_cat: '😽', - scream_cat: '🙀', - crying_cat_face: '😿', - pouting_cat: '😾', - heart: '❤️', - hand: '✋', - raised_hand: '✋', - v: '✌️', - point_up: '☝️', - fist_raised: '✊', - fist: '✊', - monkey_face: '🐵', - cat: '🐱', - cow: '🐮', - mouse: '🐭', - coffee: '☕', - hotsprings: '♨️', - anchor: '⚓', - airplane: '✈️', - hourglass: '⌛', - watch: '⌚', - sunny: '☀️', - star: '⭐', - cloud: '☁️', - umbrella: '☔', - zap: '⚡', - snowflake: '❄️', - sparkles: '✨', - black_joker: '🃏', - mahjong: '🀄', - phone: '☎️', - telephone: '☎️', - envelope: '✉️', - pencil2: '✏️', - black_nib: '✒️', - scissors: '✂️', - wheelchair: '♿', - warning: '⚠️', - aries: '♈', - taurus: '♉', - gemini: '♊', - cancer: '♋', - leo: '♌', - virgo: '♍', - libra: '♎', - scorpius: '♏', - sagittarius: '♐', - capricorn: '♑', - aquarius: '♒', - pisces: '♓', - heavy_multiplication_x: '✖️', - heavy_plus_sign: '➕', - heavy_minus_sign: '➖', - heavy_division_sign: '➗', - bangbang: '‼️', - interrobang: '⁉️', - question: '❓', - grey_question: '❔', - grey_exclamation: '❕', - exclamation: '❗', - heavy_exclamation_mark: '❗', - wavy_dash: '〰️', - recycle: '♻️', - white_check_mark: '✅', - ballot_box_with_check: '☑️', - heavy_check_mark: '✔️', - x: '❌', - negative_squared_cross_mark: '❎', - curly_loop: '➰', - loop: '➿', - part_alternation_mark: '〽️', - eight_spoked_asterisk: '✳️', - eight_pointed_black_star: '✴️', - sparkle: '❇️', - copyright: '©️', - registered: '®️', - tm: '™️', - information_source: 'ℹ️', - m: 'Ⓜ️', - black_circle: '⚫', - white_circle: '⚪', - black_large_square: '⬛', - white_large_square: '⬜', - black_medium_square: '◼️', - white_medium_square: '◻️', - black_medium_small_square: '◾', - white_medium_small_square: '◽', - black_small_square: '▪️', - white_small_square: '▫️', -}; diff --git a/client/src/components/Comment/CommentEditor/Emoji/index.module.scss b/client/src/components/Comment/CommentEditor/Emoji/index.module.scss deleted file mode 100644 index 04887ca9..00000000 --- a/client/src/components/Comment/CommentEditor/Emoji/index.module.scss +++ /dev/null @@ -1,36 +0,0 @@ -.wrapper { - display: flex; - display: flex; - width: 390px; - height: 240px; - overflow: auto; - flex-wrap: wrap; - flex-wrap: wrap; - - li { - position: relative; - display: flex; - width: 32px; - height: 32px; - font-size: 18px; - cursor: pointer; - align-items: center; - justify-content: center; - } -} - -.text { - display: flex; - align-items: center; - color: var(--disable-text-color); - cursor: pointer; - - &:hover { - color: var(--primary-color); - } - - > span { - margin-left: 4px; - transform: translateY(1px); - } -} diff --git a/client/src/components/Comment/CommentEditor/Emoji/index.tsx b/client/src/components/Comment/CommentEditor/Emoji/index.tsx deleted file mode 100644 index 5843f2a4..00000000 --- a/client/src/components/Comment/CommentEditor/Emoji/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Popover } from 'antd'; -import React from 'react'; - -import { emojis } from './emojis'; -import styles from './index.module.scss'; - -export const Emoji: React.FC<{ onClickEmoji: (arg: string) => void }> = ({ onClickEmoji, children }) => { - return ( - - {Object.keys(emojis).map((key) => { - return ( -
  • onClickEmoji(emojis[key])}> - {emojis[key]} -
  • - ); - })} - - } - placement="bottomRight" - trigger="click" - > - {children} -
    - ); -}; diff --git a/client/src/components/Comment/CommentEditor/index.module.scss b/client/src/components/Comment/CommentEditor/index.module.scss deleted file mode 100644 index b408edc3..00000000 --- a/client/src/components/Comment/CommentEditor/index.module.scss +++ /dev/null @@ -1,60 +0,0 @@ -.wrapper { - main { - display: flex; - flex-wrap: nowrap; - } - - .textareaWrapper { - position: relative; - flex: 1; - margin-left: 16px; - - .mask { - position: absolute; - top: 0; - left: 0; - z-index: 1; - width: 100%; - height: 100%; - cursor: pointer; - } - - textarea { - border-color: var(--comment-editor-border-color); - color: var(--main-text-color); - background-color: var(--bg-second); - box-shadow: none !important; - } - } - - > footer { - display: flex; - justify-content: space-between; - padding-top: 8px; - padding-left: 44px; - - :global { - .ant-btn-primary[disabled] { - border-color: var(--comment-editor-border-color); - color: var(--main-text-color); - background-color: var(--comment-editor-disable-bg); - } - } - - .emojiTrigger { - display: flex; - align-items: center; - color: var(--disable-text-color); - cursor: pointer; - - &:hover { - color: var(--primary-color); - } - - > span { - margin-left: 4px; - transform: translateY(1px); - } - } - } -} diff --git a/client/src/components/Comment/CommentEditor/index.tsx b/client/src/components/Comment/CommentEditor/index.tsx deleted file mode 100644 index 7b9a0304..00000000 --- a/client/src/components/Comment/CommentEditor/index.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { Button, Input, message } from 'antd'; -import cls from 'classnames'; -import { useTranslations } from 'next-intl'; -import React, { useCallback, useContext, useMemo, useState } from 'react'; - -import { isValidUser, UserInfo } from '@/components/UserInfo'; -import { GlobalContext } from '@/context/global'; -import { useAsyncLoading } from '@/hooks/useAsyncLoading'; -import { useToggle } from '@/hooks/useToggle'; -import { CommentProvider } from '@/providers/comment'; - -import { Emoji } from './Emoji'; -import styles from './index.module.scss'; -import { default as Router } from 'next/router'; - -const { TextArea } = Input; - -interface Props { - hostId: string; - parentComment?: IComment; - replyComment?: IComment; - onOk?: () => void; - onClose?: () => void; - small?: boolean; -} - -export const CommentEditor: React.FC = ({ hostId, parentComment, replyComment, onOk, onClose, small }) => { - const t = useTranslations('commentNamespace'); - const { user } = useContext(GlobalContext); - const [addComment, loading] = useAsyncLoading(CommentProvider.addComment); - const [needSetInfo, toggleNeedSetInfo] = useToggle(false); - const [content, setContent] = useState(''); - // @ts-ignore - const hasValidUser = useMemo(() => isValidUser(user), [user]); - const textareaPlaceholder = useMemo( - () => (replyComment ? `${t('reply')} ${replyComment.name}` : t('replyPlaceholder')), - [t, replyComment] - ); - const textareaSize = useMemo(() => (small ? { minRows: 3, maxRows: 6 } : { minRows: 4, maxRows: 8 }), [small]); - const btnSize = useMemo(() => (small ? 'small' : 'middle'), [small]); - const emojiTrigger = ( - - - - - {t('emoji')} - - ); - - const onInput = useCallback( - (e) => { - if (!hasValidUser) { - return; - } - setContent(e.target.value); - }, - [hasValidUser] - ); - - const addEmoji = useCallback( - (emoji) => { - if (!hasValidUser) { - return; - } - setContent(`${content}${emoji}`); - }, - [content, hasValidUser] - ); - - const submit = useCallback(() => { - const data = { - hostId, - name: user.name, - email: user.email, - avatar: user.avatar || '', - content, - url: window.location.pathname, - }; - - if (parentComment && parentComment.id) { - Object.assign(data, { parentCommentId: parentComment.id }); - } - - if (replyComment) { - Object.assign(data, { - replyUserName: replyComment.name, - replyUserEmail: replyComment.email, - }); - } - - addComment(data).then(() => { - message.success(t('commentSuccess')); - setContent(''); - onOk && onOk(); - }); - }, [t, hostId, parentComment, replyComment, onOk, user, content, addComment]); - - return ( -
    -
    - -
    - {!hasValidUser && ( -
    { - if (user) { - message.warning(t('toggleNeedSetInfo')); - Router.push('/admin/ownspace'); - } else { - toggleNeedSetInfo(true); - } - }} - >
    - )} -