diff --git a/BUILD.bazel b/BUILD.bazel
index acdfd98..b3b7c0f 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -6,6 +6,11 @@ load("@npm//:defs.bzl", "npm_link_all_packages")
# Link npm packages for the root workspace
npm_link_all_packages(name = "node_modules")
+exports_files(
+ ["pyproject.toml"],
+ visibility = ["//visibility:public"],
+)
+
# Empty target for the root workspace package override
py_library(
name = "root",
@@ -38,3 +43,8 @@ gazelle(
name = "gazelle",
gazelle = ":gazelle_binary",
)
+
+alias(
+ name = "format",
+ actual = "//tools/format",
+)
diff --git a/MODULE.bazel b/MODULE.bazel
index 05baa81..aa7e564 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -5,6 +5,7 @@ module(
bazel_dep(name = "aspect_rules_py", version = "1.11.2")
bazel_dep(name = "aspect_rules_js", version = "3.0.3")
+bazel_dep(name = "aspect_rules_lint", version = "2.5.0")
bazel_dep(name = "bazel_lib", version = "3.2.2")
bazel_dep(name = "rules_nodejs", version = "6.7.3")
bazel_dep(name = "platforms", version = "1.0.0")
diff --git a/TODO.md b/TODO.md
index 94f24de..a5010d6 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,7 +1,4 @@
-- [ ] Add React + Vite + PNPM FE monorepo infra
- - [ ] Add Bazel support
-- [ ] Fix various Bazel hard-coded paths
-- [ ] Add Ruff and ESLint support to Bazel
+- [ ] Remove ESLint hardcoded deps
- [ ] Add Sentry
- [ ] Add Vulture dead code analysis
- [ ] Add Autofix CI
\ No newline at end of file
diff --git a/apps/backend/main.py b/apps/backend/main.py
index d62bf46..bae7a8e 100644
--- a/apps/backend/main.py
+++ b/apps/backend/main.py
@@ -4,7 +4,10 @@ from shared import hello
app = FastAPI()
-_shared_is_compiled = shared.__file__ is not None and not shared.__file__.endswith(".py")
+_shared_is_compiled = shared.__file__ is not None and not shared.__file__.endswith(
+ ".py"
+)
+
@app.get("/")
def read_root():
@@ -21,4 +24,4 @@ def debug_shared():
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
- return {"item_id": item_id, "q": q}
\ No newline at end of file
+ return {"item_id": item_id, "q": q}
diff --git a/apps/backend/package.json b/apps/backend/package.json
index 01be1f9..b0bb225 100644
--- a/apps/backend/package.json
+++ b/apps/backend/package.json
@@ -2,4 +2,4 @@
"name": "backend",
"version": "0.0.0",
"private": true
-}
\ No newline at end of file
+}
diff --git a/apps/frontend/BUILD.bazel b/apps/frontend/BUILD.bazel
index f091f88..cdafc9a 100644
--- a/apps/frontend/BUILD.bazel
+++ b/apps/frontend/BUILD.bazel
@@ -1,3 +1,4 @@
+load("@aspect_rules_js//js:defs.bzl", "js_library")
load("@npm//:defs.bzl", "npm_link_all_packages")
load("@npm//apps/frontend:vite/package_json.bzl", vite_bin = "bin")
load("@npm//apps/frontend:vitest/package_json.bzl", vitest_bin = "bin")
@@ -41,3 +42,15 @@ vitest_bin.vitest_test(
chdir = package_name(),
data = SRCS + [":node_modules"],
)
+
+js_library(
+ name = "eslintrc",
+ srcs = ["eslint.config.js"],
+ visibility = ["//visibility:public"],
+ deps = [
+ ":node_modules/@eslint/js",
+ ":node_modules/eslint-plugin-react-hooks",
+ ":node_modules/eslint-plugin-react-refresh",
+ ":node_modules/typescript-eslint",
+ ],
+)
diff --git a/apps/frontend/eslint.config.js b/apps/frontend/eslint.config.js
index 5e6b472..75d3c46 100644
--- a/apps/frontend/eslint.config.js
+++ b/apps/frontend/eslint.config.js
@@ -1,14 +1,14 @@
-import js from '@eslint/js'
-import globals from 'globals'
-import reactHooks from 'eslint-plugin-react-hooks'
-import reactRefresh from 'eslint-plugin-react-refresh'
-import tseslint from 'typescript-eslint'
-import { defineConfig, globalIgnores } from 'eslint/config'
+import js from "@eslint/js";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
- globalIgnores(['dist']),
+ globalIgnores(["dist"]),
{
- files: ['**/*.{ts,tsx}'],
+ files: ["**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
@@ -20,4 +20,4 @@ export default defineConfig([
globals: globals.browser,
},
},
-])
+]);
diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx
index 3baf5a8..2d5306f 100644
--- a/apps/frontend/src/App.tsx
+++ b/apps/frontend/src/App.tsx
@@ -1,11 +1,11 @@
-import { HelloWorld } from 'components'
+import { HelloWorld } from "components";
function App() {
return (
<>
>
- )
+ );
}
-export default App
+export default App;
diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx
index 4aff025..5d4a2be 100644
--- a/apps/frontend/src/main.tsx
+++ b/apps/frontend/src/main.tsx
@@ -1,9 +1,9 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import App from './App.tsx'
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import App from "./App.tsx";
-createRoot(document.getElementById('root')!).render(
+createRoot(document.getElementById("root")!).render(
,
-)
+);
diff --git a/apps/frontend/src/placeholder.spec.ts b/apps/frontend/src/placeholder.spec.ts
index 42bb064..f449c08 100644
--- a/apps/frontend/src/placeholder.spec.ts
+++ b/apps/frontend/src/placeholder.spec.ts
@@ -1,7 +1,7 @@
-import {describe, expect, it} from "vitest";
+import { describe, expect, it } from "vitest";
describe("placeholder", () => {
it("should be a placeholder test", () => {
expect(true).toBe(true);
});
-});
\ No newline at end of file
+});
diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts
index 5885bce..8c0e5ac 100644
--- a/apps/frontend/vite.config.ts
+++ b/apps/frontend/vite.config.ts
@@ -1,8 +1,8 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
root: import.meta.dirname,
-})
+});
diff --git a/package.json b/package.json
index b397877..a2e1efd 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,13 @@
{
- "name": "root",
- "private": true,
- "scripts": {
- "sherif": "sherif"
- },
- "devDependencies": {
- "sherif": "^1.5.0"
- },
- "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
+ "name": "root",
+ "private": true,
+ "scripts": {
+ "sherif": "sherif"
+ },
+ "devDependencies": {
+ "eslint": "^9.39.4",
+ "prettier": "^3.8.2",
+ "sherif": "^1.5.0"
+ },
+ "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
}
diff --git a/packages/components/BUILD.bazel b/packages/components/BUILD.bazel
index 2db39ac..b5ebb99 100644
--- a/packages/components/BUILD.bazel
+++ b/packages/components/BUILD.bazel
@@ -25,3 +25,15 @@ npm_package(
srcs = [":components"],
visibility = ["//visibility:public"],
)
+
+js_library(
+ name = "eslintrc",
+ srcs = ["eslint.config.js"],
+ visibility = ["//visibility:public"],
+ deps = [
+ ":node_modules/@eslint/js",
+ ":node_modules/eslint-plugin-react-hooks",
+ ":node_modules/eslint-plugin-react-refresh",
+ ":node_modules/typescript-eslint",
+ ],
+)
diff --git a/packages/components/eslint.config.js b/packages/components/eslint.config.js
new file mode 100644
index 0000000..223b5ae
--- /dev/null
+++ b/packages/components/eslint.config.js
@@ -0,0 +1,21 @@
+import js from "@eslint/js";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+import { defineConfig, globalIgnores } from "eslint/config";
+
+export default defineConfig([
+ globalIgnores(["dist"]),
+ {
+ files: ["**/*.{ts,tsx}"],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ },
+ },
+]);
diff --git a/packages/components/package.json b/packages/components/package.json
index 569f4c3..4d89e2d 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -14,8 +14,13 @@
"react-dom": "^19.0.0"
},
"devDependencies": {
+ "@eslint/js": "^9.39.4",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
- "typescript": "~6.0.2"
+ "eslint": "^9.39.4",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.58.1"
}
}
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 1a99424..2397f19 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -2,4 +2,4 @@
"name": "shared",
"version": "0.0.0",
"private": true
-}
\ No newline at end of file
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3bc05b1..20862ba 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,12 @@ importers:
.:
devDependencies:
+ eslint:
+ specifier: ^9.39.4
+ version: 9.39.4(jiti@2.6.1)
+ prettier:
+ specifier: ^3.8.2
+ version: 3.8.2
sherif:
specifier: ^1.5.0
version: 1.11.1
@@ -75,15 +81,30 @@ importers:
specifier: ^19.0.0
version: 19.2.5(react@19.2.5)
devDependencies:
+ '@eslint/js':
+ specifier: ^9.39.4
+ version: 9.39.4
'@types/react':
specifier: ^19.2.14
version: 19.2.14
'@types/react-dom':
specifier: ^19.2.3
version: 19.2.3(@types/react@19.2.14)
+ eslint:
+ specifier: ^9.39.4
+ version: 9.39.4(jiti@2.6.1)
+ eslint-plugin-react-hooks:
+ specifier: ^7.0.1
+ version: 7.0.1(eslint@9.39.4(jiti@2.6.1))
+ eslint-plugin-react-refresh:
+ specifier: ^0.5.2
+ version: 0.5.2(eslint@9.39.4(jiti@2.6.1))
typescript:
specifier: ~6.0.2
version: 6.0.2
+ typescript-eslint:
+ specifier: ^8.58.1
+ version: 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)
packages/shared: {}
@@ -933,6 +954,11 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
+ prettier@3.8.2:
+ resolution: {integrity: sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==}
+ engines: {node: '>=14'}
+ hasBin: true
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -2028,6 +2054,8 @@ snapshots:
prelude-ls@1.2.1: {}
+ prettier@3.8.2: {}
+
punycode@2.3.1: {}
react-dom@19.2.5(react@19.2.5):
diff --git a/pyproject.toml b/pyproject.toml
index a6de0fb..c4d129c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,6 +33,8 @@ _gazelle = "bazel run //:gazelle"
_update-module-overrides = "uv run python tools/update_module_overrides.py"
build = "bazel build //..."
test = "bazel test //..."
+format = "bazel run //tools/format"
+lint = "bazel build //tools/lint:all"
[tool.poe.tasks.sync]
help = "Sync Bazel BUILD files after adding and app, deps or packages"
diff --git a/tools/deploy_mypyc.py b/tools/deploy_mypyc.py
index 0b6e7da..4de138d 100644
--- a/tools/deploy_mypyc.py
+++ b/tools/deploy_mypyc.py
@@ -10,6 +10,7 @@ import argparse
import glob
import os
import shutil
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BAZEL_BIN = os.path.join(ROOT, ".bazel", "bin")
PACKAGES_DIR = os.path.join(ROOT, "packages")
@@ -35,9 +36,7 @@ def deploy():
# Determine the package name from the directory structure
rel = os.path.relpath(root, mypyc_dir) # e.g. "shared"
# Find matching source package
- src_pkg = glob.glob(
- os.path.join(PACKAGES_DIR, "*", "src", rel)
- )
+ src_pkg = glob.glob(os.path.join(PACKAGES_DIR, "*", "src", rel))
if src_pkg:
dest = os.path.join(src_pkg[0], f)
shutil.copy2(os.path.join(root, f), dest)
@@ -46,7 +45,9 @@ def deploy():
def clean():
"""Remove all deployed .so files from source packages."""
- for pkg_src in glob.glob(os.path.join(PACKAGES_DIR, "*", "src", "**"), recursive=True):
+ for pkg_src in glob.glob(
+ os.path.join(PACKAGES_DIR, "*", "src", "**"), recursive=True
+ ):
if _is_extension(os.path.basename(pkg_src)):
os.remove(pkg_src)
print(f" removed: {os.path.relpath(pkg_src, ROOT)}")
@@ -54,7 +55,9 @@ def clean():
def main():
parser = argparse.ArgumentParser()
- parser.add_argument("--clean", action="store_true", help="Remove deployed .so files")
+ parser.add_argument(
+ "--clean", action="store_true", help="Remove deployed .so files"
+ )
args = parser.parse_args()
if args.clean:
diff --git a/tools/format/BUILD.bazel b/tools/format/BUILD.bazel
new file mode 100644
index 0000000..8315b15
--- /dev/null
+++ b/tools/format/BUILD.bazel
@@ -0,0 +1,27 @@
+"""Formatter binary definitions.
+
+This is in its own package so that loading-time symbols
+don't trigger eager fetches for builds that don't run format.
+"""
+
+load("@aspect_rules_lint//format:defs.bzl", "format_multirun")
+load("@npm//:prettier/package_json.bzl", prettier = "bin")
+
+package(default_visibility = ["//:__subpackages__"])
+
+prettier.prettier_binary(
+ name = "prettier",
+ env = {"BAZEL_BINDIR": "."},
+ fixed_args = [
+ "--loglevel=warn",
+ ],
+)
+
+format_multirun(
+ name = "format",
+ css = ":prettier",
+ html = ":prettier",
+ javascript = ":prettier",
+ python = "@aspect_rules_lint//format:ruff",
+ visibility = ["//:__subpackages__"],
+)
diff --git a/tools/lint/BUILD.bazel b/tools/lint/BUILD.bazel
new file mode 100644
index 0000000..f93fe7a
--- /dev/null
+++ b/tools/lint/BUILD.bazel
@@ -0,0 +1,21 @@
+"""Linter binary definitions.
+
+This is in its own package so that loading-time symbols
+don't trigger eager fetches for builds that don't run lint.
+"""
+
+load("@npm//:eslint/package_json.bzl", eslint_bin = "bin")
+
+package(default_visibility = ["//:__subpackages__"])
+
+eslint_bin.eslint_binary(
+ name = "eslint_frontend",
+ data = ["//apps/frontend:eslintrc"],
+ fixed_args = ["--config=apps/frontend/eslint.config.js"],
+)
+
+eslint_bin.eslint_binary(
+ name = "eslint_components",
+ data = ["//packages/components:eslintrc"],
+ fixed_args = ["--config=packages/components/eslint.config.js"],
+)
diff --git a/tools/lint/linters.bzl b/tools/lint/linters.bzl
new file mode 100644
index 0000000..be02afa
--- /dev/null
+++ b/tools/lint/linters.bzl
@@ -0,0 +1,32 @@
+"Define linter aspects for the repository"
+
+load("@aspect_rules_lint//lint:eslint.bzl", "lint_eslint_aspect")
+load("@aspect_rules_lint//lint:lint_test.bzl", "lint_test")
+load("@aspect_rules_lint//lint:ruff.bzl", "lint_ruff_aspect")
+
+ruff = lint_ruff_aspect(
+ binary = Label("@aspect_rules_lint//lint:ruff_bin"),
+ configs = [
+ Label("@//:pyproject.toml"),
+ ],
+)
+
+ruff_test = lint_test(aspect = ruff)
+
+eslint_frontend = lint_eslint_aspect(
+ binary = Label("//tools/lint:eslint_frontend"),
+ configs = [
+ Label("//apps/frontend:eslintrc"),
+ ],
+)
+
+eslint_frontend_test = lint_test(aspect = eslint_frontend)
+
+eslint_components = lint_eslint_aspect(
+ binary = Label("//tools/lint:eslint_components"),
+ configs = [
+ Label("//packages/components:eslintrc"),
+ ],
+)
+
+eslint_components_test = lint_test(aspect = eslint_components)
diff --git a/tools/mypyc_compile.py b/tools/mypyc_compile.py
index 112fd21..87148c9 100644
--- a/tools/mypyc_compile.py
+++ b/tools/mypyc_compile.py
@@ -41,9 +41,7 @@ def main() -> None:
# Use mypyc's Python API directly (avoids subprocess + PATH issues)
from mypyc.build import mypycify
- src_paths = [
- os.path.join(pkg_dir, os.path.basename(s)) for s in args.srcs
- ]
+ src_paths = [os.path.join(pkg_dir, os.path.basename(s)) for s in args.srcs]
ext_modules = mypycify(src_paths)
if not ext_modules:
@@ -74,8 +72,10 @@ def main() -> None:
dest_name = f
pkg_prefix = args.package_name + "."
if f.startswith(pkg_prefix):
- dest_name = "__init__." + f[len(pkg_prefix):]
- shutil.copy2(os.path.join(root, f), os.path.join(out_pkg, dest_name))
+ dest_name = "__init__." + f[len(pkg_prefix) :]
+ shutil.copy2(
+ os.path.join(root, f), os.path.join(out_pkg, dest_name)
+ )
print(f" compiled: {args.package_name}/{dest_name}")
diff --git a/tools/update_gazelle_resolves.py b/tools/update_gazelle_resolves.py
index bfe7770..46be426 100644
--- a/tools/update_gazelle_resolves.py
+++ b/tools/update_gazelle_resolves.py
@@ -64,7 +64,11 @@ def update_build_file(resolve_lines: list[str]) -> None:
"""Replace the resolve block in BUILD.bazel with the new lines."""
content = BUILD_FILE.read_text()
- block = f"{RESOLVE_MARKER_START}\n" + "\n".join(resolve_lines) + f"\n{RESOLVE_MARKER_END}"
+ block = (
+ f"{RESOLVE_MARKER_START}\n"
+ + "\n".join(resolve_lines)
+ + f"\n{RESOLVE_MARKER_END}"
+ )
pattern = re.compile(
rf"^{re.escape(RESOLVE_MARKER_START)}$.*?^{re.escape(RESOLVE_MARKER_END)}$",
@@ -102,7 +106,9 @@ def main() -> None:
resolve_lines = generate_resolve_lines(packages)
update_build_file(resolve_lines)
- print(f"Updated {BUILD_FILE.name} with resolve directives for: {', '.join(packages)}")
+ print(
+ f"Updated {BUILD_FILE.name} with resolve directives for: {', '.join(packages)}"
+ )
for line in resolve_lines:
print(f" {line}")
diff --git a/tools/update_module_overrides.py b/tools/update_module_overrides.py
index cea3c59..6372c02 100644
--- a/tools/update_module_overrides.py
+++ b/tools/update_module_overrides.py
@@ -20,7 +20,7 @@ def read_pyproject_name(pyproject: Path) -> str | None:
try:
data = tomllib.loads(pyproject.read_text())
return data.get("project", {}).get("name")
- except (OSError, tomllib.TOMLDecodeError):
+ except OSError, tomllib.TOMLDecodeError:
return None
@@ -85,7 +85,9 @@ def generate_override_lines(overrides: list[tuple[str, str]]) -> list[str]:
"""Generate the Starlark lines for uv.override_package calls."""
lines: list[str] = []
for name, target in overrides:
- lines.append(f'uv.override_package(name = "{name}", lock = "//:uv.lock", target = "{target}")')
+ lines.append(
+ f'uv.override_package(name = "{name}", lock = "//:uv.lock", target = "{target}")'
+ )
return lines