Product Setup
ci / frontend (push) Successful in 10m9s
ci / backend (push) Successful in 11m11s

This commit is contained in:
khodowany
2026-10-04 17:27:45 -05:00
commit c6b4ba1b43
18 changed files with 1971 additions and 0 deletions
+58
View File
@@ -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
View File
@@ -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
+32
View File
@@ -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})
+96
View File
@@ -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.
+45
View File
@@ -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 = []
+43
View File
@@ -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>
+828
View File
@@ -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
}
}
}
}
}
+17
View File
@@ -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"
}
}
+1
View File
@@ -0,0 +1 @@
import './style.css';
+58
View File
@@ -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; }
}
+18
View File
@@ -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"]
}
+20
View File
@@ -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',
},
},
});
+32
View File
@@ -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
View File
@@ -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:]))
+56
View File
@@ -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
View File
@@ -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;
}
+18
View File
@@ -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;
}
+140
View File
@@ -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()