Product Setup
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- run: npm ci
|
||||
working-directory: frontend
|
||||
- run: npm run build
|
||||
working-directory: frontend
|
||||
|
||||
backend:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
MODULUS_DIRECT: '1'
|
||||
CONAN_HOME: build/build_common/conan_cache
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential ca-certificates cmake g++-12 gcc-12 \
|
||||
git ninja-build pkg-config python3 python3-venv
|
||||
python3 -m venv build/build_common/ci_python
|
||||
build/build_common/ci_python/bin/python -m pip install conan==2.26.2
|
||||
echo "$PWD/build/build_common/ci_python/bin" >> "$GITHUB_PATH"
|
||||
- name: Restore Conan cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: build/build_common/conan_cache
|
||||
key: product_fpol_conan_${{ runner.os }}_${{ hashFiles('conanfile.py', 'modulus.json') }}
|
||||
restore-keys: product_fpol_conan_${{ runner.os }}_
|
||||
- name: Configure
|
||||
run: python3 modulus.py configure --all --update
|
||||
- name: Check formatting
|
||||
run: |
|
||||
python3 -m pip install -r build/build_tools/docker/format_requirements.txt
|
||||
python3 modulus.py format --check
|
||||
- name: Build
|
||||
run: python3 modulus.py build --all
|
||||
- name: Test
|
||||
run: python3 modulus.py test --all
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
build/
|
||||
.clangd
|
||||
.nvim.lua
|
||||
.clang-format
|
||||
pyrightconfig.json
|
||||
CMakeUserPresets.json
|
||||
compile_commands.json
|
||||
CMakeFiles/
|
||||
CMakeCache.txt
|
||||
Testing/
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
*.log
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.local
|
||||
@@ -0,0 +1,32 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(product_fpol LANGUAGES CXX)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
include(CTest)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
find_package(modulus CONFIG REQUIRED COMPONENTS boost_json openssl)
|
||||
find_package(ligo CONFIG REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_executable(fpol_server src/main.cpp src/server.cpp)
|
||||
target_link_libraries(fpol_server PRIVATE modulus::modulus ligo::ligo Threads::Threads)
|
||||
target_compile_options(fpol_server PRIVATE $<$<CXX_COMPILER_ID:GNU,Clang>:-Wall;-Wextra;-Wpedantic>)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
find_package(Python3 REQUIRED COMPONENTS Interpreter)
|
||||
add_test(NAME product_fpol
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_server.py $<TARGET_FILE:fpol_server>
|
||||
)
|
||||
set_tests_properties(product_fpol PROPERTIES TIMEOUT 60)
|
||||
add_custom_target(check
|
||||
COMMAND ${CMAKE_CTEST_COMMAND} --test-dir ${CMAKE_BINARY_DIR} -R ^product_fpol$ --output-on-failure
|
||||
DEPENDS fpol_server
|
||||
USES_TERMINAL
|
||||
)
|
||||
endif()
|
||||
|
||||
install(TARGETS fpol_server RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
@@ -0,0 +1,96 @@
|
||||
# Fantasy Politics
|
||||
|
||||
`product-fpol` is a private, noncommercial fantasy-style game for friends around the 2026 US midterm elections. The eventual game will have a candidate draft, trades, and final standings based on real-world candidate performance.
|
||||
|
||||
This repository currently contains only the foundation: a basic webpage, a C++ HTTP server, and development tooling. There are no accounts, candidate records, league rules, drafts, trades, scores, or election data yet.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Linux with Docker available to your user and Python 3 for the backend tooling.
|
||||
- Node.js 22.12 or newer and npm for the frontend; CI uses Node 24.
|
||||
- Network access on the first setup to fetch npm packages, the build image, and public Conan dependencies.
|
||||
|
||||
Modulus supplies the shared C++23/GCC 12 build environment, Conan/CMake integration, formatting, and editor configuration. Ligo supplies HTTP networking and routing. Both are consumed from the public khodo Conan registry; sibling source checkouts are not required. Package reads do not need registry credentials.
|
||||
|
||||
## Development
|
||||
|
||||
From the repository root, configure and build the backend:
|
||||
|
||||
```sh
|
||||
python3 modulus.py configure
|
||||
python3 modulus.py build
|
||||
python3 modulus.py run
|
||||
```
|
||||
|
||||
In another terminal, start the frontend:
|
||||
|
||||
```sh
|
||||
cd frontend
|
||||
npm ci
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open **http://127.0.0.1:5173**. Vite reloads frontend edits and proxies `/api` to **http://127.0.0.1:8080**. Rebuild and restart the backend after C++ changes. Both configured ports fail explicitly when occupied.
|
||||
|
||||
Check the backend directly or through Vite:
|
||||
|
||||
```sh
|
||||
curl http://127.0.0.1:8080/api/health
|
||||
curl http://127.0.0.1:5173/api/health
|
||||
```
|
||||
|
||||
The only application API is `GET /api/health`, which returns HTTP 200 using Ligo's existing envelope:
|
||||
|
||||
```json
|
||||
{"ok":true,"payload":{"status":"ok"}}
|
||||
```
|
||||
|
||||
Unknown routes return HTTP 404 with Ligo's error envelope. Health is a process liveness check; there is no database or external data source to check yet.
|
||||
|
||||
## Built frontend
|
||||
|
||||
Build the frontend, then serve it with the C++ backend:
|
||||
|
||||
```sh
|
||||
npm --prefix frontend run build
|
||||
python3 modulus.py run -- --web-root frontend/dist
|
||||
```
|
||||
|
||||
Open **http://127.0.0.1:8080**. Node is only needed to build the assets; the C++ server serves the resulting HTML, JavaScript, and CSS. Assets are loaded at startup, so restart the server after rebuilding. `npm --prefix frontend run preview` is also available on port 4173, with the same API proxy, for local frontend previews.
|
||||
|
||||
The server accepts `--host ADDRESS`, `--port PORT`, `--web-root DIRECTORY`, and `--help`. It defaults to `127.0.0.1:8080` and API-only mode. An explicit web root must contain `index.html`. Only exact registered asset paths are served; there is no SPA fallback or query-string normalization in this scaffold. Hidden files and symlinks are excluded, and `/api` is reserved. Use only the Vite build output as the web root.
|
||||
|
||||
The Modulus runner binds the server to `0.0.0.0` **inside its container**, publishing port 8080 only on the host's loopback interface. An installed executable defaults to loopback. Port `0` selects an available port and is useful for tests. SIGINT and SIGTERM stop the server cleanly.
|
||||
|
||||
## Checks and editor support
|
||||
|
||||
```sh
|
||||
python3 modulus.py test
|
||||
python3 modulus.py format --check
|
||||
npm --prefix frontend run build
|
||||
```
|
||||
|
||||
The frontend build includes strict TypeScript checking. `npm --prefix frontend run typecheck` runs that check alone. The backend CTest smoke suite starts real server processes to check health, routing, binary/static assets, startup errors, port conflicts, and shutdown.
|
||||
|
||||
Check both native configurations:
|
||||
|
||||
```sh
|
||||
python3 modulus.py configure --all
|
||||
python3 modulus.py build --all
|
||||
python3 modulus.py test --all
|
||||
```
|
||||
|
||||
Configure generates ignored editor files. Customize `modulus.json`, not those generated files; refresh compile databases with `python3 modulus.py lsp`. C++ and Python use the shared Modulus formatting policy and full names rather than aliases. Run `python3 modulus.py format` before committing.
|
||||
|
||||
The checked-in `modulus.py` is copied unchanged from Modulus's canonical `modulus/bootstrap.py`. `configure --update` refreshes shared tooling and packages. `--no-remote` on configure uses only the project-local Conan cache, which must already contain dependencies. Keep build outputs and downloaded tooling untracked.
|
||||
|
||||
The Gitea workflow checks the frontend build plus backend formatting, debug/release builds, and tests. It has no publishing or deployment job. Its direct build mode provisions the shared compiler and tools on the CI runner, following the sibling repositories' convention. CMake installation installs the backend executable; distribute built frontend assets separately when deployment is added.
|
||||
|
||||
## Future product work
|
||||
|
||||
- Define league rules, eligible races and candidates, drafting, trades, and scoring together before introducing domain models.
|
||||
- Add persistence when needed; Noto is the existing SQLite library to evaluate then.
|
||||
- Choose an election data source later. Live feeds, scraping, update schedules, and correction handling remain undecided; this scaffold makes no external election requests.
|
||||
- Add accounts, invitations, deployment, and operational requirements when the game needs them.
|
||||
|
||||
Keep reusable networking and infrastructure in the shared libraries and product rules in this repository. Luma and Sonus are native UI/audio libraries and are not dependencies of this web application.
|
||||
@@ -0,0 +1,45 @@
|
||||
import conan
|
||||
import conan.tools.build
|
||||
import conan.tools.cmake
|
||||
|
||||
class product_fpol_conan(conan.ConanFile):
|
||||
name = "product_fpol"
|
||||
version = "latest"
|
||||
description = "Fantasy Politics web application scaffold"
|
||||
package_type = "application"
|
||||
settings = "os", "arch", "compiler", "build_type"
|
||||
options = {"with_tests": [True, False]}
|
||||
default_options = {"with_tests": False}
|
||||
python_requires = "modulus/latest"
|
||||
exports = "modulus.json"
|
||||
exports_sources = (
|
||||
"CMakeLists.txt",
|
||||
"src/*.cpp",
|
||||
"src/*.hpp",
|
||||
"tests/*.py",
|
||||
)
|
||||
def requirements(self):
|
||||
self.requires(str(self.python_requires["modulus"].ref), transitive_headers=True, transitive_libs=True, options={"with_openssl": True})
|
||||
self.requires("ligo/latest", transitive_headers=True, transitive_libs=True)
|
||||
def generate(self):
|
||||
dependencies = conan.tools.cmake.CMakeDeps(self)
|
||||
dependencies.generate()
|
||||
toolchain = conan.tools.cmake.CMakeToolchain(self)
|
||||
toolchain.user_presets_path = False
|
||||
toolchain.variables["BUILD_TESTING"] = bool(self.options.with_tests)
|
||||
toolchain.variables.update(self.python_requires["modulus"].module.python_cmake_variables(self))
|
||||
toolchain.generate()
|
||||
def build(self):
|
||||
cmake = conan.tools.cmake.CMake(self)
|
||||
cmake.configure()
|
||||
cmake.build()
|
||||
if self.options.with_tests and conan.tools.build.can_run(self):
|
||||
cmake.test(target="check")
|
||||
def package_id(self):
|
||||
self.info.options.rm_safe("with_tests")
|
||||
def package(self):
|
||||
conan.tools.cmake.CMake(self).install()
|
||||
def package_info(self):
|
||||
self.cpp_info.bindirs = ["bin"]
|
||||
self.cpp_info.libdirs = []
|
||||
self.cpp_info.includedirs = []
|
||||
@@ -0,0 +1,43 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#f6f4ee" />
|
||||
<meta name="description" content="Fantasy Politics: a private fantasy-style game for friends around the 2026 US midterm elections." />
|
||||
<title>Fantasy Politics</title>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<a class="wordmark" href="/" aria-label="Fantasy Politics home">
|
||||
<span class="monogram" aria-hidden="true">fp.</span>
|
||||
<span>Fantasy Politics</span>
|
||||
</a>
|
||||
<span class="edition">The 2026 edition</span>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="intro" aria-labelledby="page-title">
|
||||
<p class="eyebrow"><span aria-hidden="true">★</span> Friends. Friendly competition. The midterms.</p>
|
||||
<h1 id="page-title">Your picks.<br />Your party.<br /><em>Bragging rights.</em></h1>
|
||||
<p class="description">A fantasy-style game for our own little political league. Build a party of candidates, follow the 2026 US midterms, and see whose picks come out on top.</p>
|
||||
<p class="status"><span class="status-dot" aria-hidden="true"></span> The league is taking shape. More soon.</p>
|
||||
</section>
|
||||
|
||||
<section class="overview" aria-labelledby="overview-title">
|
||||
<div class="overview-heading">
|
||||
<h2 id="overview-title">The idea</h2>
|
||||
<span>On the drawing board</span>
|
||||
</div>
|
||||
<ol class="steps">
|
||||
<li><span class="step-number" aria-hidden="true">01</span><h3>Draft your party</h3><p>Pick your candidates when the league gets together for the draft.</p></li>
|
||||
<li><span class="step-number" aria-hidden="true">02</span><h3>Make your moves</h3><p>Trade with friends as the election season unfolds.</p></li>
|
||||
<li><span class="step-number" aria-hidden="true">03</span><h3>Watch it play out</h3><p>Real-world candidate performance will decide the final standings.</p></li>
|
||||
</ol>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer><span>A game among friends.</span><span>2026 US midterm elections</span></footer>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+828
@@ -0,0 +1,828 @@
|
||||
{
|
||||
"name": "product-fpol",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "product-fpol",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"typescript": "5.9.3",
|
||||
"vite": "8.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.143.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz",
|
||||
"integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz",
|
||||
"integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz",
|
||||
"integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz",
|
||||
"integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz",
|
||||
"integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz",
|
||||
"integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz",
|
||||
"integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz",
|
||||
"integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz",
|
||||
"integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz",
|
||||
"integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz",
|
||||
"integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz",
|
||||
"integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz",
|
||||
"integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz",
|
||||
"integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz",
|
||||
"integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
|
||||
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.33.0",
|
||||
"lightningcss-darwin-arm64": "1.33.0",
|
||||
"lightningcss-darwin-x64": "1.33.0",
|
||||
"lightningcss-freebsd-x64": "1.33.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.33.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.33.0",
|
||||
"lightningcss-linux-arm64-musl": "1.33.0",
|
||||
"lightningcss-linux-x64-gnu": "1.33.0",
|
||||
"lightningcss-linux-x64-musl": "1.33.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.33.0",
|
||||
"lightningcss-win32-x64-msvc": "1.33.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
|
||||
"integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
|
||||
"integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
|
||||
"integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
|
||||
"integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
|
||||
"integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
|
||||
"integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
|
||||
"integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
|
||||
"integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
|
||||
"integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
|
||||
"integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
|
||||
"integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"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": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.26",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.17",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz",
|
||||
"integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.143.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.2.3",
|
||||
"@rolldown/binding-darwin-arm64": "1.2.3",
|
||||
"@rolldown/binding-darwin-x64": "1.2.3",
|
||||
"@rolldown/binding-freebsd-x64": "1.2.3",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.3",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.2.3",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.2.3",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.3",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.2.3",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.2.3",
|
||||
"@rolldown/binding-linux-x64-musl": "1.2.3",
|
||||
"@rolldown/binding-openharmony-arm64": "1.2.3",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.2.3",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz",
|
||||
"integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.33.0",
|
||||
"picomatch": "^4.0.5",
|
||||
"postcss": "^8.5.25",
|
||||
"rolldown": "~1.2.1",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.4.0",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitejs/devtools": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "product-fpol",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"engines": { "node": ">=22.12.0" },
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "npm run typecheck && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "5.9.3",
|
||||
"vite": "8.2.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
import './style.css';
|
||||
@@ -0,0 +1,58 @@
|
||||
:root {
|
||||
font-family: 'Avenir Next', Avenir, 'Segoe UI', sans-serif;
|
||||
color: #213632;
|
||||
background: #f6f4ee;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
--muted: #596760;
|
||||
--line: #d6dbd2;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; }
|
||||
a { color: inherit; }
|
||||
a:focus-visible { outline: 3px solid #9a5a21; outline-offset: 6px; }
|
||||
.site-header, main, footer { width: min(1120px, calc(100% - 80px)); margin-inline: auto; }
|
||||
.site-header { min-height: 112px; display: flex; align-items: center; justify-content: space-between; gap: 24px; border-bottom: 1px solid var(--line); }
|
||||
.wordmark { display: inline-flex; align-items: center; gap: 14px; text-decoration: none; font-size: 17px; font-weight: 650; letter-spacing: -0.03em; }
|
||||
.monogram { display: grid; place-items: center; width: 43px; height: 43px; border: 1px solid #213632; border-radius: 50%; font-family: Georgia, serif; font-size: 25px; font-weight: 400; padding-bottom: 4px; }
|
||||
.edition { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.13em; }
|
||||
.intro { padding: 75px 0 70px; }
|
||||
.eyebrow { margin: 0 0 28px; color: var(--muted); font-size: 11px; font-weight: 650; text-transform: uppercase; letter-spacing: 0.12em; }
|
||||
.eyebrow span { color: #a36324; margin-right: 9px; font-size: 16px; }
|
||||
h1 { margin: 0; font-family: Georgia, 'Times New Roman', serif; font-size: clamp(48px, 6.5vw, 82px); font-weight: 400; line-height: 1.04; letter-spacing: -0.055em; }
|
||||
h1 em { font-weight: 400; color: #67765e; }
|
||||
.description { max-width: 520px; margin: 30px 0 26px; font-size: 17px; line-height: 1.75; color: var(--muted); }
|
||||
.status { display: inline-flex; align-items: center; gap: 10px; margin: 0; padding: 12px 16px; background: #e9ece3; border-radius: 3px; font-size: 12px; line-height: 1.6; }
|
||||
.status-dot { flex-shrink: 0; width: 6px; height: 6px; border-radius: 50%; background: #67765e; }
|
||||
.overview { padding-top: 25px; border-top: 1px solid var(--line); }
|
||||
.overview-heading { display: flex; justify-content: space-between; align-items: baseline; gap: 20px; }
|
||||
.overview-heading h2 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.14em; margin: 0; }
|
||||
.overview-heading > span { font-size: 12px; color: var(--muted); }
|
||||
.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 42px; margin: 30px 0 62px; padding: 0; list-style: none; }
|
||||
.step-number { color: #7c8973; font-family: Georgia, serif; font-size: 18px; }
|
||||
.steps h3 { margin: 16px 0 8px; font-size: 17px; font-weight: 600; letter-spacing: -0.02em; }
|
||||
.steps p { max-width: 285px; margin: 0; font-size: 14px; line-height: 1.7; color: var(--muted); }
|
||||
footer { display: flex; justify-content: space-between; gap: 20px; padding: 24px 0 30px; border-top: 1px solid var(--line); font-size: 11px; color: var(--muted); }
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
.intro { position: relative; }
|
||||
.intro::after { content: '26'; position: absolute; top: 78px; right: 30px; font-family: Georgia, serif; font-size: 285px; line-height: 1; letter-spacing: -0.09em; color: #e3e7dc; z-index: -1; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.site-header, main, footer { width: calc(100% - 40px); }
|
||||
.site-header { min-height: 88px; }
|
||||
.wordmark { font-size: 15px; gap: 10px; }
|
||||
.edition { max-width: 85px; text-align: right; font-size: 10px; line-height: 1.6; }
|
||||
.intro { padding: 48px 0; }
|
||||
.eyebrow { max-width: 290px; line-height: 1.8; }
|
||||
.description { font-size: 16px; }
|
||||
.steps { grid-template-columns: 1fr; gap: 28px; margin-bottom: 42px; }
|
||||
.steps li { display: grid; grid-template-columns: 28px 1fr; gap: 0 15px; }
|
||||
.step-number { grid-row: span 2; padding-top: 1px; }
|
||||
.steps h3 { margin-top: 0; }
|
||||
.steps p { max-width: none; }
|
||||
footer { flex-wrap: wrap; gap: 8px 20px; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src", "vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: '127.0.0.1',
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
proxy: {
|
||||
'/api': 'http://127.0.0.1:8080',
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
host: '127.0.0.1',
|
||||
port: 4173,
|
||||
strictPort: true,
|
||||
proxy: {
|
||||
'/api': 'http://127.0.0.1:8080',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"package": { "name": "product_fpol" },
|
||||
"tests": { "enabled": true, "name": "product_fpol" },
|
||||
"tooling": {
|
||||
"default_target": "x86_64_gcc12_debug",
|
||||
"remote": {
|
||||
"name": "khodo",
|
||||
"url": "https://git.thyme-labs.com/api/packages/khodo/conan"
|
||||
},
|
||||
"targets": {
|
||||
"x86_64_gcc12_debug": {
|
||||
"profile": "build/build_tools/conan/profiles/x86_64_gcc12_debug",
|
||||
"build_dir": "build/build_x86_debug",
|
||||
"build_type": "Debug",
|
||||
"can_run": true
|
||||
},
|
||||
"x86_64_gcc12_release": {
|
||||
"profile": "build/build_tools/conan/profiles/x86_64_gcc12_release",
|
||||
"build_dir": "build/build_x86_release",
|
||||
"build_type": "Release",
|
||||
"can_run": true
|
||||
}
|
||||
},
|
||||
"lsp": { "enabled": true },
|
||||
"run": {
|
||||
"enabled": true,
|
||||
"executable": "fpol_server",
|
||||
"args": ["--host", "0.0.0.0", "--port", "8080"],
|
||||
"ports": ["127.0.0.1:8080:8080"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+347
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env python3
|
||||
# Canonical consumer bootstrap. Copy this file unchanged to a project's modulus.py.
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
PACKAGE_REF = "modulus/latest"
|
||||
CONAN_VERSION = "2.26.2"
|
||||
FETCH_IMAGE = "modulus_fetch:dev"
|
||||
COMMANDS = (
|
||||
"configure",
|
||||
"build",
|
||||
"test",
|
||||
"package",
|
||||
"lsp",
|
||||
"clangd",
|
||||
"format",
|
||||
"run",
|
||||
"python",
|
||||
"publish",
|
||||
)
|
||||
COMMON_DIR = pathlib.Path("build") / "build_common"
|
||||
TOOLS_DIR = pathlib.Path("build") / "build_tools"
|
||||
RUNTIME_DIR = COMMON_DIR / "modulus"
|
||||
CONAN_HOME_DIR = COMMON_DIR / "conan_cache"
|
||||
DEFAULT_REMOTE_NAME = "khodo"
|
||||
DEFAULT_REMOTE_URL = "https://git.thyme-labs.com/api/packages/khodo/conan"
|
||||
DEFAULT_DOCKER_MARKER = "/opt/modulus_build_image"
|
||||
|
||||
def repo_root():
|
||||
root = pathlib.Path(__file__).resolve().parent
|
||||
if not (root / "modulus.json").is_file():
|
||||
raise SystemExit("modulus.py must be run from a Modulus-managed repository root")
|
||||
return root
|
||||
|
||||
def read_json(path):
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise SystemExit(f"failed to read {path}: {error}") from error
|
||||
if not isinstance(data, dict):
|
||||
raise SystemExit(f"{path} must contain a JSON object")
|
||||
return data
|
||||
|
||||
def remote_config(root):
|
||||
config = read_json(root / "modulus.json")
|
||||
remote = config.get(
|
||||
"tooling",
|
||||
{},
|
||||
).get("remote", {})
|
||||
if not isinstance(remote, dict):
|
||||
remote = {}
|
||||
return (
|
||||
str(remote.get("name") or DEFAULT_REMOTE_NAME),
|
||||
str(remote.get("url") or DEFAULT_REMOTE_URL),
|
||||
)
|
||||
|
||||
def runtime_ready(root):
|
||||
return (
|
||||
(root / TOOLS_DIR / "configure.py").is_file()
|
||||
and (root / TOOLS_DIR / "lsp.py").is_file()
|
||||
and (root / TOOLS_DIR / "clangd.py").is_file()
|
||||
and (root / RUNTIME_DIR / "modulus" / "cli.py").is_file()
|
||||
and (root / RUNTIME_DIR / "modulus" / "export.py").is_file()
|
||||
and (root / TOOLS_DIR / "format.py").is_file()
|
||||
and (root / RUNTIME_DIR / "modulus" / "formatting.py").is_file()
|
||||
and (root / RUNTIME_DIR / "modulus" / "format_layout.py").is_file()
|
||||
and (root / RUNTIME_DIR / "modulus" / "python.py").is_file()
|
||||
and (root / RUNTIME_DIR / "modulus" / "artifacts.py").is_file()
|
||||
and (root / TOOLS_DIR / "python_runner.py").is_file()
|
||||
)
|
||||
|
||||
def env_flag(name):
|
||||
return os.environ.get(name, "").lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
def docker_marker(root):
|
||||
config = read_json(root / "modulus.json")
|
||||
marker = config.get(
|
||||
"tooling",
|
||||
{},
|
||||
).get("docker", {}).get("marker", DEFAULT_DOCKER_MARKER)
|
||||
return pathlib.Path(str(marker))
|
||||
|
||||
def direct_mode(root):
|
||||
return env_flag("MODULUS_DIRECT") or docker_marker(root).exists()
|
||||
|
||||
def run(command, input_text=None, capture=False, display_command=None, env=None, check=True):
|
||||
visible = display_command if display_command is not None else command
|
||||
print("+", " ".join(shlex.quote(str(part)) for part in visible))
|
||||
completed = subprocess.run(
|
||||
[str(part) for part in command],
|
||||
input=input_text,
|
||||
stdout=subprocess.PIPE if capture else None,
|
||||
stderr=subprocess.PIPE if capture else None,
|
||||
text=True,
|
||||
check=False,
|
||||
env=env,
|
||||
)
|
||||
if check and completed.returncode != 0:
|
||||
if capture and completed.stdout:
|
||||
print(completed.stdout, end="")
|
||||
if capture and completed.stderr:
|
||||
print(completed.stderr, end="", file=sys.stderr)
|
||||
raise SystemExit(completed.returncode)
|
||||
return completed
|
||||
|
||||
def fetch_dockerfile():
|
||||
return f"""FROM debian:bookworm-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update \\
|
||||
&& apt-get install -y --no-install-recommends \\
|
||||
ca-certificates \\
|
||||
python3 \\
|
||||
python3-venv \\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN python3 -m venv /opt/modulus_python
|
||||
|
||||
ENV PATH=/opt/modulus_python/bin:$PATH
|
||||
|
||||
RUN python -m pip install --no-cache-dir conan=={CONAN_VERSION}
|
||||
|
||||
WORKDIR /workspace
|
||||
"""
|
||||
|
||||
def ensure_fetch_image(offline=False):
|
||||
inspected = subprocess.run(
|
||||
[
|
||||
"docker",
|
||||
"image",
|
||||
"inspect",
|
||||
FETCH_IMAGE,
|
||||
],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
if inspected.returncode == 0 and (offline or not docker_platform_args()):
|
||||
return
|
||||
if offline:
|
||||
raise SystemExit('offline bootstrap requires the existing Modulus fetch image or MODULUS_DIRECT=1 with Conan installed')
|
||||
run(
|
||||
[
|
||||
"docker",
|
||||
"build",
|
||||
*docker_platform_args(),
|
||||
"--tag",
|
||||
FETCH_IMAGE,
|
||||
"-",
|
||||
],
|
||||
input_text=fetch_dockerfile(),
|
||||
)
|
||||
|
||||
def in_container():
|
||||
return pathlib.Path("/.dockerenv").exists()
|
||||
|
||||
def docker_platform_args():
|
||||
platform = os.environ.get("MODULUS_DOCKER_PLATFORM", "").strip()
|
||||
return ["--platform", platform] if platform else []
|
||||
|
||||
def docker_workspace_args(root):
|
||||
if in_container():
|
||||
container = os.environ.get("HOSTNAME", "").strip()
|
||||
if not container:
|
||||
raise SystemExit("HOSTNAME is required to share the CI workspace with the Modulus fetch container")
|
||||
return [
|
||||
"--volumes-from",
|
||||
container,
|
||||
]
|
||||
return [
|
||||
"--volume",
|
||||
f"{root}:{root}",
|
||||
]
|
||||
|
||||
def fetch_env(root):
|
||||
env = os.environ.copy()
|
||||
for name in list(env):
|
||||
if name in {"CONAN_USER", "CONAN_TOKEN"} or name.startswith(("CONAN_LOGIN_USERNAME", "CONAN_PASSWORD")):
|
||||
del env[name]
|
||||
env["CONAN_HOME"] = str(root / CONAN_HOME_DIR)
|
||||
env["CONAN_NON_INTERACTIVE"] = "1"
|
||||
return env
|
||||
|
||||
def run_fetch(root, command, capture=False, display_command=None, check=True, offline=False):
|
||||
if direct_mode(root):
|
||||
return run(command, capture=capture, display_command=display_command, env=fetch_env(root), check=check)
|
||||
ensure_fetch_image(offline=offline)
|
||||
docker_command = [
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--interactive",
|
||||
*(["--pull=never", "--network=none"] if offline else []),
|
||||
*docker_platform_args(),
|
||||
"--user",
|
||||
f"{os.getuid()}:{os.getgid()}",
|
||||
"--env",
|
||||
"HOME=/tmp",
|
||||
"--env",
|
||||
"CONAN_HOME",
|
||||
"--env",
|
||||
"CONAN_NON_INTERACTIVE",
|
||||
*docker_workspace_args(root),
|
||||
"--workdir",
|
||||
str(root),
|
||||
FETCH_IMAGE,
|
||||
*command,
|
||||
]
|
||||
return run(docker_command, capture=capture, display_command=display_command, env=fetch_env(root), check=check)
|
||||
|
||||
def configure_remote(root, remote_name, remote_url):
|
||||
run_fetch(
|
||||
root,
|
||||
[
|
||||
"conan",
|
||||
"remote",
|
||||
"add",
|
||||
"-f",
|
||||
remote_name,
|
||||
remote_url,
|
||||
],
|
||||
)
|
||||
run_fetch(root, ["conan", "remote", "logout", remote_name])
|
||||
|
||||
def package_refs(data):
|
||||
refs = []
|
||||
for location_data in data.values():
|
||||
if not isinstance(location_data, dict):
|
||||
continue
|
||||
for ref, ref_data in location_data.items():
|
||||
if not ref.startswith(PACKAGE_REF) or not isinstance(ref_data, dict):
|
||||
continue
|
||||
revisions = ref_data.get(
|
||||
"revisions",
|
||||
{},
|
||||
)
|
||||
if not isinstance(revisions, dict):
|
||||
continue
|
||||
for recipe_revision, revision_data in revisions.items():
|
||||
if not isinstance(revision_data, dict):
|
||||
continue
|
||||
packages = revision_data.get(
|
||||
"packages",
|
||||
{},
|
||||
)
|
||||
if not isinstance(packages, dict):
|
||||
continue
|
||||
for package_id, package_data in packages.items():
|
||||
package_revisions = package_data.get(
|
||||
"revisions",
|
||||
{},
|
||||
)if isinstance(package_data, dict) else {}
|
||||
if isinstance(package_revisions, dict) and package_revisions:
|
||||
for package_revision in package_revisions:
|
||||
refs.append(f"{PACKAGE_REF}#{recipe_revision}:{package_id}#{package_revision}")
|
||||
else:
|
||||
refs.append(f"{PACKAGE_REF}#{recipe_revision}:{package_id}")
|
||||
return sorted(refs)
|
||||
|
||||
def find_modulus(root, remote_name=None):
|
||||
completed = run_fetch(
|
||||
root,
|
||||
[
|
||||
"conan",
|
||||
"list",
|
||||
PACKAGE_REF + "#latest:*#latest",
|
||||
*(["-r", remote_name] if remote_name else ["-c"]),
|
||||
"-f",
|
||||
"json",
|
||||
],
|
||||
capture=True,
|
||||
offline=remote_name is None,
|
||||
)
|
||||
refs = package_refs(json.loads(completed.stdout))
|
||||
if not refs:
|
||||
raise SystemExit(f"no {PACKAGE_REF} package available in {remote_name or 'the local Conan cache'}")
|
||||
package_ref = refs[-1]
|
||||
if remote_name:
|
||||
run_fetch(root, ["conan", "download", package_ref, "-r", remote_name])
|
||||
completed = run_fetch(
|
||||
root,
|
||||
[
|
||||
"conan",
|
||||
"cache",
|
||||
"path",
|
||||
package_ref,
|
||||
],
|
||||
capture=True,
|
||||
offline=remote_name is None,
|
||||
)
|
||||
package_root = pathlib.Path(completed.stdout.strip())
|
||||
if not (package_root / "modulus" / "export.py").is_file() or not (package_root / "tools").is_dir():
|
||||
raise SystemExit(f"downloaded {PACKAGE_REF} package does not contain Modulus export files: {package_root}")
|
||||
return package_root
|
||||
|
||||
def materialize_runtime(root, no_remote=False):
|
||||
remote_name, remote_url = remote_config(root)
|
||||
if not no_remote:
|
||||
configure_remote(root, remote_name, remote_url)
|
||||
package_root = find_modulus(root, None if no_remote else remote_name)
|
||||
sys.path.insert(0, str(package_root))
|
||||
import modulus.export
|
||||
modulus.export.sync_project(root, package_root)
|
||||
|
||||
def ensure_runtime(root, update=False, no_remote=False):
|
||||
if update or not runtime_ready(root):
|
||||
materialize_runtime(root, no_remote=no_remote)
|
||||
|
||||
def delegate(command, argv):
|
||||
root = repo_root()
|
||||
if command in {"clangd", "format"}:
|
||||
if not runtime_ready(root):
|
||||
print(f"Modulus {command} tooling is missing; run ./modulus.py configure first", file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
ensure_runtime(root, update=command == "configure" and "--update" in argv, no_remote="--no-remote" in argv)
|
||||
sys.path.insert(0, str(root / RUNTIME_DIR))
|
||||
import modulus.cli
|
||||
previous_argv = sys.argv
|
||||
previous_cwd = pathlib.Path.cwd()
|
||||
sys.argv = [
|
||||
str(root / "modulus.py"),
|
||||
*argv,
|
||||
]
|
||||
os.chdir(root)
|
||||
try:
|
||||
return modulus.cli.main(command)
|
||||
finally:
|
||||
os.chdir(previous_cwd)
|
||||
sys.argv = previous_argv
|
||||
|
||||
def main(argv):
|
||||
if not argv or argv[0] in ("-h", "--help"):
|
||||
print("usage: python3 modulus.py " + "|".join(COMMANDS) + " ...")
|
||||
return 0 if argv else 2
|
||||
command = argv[0]
|
||||
if command not in COMMANDS:
|
||||
raise SystemExit(f"unknown Modulus command: {command}")
|
||||
return delegate(command, argv[1:])
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "server.hpp"
|
||||
|
||||
#include <charconv>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <modulus.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto parse_options(int argc, char **argv) -> fpol::server_options {
|
||||
auto options = fpol::server_options{};
|
||||
for (auto index = 1; index < argc; ++index) {
|
||||
auto name = std::string_view{argv[index]};
|
||||
if (name != "--host" && name != "--port" && name != "--web-root") {
|
||||
throw std::invalid_argument("Unknown option: " + std::string{name});
|
||||
}
|
||||
if (++index == argc || std::string_view{argv[index]}.empty()) {
|
||||
throw std::invalid_argument("Missing value for " + std::string{name});
|
||||
}
|
||||
auto value = std::string_view{argv[index]};
|
||||
if (name == "--host") {
|
||||
options.host = value;
|
||||
} else if (name == "--web-root") {
|
||||
options.web_root = value;
|
||||
} else {
|
||||
auto port = 0U;
|
||||
auto parsed = std::from_chars(value.data(), value.data() + value.size(), port);
|
||||
if (parsed.ec != std::errc{} || parsed.ptr != value.data() + value.size() || port > 65535U) {
|
||||
throw std::invalid_argument("Port must be an integer from 0 to 65535");
|
||||
}
|
||||
options.port = static_cast<std::uint16_t>(port);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
auto main(int argc, char **argv) -> int {
|
||||
if (argc == 2 && std::string_view{argv[1]} == "--help") {
|
||||
std::cout << "Usage: fpol_server [--host ADDRESS] [--port PORT] [--web-root DIRECTORY]\n"
|
||||
"Defaults: 127.0.0.1:8080, API only. Port 0 selects an available port.\n";
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return fpol::run(parse_options(argc, argv));
|
||||
} catch (std::exception const &error) {
|
||||
modulus::log_error(error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
#include "server.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
#include <exception>
|
||||
#include <expected>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
|
||||
#include <ligo.hpp>
|
||||
#include <modulus.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto content_type(std::filesystem::path const &path) -> std::string {
|
||||
auto extension = path.extension().string();
|
||||
if (extension == ".html") {
|
||||
return "text/html; charset=utf-8";
|
||||
}
|
||||
if (extension == ".css") {
|
||||
return "text/css; charset=utf-8";
|
||||
}
|
||||
if (extension == ".js") {
|
||||
return "text/javascript; charset=utf-8";
|
||||
}
|
||||
if (extension == ".json") {
|
||||
return "application/json";
|
||||
}
|
||||
if (extension == ".svg") {
|
||||
return "image/svg+xml";
|
||||
}
|
||||
if (extension == ".png") {
|
||||
return "image/png";
|
||||
}
|
||||
if (extension == ".ico") {
|
||||
return "image/x-icon";
|
||||
}
|
||||
if (extension == ".woff2") {
|
||||
return "font/woff2";
|
||||
}
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
auto add_asset(ligo::service_router &router, std::string route, std::filesystem::path const &path) -> void {
|
||||
auto file = std::ifstream{path, std::ios::binary};
|
||||
if (!file) {
|
||||
throw std::runtime_error("Cannot read web asset: " + path.string());
|
||||
}
|
||||
auto body = std::string{std::istreambuf_iterator<char>{file}, std::istreambuf_iterator<char>{}};
|
||||
if (file.bad()) {
|
||||
throw std::runtime_error("Failed reading web asset: " + path.string());
|
||||
}
|
||||
auto response = std::make_shared<ligo::service_response const>(ligo::text_response(content_type(path), std::move(body)));
|
||||
router.add_response_route("GET", std::move(route), [response](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
|
||||
co_return *response;
|
||||
});
|
||||
}
|
||||
|
||||
auto add_web_routes(ligo::service_router &router, std::filesystem::path const &root) -> void {
|
||||
if (std::filesystem::is_symlink(root) || !std::filesystem::is_directory(root)) {
|
||||
throw std::invalid_argument("Web root must be a directory, not a symlink: " + root.string());
|
||||
}
|
||||
auto index = root / "index.html";
|
||||
if (std::filesystem::is_symlink(index) || !std::filesystem::is_regular_file(index)) {
|
||||
throw std::invalid_argument("Web root must contain index.html; run npm run build in frontend first");
|
||||
}
|
||||
// Register only existing build artifacts, never resolve request paths on disk.
|
||||
// Assets are read once and remain unchanged until the server restarts.
|
||||
add_asset(router, "/", index);
|
||||
for (auto const &entry : std::filesystem::recursive_directory_iterator{root}) {
|
||||
if (entry.is_symlink() || !entry.is_regular_file()) {
|
||||
continue;
|
||||
}
|
||||
auto relative = entry.path().lexically_relative(root);
|
||||
auto hidden = false;
|
||||
for (auto const &part : relative) {
|
||||
if (part.string().starts_with('.')) {
|
||||
hidden = true;
|
||||
}
|
||||
}
|
||||
auto route = "/" + relative.generic_string();
|
||||
if (hidden || route == "/api" || route.starts_with("/api/")) {
|
||||
continue;
|
||||
}
|
||||
add_asset(router, std::move(route), entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
auto stop_server(ligo::http::server &server) -> boost::asio::awaitable<void> {
|
||||
auto result = co_await server.async_stop(std::chrono::milliseconds{3000});
|
||||
if (!result) {
|
||||
modulus::log_warn(result.error().describe());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
auto fpol::run(fpol::server_options const &options) -> int {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto router = ligo::service_router{};
|
||||
router.add_route("GET", "/api/health", [](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
|
||||
co_return boost::json::object{{"status", "ok"}};
|
||||
});
|
||||
if (!options.web_root.empty()) {
|
||||
add_web_routes(router, options.web_root);
|
||||
}
|
||||
auto http_options = ligo::http::server_options{};
|
||||
http_options.on_error = [](modulus::error error) { modulus::log_warn(error.describe()); };
|
||||
auto server = ligo::http::server{io, {.host = options.host, .port = options.port}, std::move(router), std::move(http_options)};
|
||||
auto signals = boost::asio::signal_set{io, SIGINT, SIGTERM};
|
||||
auto started = server.start();
|
||||
if (!started) {
|
||||
modulus::log_error(started.error().describe());
|
||||
return 1;
|
||||
}
|
||||
auto exit_code = 0;
|
||||
signals.async_wait([&](boost::system::error_code error, int) {
|
||||
if (!error) {
|
||||
modulus::log_info("Stopping server");
|
||||
boost::asio::co_spawn(io, stop_server(server), [&](std::exception_ptr failure) {
|
||||
if (failure) {
|
||||
modulus::log_error("Server shutdown failed");
|
||||
exit_code = 1;
|
||||
}
|
||||
io.stop();
|
||||
});
|
||||
}
|
||||
});
|
||||
auto host = options.host.find(':') == std::string::npos ? options.host : "[" + options.host + "]";
|
||||
modulus::log_info("Listening on http://" + host + ":" + std::to_string(server.selected_port()));
|
||||
io.run();
|
||||
return exit_code;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
|
||||
namespace fpol {
|
||||
|
||||
struct server_options {
|
||||
std::string host{"127.0.0.1"};
|
||||
std::uint16_t port{8080};
|
||||
std::filesystem::path web_root{};
|
||||
};
|
||||
|
||||
auto run(fpol::server_options const &options) -> int;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import contextlib
|
||||
import http.client
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
|
||||
SERVER = pathlib.Path(sys.argv.pop(1)).resolve()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def running_server(*arguments, stop_signal=signal.SIGTERM):
|
||||
with tempfile.TemporaryDirectory(prefix="fpol_test_") as directory:
|
||||
log_path = pathlib.Path(directory) / "server.log"
|
||||
with log_path.open("w") as log:
|
||||
process = subprocess.Popen([str(SERVER), "--port", "0", *map(str, arguments)], stdout=log, stderr=log)
|
||||
try:
|
||||
deadline = time.monotonic() + 10
|
||||
while time.monotonic() < deadline:
|
||||
output = log_path.read_text()
|
||||
match = re.search(r"Listening on http://127\.0\.0\.1:(\d+)", output)
|
||||
if match and process.poll() is None:
|
||||
yield int(match.group(1))
|
||||
break
|
||||
if process.poll() is not None:
|
||||
raise AssertionError(f"Server exited during startup:\n{output}")
|
||||
time.sleep(0.02)
|
||||
else:
|
||||
raise AssertionError(f"Server did not start:\n{log_path.read_text()}")
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.send_signal(stop_signal)
|
||||
try:
|
||||
return_code = process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait()
|
||||
raise AssertionError("Server did not shut down within five seconds") from None
|
||||
if return_code != 0:
|
||||
raise AssertionError(f"Server exited with {return_code}:\n{log_path.read_text()}")
|
||||
|
||||
def request(port, path, method="GET"):
|
||||
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=3)
|
||||
try:
|
||||
connection.request(method, path)
|
||||
response = connection.getresponse()
|
||||
return response.status, response.getheader("Content-Type"), response.read()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
class server_tests(unittest.TestCase):
|
||||
def test_health_and_api_only_routes(self):
|
||||
with running_server() as port:
|
||||
status, content_type, body = request(port, "/api/health")
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(content_type, "application/json")
|
||||
self.assertEqual(json.loads(body), {"ok": True, "payload": {"status": "ok"}})
|
||||
for path in ("/", "/api/unknown", "/missing.js"):
|
||||
with self.subTest(path=path):
|
||||
status, _, body = request(port, path)
|
||||
self.assertEqual(status, 404)
|
||||
self.assertEqual(json.loads(body)["code"], "not_found")
|
||||
self.assertEqual(request(port, "/api/health", "POST")[0], 404)
|
||||
def test_static_assets_and_route_boundaries(self):
|
||||
with tempfile.TemporaryDirectory(prefix="fpol_web_") as directory:
|
||||
root = pathlib.Path(directory)
|
||||
(root / "assets").mkdir()
|
||||
(root / "api").mkdir()
|
||||
(root / "index.html").write_text("<!doctype html><title>Fantasy Politics</title>")
|
||||
(root / "assets" / "app.js").write_text("console.log('ready');")
|
||||
(root / "assets" / "app.css").write_text("body { color: green; }")
|
||||
(root / "assets" / "image.png").write_bytes(b"\x89PNG\r\n\x1a\n\x00\xff")
|
||||
(root / ".env").write_text("not a public asset")
|
||||
(root / ".hidden").mkdir()
|
||||
(root / ".hidden" / "data.txt").write_text("not a public asset")
|
||||
(root / "api" / "health").write_text("must not replace the health route")
|
||||
(root / "linked.html").symlink_to(root / "index.html")
|
||||
(root / "linked_assets").symlink_to(root / "assets", target_is_directory=True)
|
||||
with running_server("--web-root", root) as port:
|
||||
for route, filename, content_type in (
|
||||
("/", "index.html", "text/html; charset=utf-8"),
|
||||
("/index.html", "index.html", "text/html; charset=utf-8"),
|
||||
("/assets/app.js", "assets/app.js", "text/javascript; charset=utf-8"),
|
||||
("/assets/app.css", "assets/app.css", "text/css; charset=utf-8"),
|
||||
("/assets/image.png", "assets/image.png", "image/png"),
|
||||
):
|
||||
with self.subTest(route=route):
|
||||
self.assertEqual(request(port, route), (200, content_type, (root / filename).read_bytes()))
|
||||
for route in ("/linked.html", "/linked_assets/app.js", "/.env", "/.hidden/data.txt", "/../index.html", "/%2e%2e/index.html", "/missing.js", "/api/missing"):
|
||||
with self.subTest(route=route):
|
||||
self.assertEqual(request(port, route)[0], 404)
|
||||
self.assertEqual(json.loads(request(port, "/api/health")[2])["payload"], {"status": "ok"})
|
||||
def test_invalid_startup(self):
|
||||
with tempfile.TemporaryDirectory(prefix="fpol_invalid_") as directory:
|
||||
root = pathlib.Path(directory)
|
||||
file = root / "file.txt"
|
||||
file.write_text("not a web root")
|
||||
for arguments, diagnostic in (
|
||||
(["--unknown"], "Unknown option"),
|
||||
(["--port"], "Missing value"),
|
||||
(["--port", "abc"], "Port must be an integer"),
|
||||
(["--port", "65536"], "Port must be an integer"),
|
||||
(["--port", "-1"], "Port must be an integer"),
|
||||
(["--port", "80x"], "Port must be an integer"),
|
||||
(["--host", ""], "Missing value"),
|
||||
(["--web-root", root / "missing"], "Web root must be a directory"),
|
||||
(["--web-root", file], "Web root must be a directory"),
|
||||
(["--web-root", root], "Web root must contain index.html"),
|
||||
):
|
||||
with self.subTest(arguments=arguments):
|
||||
result = subprocess.run([str(SERVER), *map(str, arguments)], capture_output=True, text=True, timeout=5)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("[error]", result.stderr)
|
||||
self.assertIn(diagnostic, result.stderr)
|
||||
(root / "index.html").symlink_to(file)
|
||||
result = subprocess.run([str(SERVER), "--web-root", str(root)], capture_output=True, text=True, timeout=5)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("Web root must contain index.html", result.stderr)
|
||||
def test_occupied_port(self):
|
||||
with socket.socket() as listener:
|
||||
listener.bind(("127.0.0.1", 0))
|
||||
listener.listen()
|
||||
result = subprocess.run([str(SERVER), "--port", str(listener.getsockname()[1])], capture_output=True, text=True, timeout=5)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("[error]", result.stderr)
|
||||
def test_help(self):
|
||||
result = subprocess.run([str(SERVER), "--help"], capture_output=True, text=True, timeout=5)
|
||||
self.assertEqual(result.returncode, 0)
|
||||
self.assertIn("--web-root", result.stdout)
|
||||
def test_interrupt_shutdown(self):
|
||||
with running_server(stop_signal=signal.SIGINT) as port:
|
||||
self.assertEqual(request(port, "/api/health")[0], 200)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user