From 121a283e8a356a1922645e5915d497310daf0f53 Mon Sep 17 00:00:00 2001 From: Nicu Chiciuc Date: Tue, 4 Aug 2026 01:40:38 +0300 Subject: [PATCH 1/5] Harden Cloudflare deployment checks --- scripts/deploy-cloudflare.test.ts | 53 ++++++++++++++++++ scripts/deploy-cloudflare.ts | 63 +++++++++++++++++----- scripts/verify-current-branch-head.test.ts | 35 +++++++++--- scripts/verify-current-branch-head.ts | 18 ++++--- 4 files changed, 143 insertions(+), 26 deletions(-) create mode 100644 scripts/deploy-cloudflare.test.ts diff --git a/scripts/deploy-cloudflare.test.ts b/scripts/deploy-cloudflare.test.ts new file mode 100644 index 0000000..4d77b71 --- /dev/null +++ b/scripts/deploy-cloudflare.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { selectCloudflareDeployPlan } from "./deploy-cloudflare.ts"; + +describe("deploy-cloudflare", () => { + it("uses the application-only build for a local dry-run", () => { + expect( + selectCloudflareDeployPlan(["deploy", "--dry-run"], { + CLOUDFLARE_WORKER_NAME: "example-app", + }), + ).toEqual({ + buildArgs: ["run", "build:app"], + wranglerArgs: ["deploy", "--name", "example-app", "--dry-run"], + }); + }); + + it("uses the complete Cloudflare build before a local deploy", () => { + expect( + selectCloudflareDeployPlan(["preview"], { + CLOUDFLARE_WORKER_NAME: "example-app", + }), + ).toEqual({ + buildArgs: ["run", "build:cloudflare"], + wranglerArgs: ["versions", "upload", "--name", "example-app"], + }); + }); + + it("does not repeat the build during Workers Builds", () => { + expect( + selectCloudflareDeployPlan(["deploy"], { + WORKERS_CI: "true", + WRANGLER_CI_OVERRIDE_NAME: "connected-worker", + }), + ).toEqual({ + buildArgs: null, + wranglerArgs: ["deploy", "--name", "connected-worker"], + }); + }); + + it("rejects Wrangler flags that can replace the connected Worker name", () => { + const env = { CLOUDFLARE_WORKER_NAME: "example-app" }; + + expect(() => selectCloudflareDeployPlan(["deploy", "--name", "other"], env)).toThrow( + "Do not pass Wrangler --name/-n manually", + ); + expect(() => selectCloudflareDeployPlan(["deploy", "--name=other"], env)).toThrow( + "Do not pass Wrangler --name/-n manually", + ); + expect(() => selectCloudflareDeployPlan(["deploy", "-n", "other"], env)).toThrow( + "Do not pass Wrangler --name/-n manually", + ); + }); +}); diff --git a/scripts/deploy-cloudflare.ts b/scripts/deploy-cloudflare.ts index 42fbb19..10483ea 100644 --- a/scripts/deploy-cloudflare.ts +++ b/scripts/deploy-cloudflare.ts @@ -1,6 +1,7 @@ /// import { spawn } from "node:child_process"; import process from "node:process"; +import { pathToFileURL } from "node:url"; const modes = { deploy: ["deploy"], @@ -13,8 +14,12 @@ function isMode(value: string | undefined): value is Mode { return value === "deploy" || value === "preview"; } -function readWorkerName() { - const workerName = process.env.WRANGLER_CI_OVERRIDE_NAME ?? process.env.CLOUDFLARE_WORKER_NAME; +function isReservedWranglerFlag(value: string) { + return value === "--name" || value.startsWith("--name=") || value === "-n"; +} + +function readWorkerName(env: NodeJS.ProcessEnv) { + const workerName = env.WRANGLER_CI_OVERRIDE_NAME ?? env.CLOUDFLARE_WORKER_NAME; if (!workerName) { throw new Error( @@ -33,9 +38,9 @@ function readWorkerName() { return workerName; } -function run(command: string, args: string[]) { +function run(command: string, args: readonly string[]) { return new Promise((resolve, reject) => { - const child = spawn(command, args, { + const child = spawn(command, [...args], { shell: process.platform === "win32", stdio: "inherit", }); @@ -52,17 +57,51 @@ function run(command: string, args: string[]) { }); } -const [modeArg, ...extraArgs] = process.argv.slice(2); +type CloudflareDeployPlan = { + buildArgs: readonly string[] | null; + wranglerArgs: readonly string[]; +}; -if (!isMode(modeArg)) { - throw new Error("Usage: node ./scripts/deploy-cloudflare.ts [wrangler flags]"); +export function selectCloudflareDeployPlan( + args: readonly string[], + env: NodeJS.ProcessEnv, +): CloudflareDeployPlan { + const [modeArg, ...extraArgs] = args; + + if (!isMode(modeArg)) { + throw new Error("Usage: node ./scripts/deploy-cloudflare.ts [wrangler flags]"); + } + + if (extraArgs.some(isReservedWranglerFlag)) { + throw new Error( + "Do not pass Wrangler --name/-n manually. Set CLOUDFLARE_WORKER_NAME or let Workers Builds provide WRANGLER_CI_OVERRIDE_NAME.", + ); + } + + const workerName = readWorkerName(env); + const isWorkersBuild = env.WORKERS_CI === "1" || env.WORKERS_CI === "true"; + const isDryRun = extraArgs.includes("--dry-run"); + + return { + buildArgs: isWorkersBuild ? null : ["run", isDryRun ? "build:app" : "build:cloudflare"], + wranglerArgs: [...modes[modeArg], "--name", workerName, ...extraArgs], + }; } -const workerName = readWorkerName(); -const isWorkersBuild = process.env.WORKERS_CI === "1" || process.env.WORKERS_CI === "true"; +export async function main( + args: readonly string[] = process.argv.slice(2), + env: NodeJS.ProcessEnv = process.env, +) { + const plan = selectCloudflareDeployPlan(args, env); -if (!isWorkersBuild) { - await run("vp", ["run", "build:cloudflare"]); + if (plan.buildArgs) { + await run("vp", plan.buildArgs); + } + + await run("wrangler", plan.wranglerArgs); } -await run("wrangler", [...modes[modeArg], "--name", workerName, ...extraArgs]); +const entrypoint = process.argv[1]; +if (entrypoint && import.meta.url === pathToFileURL(entrypoint).href) { + await main(); +} diff --git a/scripts/verify-current-branch-head.test.ts b/scripts/verify-current-branch-head.test.ts index 675ee6d..f09ef0e 100644 --- a/scripts/verify-current-branch-head.test.ts +++ b/scripts/verify-current-branch-head.test.ts @@ -3,17 +3,18 @@ import { describe, expect, it } from "vite-plus/test"; import { verifyCurrentBranchHead } from "./verify-current-branch-head.ts"; describe("verify-current-branch-head", () => { - it("accepts the build while its commit is the branch head", () => { + it("accepts a manual build while its checkout is the branch head", () => { verifyCurrentBranchHead( { WORKERS_CI: "1", WORKERS_CI_BRANCH: "feature", - WORKERS_CI_COMMIT_SHA: "new", + WORKERS_CI_COMMIT_SHA: "feature", }, (branch) => { expect(branch).toBe("feature"); return "new"; }, + () => "new", ); }); @@ -23,20 +24,40 @@ describe("verify-current-branch-head", () => { { WORKERS_CI: "1", WORKERS_CI_BRANCH: "feature", - WORKERS_CI_COMMIT_SHA: "old", }, () => "new", + () => "old", ), ).toThrow("Convex was not deployed"); }); - it("fails closed without Workers Builds commit identity", () => { + it("fails closed without Workers Builds branch identity", () => { expect(() => - verifyCurrentBranchHead({ WORKERS_CI: "1", WORKERS_CI_BRANCH: "feature" }, () => "new"), - ).toThrow("WORKERS_CI_COMMIT_SHA"); + verifyCurrentBranchHead( + { WORKERS_CI: "1" }, + () => "new", + () => "new", + ), + ).toThrow("WORKERS_CI_BRANCH"); + }); + + it("fails closed without a checked-out commit", () => { + expect(() => + verifyCurrentBranchHead( + { WORKERS_CI: "1", WORKERS_CI_BRANCH: "feature" }, + () => "new", + () => "", + ), + ).toThrow("checked-out commit"); }); it("skips the provider check during local deploy validation", () => { - expect(() => verifyCurrentBranchHead({}, () => "unused")).not.toThrow(); + expect(() => + verifyCurrentBranchHead( + {}, + () => "unused", + () => "unused", + ), + ).not.toThrow(); }); }); diff --git a/scripts/verify-current-branch-head.ts b/scripts/verify-current-branch-head.ts index 73d3ae9..c064cea 100644 --- a/scripts/verify-current-branch-head.ts +++ b/scripts/verify-current-branch-head.ts @@ -19,25 +19,29 @@ export function verifyCurrentBranchHead( return remoteHead; }, + readCheckoutHead = () => execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(), ) { if (env.WORKERS_CI !== "1" && env.WORKERS_CI !== "true") { return; } const branch = env.WORKERS_CI_BRANCH; - const commitSha = env.WORKERS_CI_COMMIT_SHA; - if (!branch || !commitSha) { - throw new Error( - "Workers Builds must provide WORKERS_CI_BRANCH and WORKERS_CI_COMMIT_SHA before Convex deploys.", - ); + if (!branch) { + throw new Error("Workers Builds must provide WORKERS_CI_BRANCH before Convex deploys."); + } + + // A manual Workers Build can identify the commit as a branch name, so use the checkout itself. + const checkoutHead = readCheckoutHead(); + if (!checkoutHead) { + throw new Error("Git did not return the checked-out commit for this Workers Build."); } const remoteHead = readRemoteHead(branch); - if (remoteHead !== commitSha) { + if (remoteHead !== checkoutHead) { throw new Error( - `Workers Build ${commitSha} is stale: ${branch} now points to ${remoteHead}. Convex was not deployed.`, + `Workers Build ${checkoutHead} is stale: ${branch} now points to ${remoteHead}. Convex was not deployed.`, ); } } From 45a915298a30216650d647b51f89795085af0d0e Mon Sep 17 00:00:00 2001 From: Nicu Chiciuc Date: Tue, 4 Aug 2026 01:41:12 +0300 Subject: [PATCH 2/5] Keep the starter portable and focused --- .gitignore | 4 + README.md | 18 +- REPO_HISTORY.md | 379 ---------------------------------------- SECURITY.md | 5 +- docs/package-imports.md | 60 ------- package.json | 1 + tsconfig.json | 1 + 7 files changed, 13 insertions(+), 455 deletions(-) delete mode 100644 REPO_HISTORY.md delete mode 100644 docs/package-imports.md diff --git a/.gitignore b/.gitignore index c7e9a22..e4fee43 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,10 @@ dist dist-ssr .wrangler *.local +.dev.vars* +.env* +.convex +convex/.tmp # Editor directories and files .vscode/* diff --git a/README.md b/README.md index 7b89aaf..f8ae88e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ This repository is the starter app that Samebase copies into a new GitHub repository. +It is a small, complete app base. It includes working authentication, real-time data, sharing, and +deployment paths without adding product-specific services that a new app might not need. + For the complete provider setup, use the [Samebase do-it-yourself guide](https://samebase.com/docs/do-it-yourself). This README covers work inside the repository. @@ -11,11 +14,12 @@ inside the repository. - React 19 and TanStack Start in SPA mode - Convex for the real-time backend, database, and guest authentication - Cloudflare Workers Static Assets for delivery +- shadcn/ui primitives for the user interface - Vite+ for development, formatting, linting, tests, and builds - Node.js 24 for application and automation code The example app is a public todo list. Guests can sign in without an external identity provider, -create todos, and see who created each item. +create todos, see real-time updates, and scan a QR code to open the same list on another device. ## Local development @@ -102,18 +106,6 @@ Do not hand-edit generated files when their source tool can update them. When a Convex AI-file update changes the installed source snapshot, confirm its distribution license and update `THIRD_PARTY_NOTICES.md` when its third-party material changes. -## Repository history - -The early numbered commits show how the starter was assembled. `REPO_HISTORY.md` keeps the same -construction notes in the checked-out tree because GitHub template copies do not preserve commit -history. - -New maintenance uses normal pull requests and append-only commits. The public `main` history is not -rewritten to keep later updates inside the original numbered sequence. - -Use GitHub's template button for a clean new repository. Fork this repository only when you also -want its history. - ## License Licensed under the [Apache License 2.0](./LICENSE). See diff --git a/REPO_HISTORY.md b/REPO_HISTORY.md deleted file mode 100644 index 9c58d15..0000000 --- a/REPO_HISTORY.md +++ /dev/null @@ -1,379 +0,0 @@ -# Initial construction history - -The numbered sections describe the curated construction of the starter app. New maintenance uses -normal pull requests and append-only commits. This file is not a changelog, and later fixes do not -rewrite the public Git history. - -## 1. initialize vite plus application - -```sh -vp create vite:application \ - --directory app-start-workers \ - --agent codex \ - --editor vscode \ - --hooks \ - --no-interactive \ - --verbose -``` - -## 2. add React and TanStack Start manually - -```sh -vp add react react-dom @tanstack/react-router @tanstack/react-start -vp add -D @tanstack/router-plugin @types/node @types/react @types/react-dom @vitejs/plugin-react -vp build -vp run build -``` - -Replace the generated Vite demo with a small TanStack Start route shell: - -- remove `index.html` -- remove the generated Vite demo files under `src/` -- create `src/router.tsx` -- create `src/routes/__root.tsx` -- create `src/routes/index.tsx` -- create `src/routes/about.tsx` - -TanStack Start is configured in SPA mode, with `/index.html` as the prerendered -shell for static hosting. - -## 3. add Tailwind CSS - -```sh -vp add tailwindcss @tailwindcss/vite -vp run build -``` - -Add the Tailwind Vite plugin and import Tailwind from `src/style.css`. The app -still uses plain CSS classes at this step; Tailwind is present before shadcn/ui -so the styling layers stay easy to inspect. - -## 4. add package import aliases - -```sh -vp run build -``` - -Add Node package imports for app-internal aliases instead of a TypeScript -`@/*` path alias. The mapping lives in `package.json` `imports`: - -- `#components/*`, `#lib/*`, and `#hooks/*` map into `src/` -- TypeScript, Vite, and shadcn resolve the same specifiers, so there is no - duplicate `compilerOptions.paths` or Vite `resolve.alias` entry -- named roots are used because Node 22 rejects `#/...` specifiers - -`docs/package-imports.md` records the research behind this choice. - -## 5. initialize shadcn/ui - -```sh -vp add class-variance-authority clsx lucide-react radix-ui shadcn tailwind-merge tw-animate-css -pnpm approve-builds msw -vp run build -``` - -Add the shadcn Nova preset foundation: - -- `components.json` -- `src/lib/utils.ts` -- shadcn theme imports and CSS variables in `src/style.css` -- a system `--font-sans` stack, so the starter has no webfont swap on first load -- a pointer cursor on enabled buttons, which Tailwind v4 no longer applies by - default - -This commit prepares the theme and `cn()` helper, but does not add concrete UI -primitives yet. - -## 6. enforce Oxc formatting in VS Code - -```sh -vp run build -``` - -Force common web file types to use the Oxc VS Code formatter. The setting is -tracked even though the generated Vite `.gitignore` ignores most `.vscode` -files, because this template wants format-on-save to match the project. - -## 7. add the shadcn primitives we need - -```sh -vp exec shadcn add button checkbox input --yes -vp run build -``` - -Generate the same UI primitives used by the starter todo UI: - -- `src/components/ui/button.tsx` -- `src/components/ui/checkbox.tsx` -- `src/components/ui/input.tsx` - -No product UI changes yet; this keeps generated primitive code separate from -the app example. - -## 8. add Convex - -```sh -vp add convex -pnpm approve-builds esbuild -CONVEX_AGENT_MODE=anonymous vp exec convex dev --once --typecheck=disable -vp run build -``` - -Add Convex as the backend layer while keeping the first backend state empty: - -- `convex.json` disables Convex AI files for this step -- `convex/schema.ts` starts with an empty schema -- `src/lib/convex.tsx` wires the React provider at the route root -- generated Convex bindings under `convex/_generated/` are committed - -The app can run with a real `VITE_CONVEX_URL`, and shows a small setup message -when that environment variable is missing. - -## 9. add Convex AI files - -```sh -vp exec convex ai-files install -vp run build -``` - -Enable Convex AI files for Codex and install the generated guidance files. This -adds the agent instructions, Convex AI guidelines, and the generated skills lock -so future agents know how to work inside the Convex backend. - -## 10. add the todo example - -```sh -vp exec convex codegen --typecheck=disable -vp run build -``` - -Add the first real app behavior: - -- `convex/schema.ts` defines the `todos` table -- `convex/todos.ts` exposes list, create, and toggle functions -- `src/routes/index.tsx` renders the todo UI with Convex hooks -- the root and about routes switch to the shadcn-styled app shell - -The Convex bindings are regenerated after the schema and functions are added. - -## 11. add the QR share block - -```sh -vp add qrcode.react -vp run build -``` - -Render a QR code for the current browser URL above the todo list. The URL is -read after mount so TanStack Start's prerendered HTML stays stable. - -## 12. add the local dev workflow - -```sh -vp run build -``` - -Add `scripts/run-worktree-dev.ts` so anonymous Convex mode works on macOS, Linux, -and Windows without relying on shell-specific environment variable syntax. The -user-facing `dev:worktree` script now delegates to that Node wrapper. - -## 13. deploy static assets with Workers - -```sh -vp add -D wrangler -vp exec wrangler --version -vp run deploy:dry-run -``` - -Create `wrangler.jsonc` so the repository owns the Cloudflare Workers deploy -contract: - -- the build command runs `pnpm run build:cloudflare` -- static assets are served from `./dist/client` -- missing paths fall back to the SPA shell - -`scripts/build-cloudflare.ts` deploys Convex first when `CONVEX_DEPLOY_KEY` is -set, creates Convex preview deployments when `WORKERS_CI_BRANCH` is set, and -falls back to a static-only build for local dry-runs without a deploy key. - -## 14. teach Workers self-deployment - -```sh -vp run build -vp run deploy:dry-run -``` - -Create user-facing setup docs: - -- `README.md` links to the complete Samebase do-it-yourself guide and documents - the repository's local, check, build, and deploy contracts -- `docs/local-setup.md` explains local Vite+, Convex, and worktree setup - -The dashboard setup keeps Cloudflare's default `pnpm run build`: `build` -delegates to the Cloudflare-aware build script while `build:app` keeps the -plain TanStack/static build visible. The README points users at -`wrangler.jsonc` as the source of truth for the Workers build command, asset -directory, and SPA fallback. - -## 15. remove the fixed Worker name - -```sh -vp run check -CLOUDFLARE_WORKER_NAME=app-start-workers vp run deploy:dry-run -CLOUDFLARE_WORKER_NAME=app-start-workers vp run deploy:preview:dry-run -``` - -Remove `name` from `wrangler.jsonc` so the template does not force every user -or automated Samebase app provisioning to rename either Cloudflare or the -repository. - -Add `scripts/deploy-cloudflare.ts` so Cloudflare Workers Builds can pass the -actual connected Worker name through `WRANGLER_CI_OVERRIDE_NAME`: - -- `pnpm run deploy` wraps `wrangler deploy --name ` -- `pnpm run deploy:preview` wraps `wrangler versions upload --name ` -- local dry-runs can set `CLOUDFLARE_WORKER_NAME` - -The wrapper runs the Cloudflare build path locally before Wrangler, but skips -that build during Workers Builds because the dashboard already ran -`pnpm run build`. `wrangler.jsonc` stays focused on assets, SPA fallback, and -preview URLs. - -`.node-version` pins Workers Builds to Node 24. Node 24 runs these TypeScript -helper scripts directly because they only use erasable TypeScript syntax, so no -runtime TypeScript loader enters the deploy path. - -## 16. split the Convex deploy keys - -```sh -vp run check -WORKERS_CI=1 CONVEX_DEPLOY_KEY=legacy node ./scripts/build-cloudflare.ts -WORKERS_CI=1 WORKERS_CI_BRANCH=feature node ./scripts/build-cloudflare.ts -``` - -Production and preview builds must not share a Convex deployment: - -- `CONVEX_DEPLOY_KEY` is selected only when `WORKERS_CI_BRANCH` is `main`. It - is a Convex production deploy key with exactly `deployment:deploy`, - `deployment:env:view`, `deployment:env:write`, and `deployment:data:view`. - Do not grant data write, function-run, logs, backups, or integration - permissions. -- `PREVIEW_CONVEX_DEPLOY_KEY` is required for every non-production branch and - is Convex's project-level Preview deploy key. -- a run with either key but no `WORKERS_CI_BRANCH` fails closed instead of - guessing which Convex deployment to touch. - -The Workers dashboard does not expose a Pages-style per-environment selector -for build variables, so `scripts/build-cloudflare.ts` selects the key from -`WORKERS_CI_BRANCH`. `docs/cloudflare-workers-builds.md` records the contract. - -## 17. add guest auth - -```sh -vp add @convex-dev/auth @auth/core@0.37.0 -CONVEX_AGENT_MODE=anonymous vp exec convex dev --once --typecheck=disable -vp run check -vp run build -``` - -Add Convex Auth with the anonymous provider so the starter app has a real -authenticated identity without any external auth service: - -- `convex/auth.ts`, `convex/auth.config.ts`, and `convex/http.ts` configure - Convex Auth -- `convex/schema.ts` adds the auth tables and stores `todos.userId` -- `convex/todos.ts` derives the user from Convex Auth instead of trusting the - client when creating or toggling todos -- `convex/guests.ts` assigns each guest a readable display name from a fixed - pool -- the home route prompts unauthenticated users to continue as a guest and lets - signed-in users sign out - -The dev and Cloudflare build scripts configure Convex Auth JWT keys only when a -deployment does not already have them, so new deployments work without rotating -existing sessions on every build. - -## 18. share the todo list publicly - -```sh -vp run check -vp run build -``` - -The todo list becomes public while writes stay authenticated: - -- `convex/todos.ts` returns every user's todos with the author's display name, - and the home route renders the creator under each todo -- todo text is capped at 280 characters and each user keeps at most 50 todos, - so a public list cannot be flooded -- `convex/guests.ts` falls back to a generated guest name when the fixed pool - runs out - -## 19. prerender the home and about routes - -```sh -vp run check -vp run build -vp run deploy:dry-run -``` - -Prerender the public routes during the static build: - -- `scripts/cloudflare-prerender-pages.ts` is the source of truth for the - prerendered pages -- `vite.config.ts` prerenders them through TanStack Start and keeps the SPA - shell on `/index.html` for Cloudflare's SPA fallback -- `scripts/generate-cloudflare-redirects.ts` rewrites only the tagged generated - block in `public/_redirects`, and `verify:cloudflare-redirects` fails - `vp run check` when the committed file drifts -- Vite+ tests cover the redirects generator and the Convex deploy key selection - -## 20. guard Convex deploy ordering - -```sh -vp run check -vp test -``` - -Keep each non-production branch on one named Convex preview while preventing an -older concurrent Workers Build from replacing newer backend code: - -- `scripts/build-cloudflare.ts` continues to use `WORKERS_CI_BRANCH` as the - stable preview name -- the application build runs before `scripts/verify-current-branch-head.ts` -- the verifier compares `WORKERS_CI_COMMIT_SHA` with the current remote branch - head at the last controllable point before Convex pushes functions -- a stale build fails explicitly without deploying Convex -- the same check protects `main` from an older concurrent production build - -An internal multi-Worker fixture first proved sequential reuse, unwatched path -absence, failure and retry, an actual -last-completion-wins race, and the guarded version of that race. It then proved -that two Workers can concurrently cold-create and reuse one branch-named Convex -preview when both deploy byte-identical backend source. During a forced -overlap, both old builds were rejected. During a one-Worker failure, the other -Worker and Convex advanced while the failed Worker alias stayed on its last -successful commit. The next commit recovered all three. This tested baseline -required no designated deployer, but it duplicates Convex deploy work once per -Worker. The guard adds one Git request per provider build. A small non-atomic -interval remains between the check and the Convex push. - -## 21. use one Vite+ toolchain contract - -```sh -vp run check -vp run build -``` - -Keep the operator commands and the tools that implement them aligned: - -- `vp run format` and `vp run format:check` use the Vite+ formatter -- `vp run lint` uses the Vite+ linter -- `vp run test` uses the Vite+ test runner, and tests import from - `vite-plus/test` -- `vp run typecheck` covers the browser, Node, and Convex TypeScript projects -- Oxlint stays type-aware, while those explicit project checks own full TypeScript validation -- `vp run check` composes all validation without calling itself -- `vp run build` reaches `build:app`, which runs the complete check before the - Vite+ build -- `vite-plus`, its Vite core alias, and its test alias use the same fixed - version diff --git a/SECURITY.md b/SECURITY.md index 3a8c4fa..59fcc03 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,12 +3,11 @@ Do not report security vulnerabilities in a public issue. Use -[GitHub private vulnerability reporting](https://github.com/samebase/app/security/advisories/new) +[GitHub private vulnerability reporting](../../security/advisories/new) to report a vulnerability. Include the affected workflow, the expected result, the actual result, and the minimum steps needed to reproduce it. The repository owner must enable private vulnerability reporting before the repository becomes public. -Use [GitHub Issues](https://github.com/samebase/app/issues) for non-sensitive bugs and support -requests. +Use [GitHub Issues](../../issues) for non-sensitive bugs and support requests. diff --git a/docs/package-imports.md b/docs/package-imports.md deleted file mode 100644 index aa6278b..0000000 --- a/docs/package-imports.md +++ /dev/null @@ -1,60 +0,0 @@ -# Package imports research - -Date: 2026-05-17 - -## Decision - -Use package imports for app-internal source aliases in this template. The -mapping lives in `package.json#imports`, and TypeScript, Vite, Vite+, shadcn, -Bun, and Deno all have enough support for the pattern to be a good long-term -direction. - -For this app, prefer shadcn's named roots: - -```json -{ - "imports": { - "#components/*": "./src/components/*.tsx", - "#lib/*": "./src/lib/*.ts", - "#hooks/*": "./src/hooks/*.ts" - } -} -``` - -Do not use `#/*` as the catch-all spelling. It works in current Node 24, Bun, -and Deno, but Node 22 rejects `#/...` specifiers. Named roots such as -`#components/*`, `#lib/*`, `#hooks/*`, and `#src/*` avoid that runtime-version -edge. - -## Findings - -- Node defines package imports as private, package-local mappings that start - with `#`. -- TypeScript resolves package imports by default under - `moduleResolution: "bundler"`, so this app does not need a duplicate - `compilerOptions.paths` alias. -- Vite 8 and Vite+ 0.1.18 both build a small package-imports fixture without a - `resolve.alias` entry. Vite+ `vp check` also passed after formatting. -- Bun 1.3 and Deno 2.7 both resolved a package-imports fixture successfully. -- shadcn 4.7 reads package imports and generates the expected import spelling. - With extension-specific targets, it emits extensionless imports such as - `#lib/utils`. -- In monorepos, package imports are scoped to the package containing the source - file. Use per-workspace `imports` for private app paths and package `exports` - for shared workspace APIs. - -## Samebase follow-up - -Full Samebase has many more `@/...` imports and mixed `.ts` / `.tsx` files under -the same top-level folders. A future Samebase migration should either use explicit -source extensions with a broad `#src/*` mapping, or define named roots carefully -enough that each root maps to one source extension. - -References: - -- https://nodejs.org/api/packages.html#subpath-imports -- https://www.typescriptlang.org/tsconfig/resolvePackageJsonImports.html -- https://ui.shadcn.com/docs/package-imports -- https://ui.shadcn.com/docs/components-json -- https://bun.sh/docs/runtime/module-resolution -- https://docs.deno.com/runtime/fundamentals/node/ diff --git a/package.json b/package.json index fb6973f..223a0b1 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "samebase-app", "version": "0.0.0", "private": true, + "license": "Apache-2.0", "type": "module", "imports": { "#components/*": "./src/components/*.tsx", diff --git a/tsconfig.json b/tsconfig.json index 94a8d3d..7027d47 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "lib": ["ES2023", "DOM", "DOM.Iterable"], "types": ["vite/client", "node"], "skipLibCheck": true, + "strict": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, From 44ae2529589bcb012424b53704c2392109e35c04 Mon Sep 17 00:00:00 2001 From: Nicu Chiciuc Date: Tue, 4 Aug 2026 01:44:41 +0300 Subject: [PATCH 3/5] Cover explicit Cloudflare dry-run flags --- docs/cloudflare-workers-builds.md | 10 ++++++---- scripts/deploy-cloudflare.test.ts | 22 ++++++++++++++++++++++ scripts/deploy-cloudflare.ts | 6 +++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/docs/cloudflare-workers-builds.md b/docs/cloudflare-workers-builds.md index f399059..0e7ab69 100644 --- a/docs/cloudflare-workers-builds.md +++ b/docs/cloudflare-workers-builds.md @@ -42,10 +42,12 @@ name, so repeated commits reuse one preview deployment, URL, and data. Cloudflare may build more than one commit from the same branch concurrently. Stable naming does not order those builds: without another check, an older build that finishes last can replace newer Convex functions. After building the app -and immediately before Convex pushes functions, this template compares -`WORKERS_CI_COMMIT_SHA` with the remote head of `WORKERS_CI_BRANCH`. A stale -build fails without deploying Convex. The check applies to `main` too, where the -same overlap could otherwise roll production back. +and immediately before Convex pushes functions, this template compares the +checked-out Git commit with the remote head of `WORKERS_CI_BRANCH`. A stale +build fails without deploying Convex. The checkout is authoritative because a +manual Workers Build can report the branch name in `WORKERS_CI_COMMIT_SHA`. The +check applies to `main` too, where the same overlap could otherwise roll +production back. The check adds one authenticated `git ls-remote` request to each provider build. It is not an atomic compare-and-swap. A branch can still advance in the short diff --git a/scripts/deploy-cloudflare.test.ts b/scripts/deploy-cloudflare.test.ts index 4d77b71..07a3f54 100644 --- a/scripts/deploy-cloudflare.test.ts +++ b/scripts/deploy-cloudflare.test.ts @@ -14,6 +14,28 @@ describe("deploy-cloudflare", () => { }); }); + it("recognizes Wrangler's explicit true dry-run value", () => { + expect( + selectCloudflareDeployPlan(["deploy", "--dry-run=true"], { + CLOUDFLARE_WORKER_NAME: "example-app", + }), + ).toEqual({ + buildArgs: ["run", "build:app"], + wranglerArgs: ["deploy", "--name", "example-app", "--dry-run=true"], + }); + }); + + it("keeps Wrangler's explicit false dry-run value on the deploy path", () => { + expect( + selectCloudflareDeployPlan(["deploy", "--dry-run=false"], { + CLOUDFLARE_WORKER_NAME: "example-app", + }), + ).toEqual({ + buildArgs: ["run", "build:cloudflare"], + wranglerArgs: ["deploy", "--name", "example-app", "--dry-run=false"], + }); + }); + it("uses the complete Cloudflare build before a local deploy", () => { expect( selectCloudflareDeployPlan(["preview"], { diff --git a/scripts/deploy-cloudflare.ts b/scripts/deploy-cloudflare.ts index 10483ea..76e72ee 100644 --- a/scripts/deploy-cloudflare.ts +++ b/scripts/deploy-cloudflare.ts @@ -18,6 +18,10 @@ function isReservedWranglerFlag(value: string) { return value === "--name" || value.startsWith("--name=") || value === "-n"; } +function isDryRunFlag(value: string) { + return value === "--dry-run" || value === "--dry-run=true"; +} + function readWorkerName(env: NodeJS.ProcessEnv) { const workerName = env.WRANGLER_CI_OVERRIDE_NAME ?? env.CLOUDFLARE_WORKER_NAME; @@ -80,7 +84,7 @@ export function selectCloudflareDeployPlan( const workerName = readWorkerName(env); const isWorkersBuild = env.WORKERS_CI === "1" || env.WORKERS_CI === "true"; - const isDryRun = extraArgs.includes("--dry-run"); + const isDryRun = extraArgs.some(isDryRunFlag); return { buildArgs: isWorkersBuild ? null : ["run", isDryRun ? "build:app" : "build:cloudflare"], From cedb75a3398eaa02a9961829dd46de06039fb919 Mon Sep 17 00:00:00 2001 From: Nicu Chiciuc Date: Tue, 4 Aug 2026 01:47:09 +0300 Subject: [PATCH 4/5] Reject hidden Wrangler deploy options --- scripts/deploy-cloudflare.test.ts | 8 ++++++++ scripts/deploy-cloudflare.ts | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/scripts/deploy-cloudflare.test.ts b/scripts/deploy-cloudflare.test.ts index 07a3f54..d37c3c7 100644 --- a/scripts/deploy-cloudflare.test.ts +++ b/scripts/deploy-cloudflare.test.ts @@ -72,4 +72,12 @@ describe("deploy-cloudflare", () => { "Do not pass Wrangler --name/-n manually", ); }); + + it("rejects an option terminator that can hide a later dry-run flag", () => { + expect(() => + selectCloudflareDeployPlan(["deploy", "--", "--dry-run=true"], { + CLOUDFLARE_WORKER_NAME: "example-app", + }), + ).toThrow("Do not pass a standalone -- to Wrangler"); + }); }); diff --git a/scripts/deploy-cloudflare.ts b/scripts/deploy-cloudflare.ts index 76e72ee..0acd6fb 100644 --- a/scripts/deploy-cloudflare.ts +++ b/scripts/deploy-cloudflare.ts @@ -82,6 +82,12 @@ export function selectCloudflareDeployPlan( ); } + if (extraArgs.includes("--")) { + throw new Error( + "Do not pass a standalone -- to Wrangler. Pass Wrangler flags directly after the deploy command.", + ); + } + const workerName = readWorkerName(env); const isWorkersBuild = env.WORKERS_CI === "1" || env.WORKERS_CI === "true"; const isDryRun = extraArgs.some(isDryRunFlag); From 229ce1583c8d691b182cd6a253672502ea130a1d Mon Sep 17 00:00:00 2001 From: Nicu Chiciuc Date: Tue, 4 Aug 2026 02:47:17 +0300 Subject: [PATCH 5/5] Enable low-friction TypeScript checks --- convex/auth.config.ts | 2 +- convex/tsconfig.json | 8 ++++++++ scripts/build-cloudflare.ts | 4 ++-- scripts/deploy-cloudflare.ts | 4 ++-- scripts/verify-current-branch-head.ts | 4 ++-- src/lib/convex.tsx | 2 +- tsconfig.json | 9 +++++++++ tsconfig.node.json | 9 +++++++++ 8 files changed, 34 insertions(+), 8 deletions(-) diff --git a/convex/auth.config.ts b/convex/auth.config.ts index 1c2319f..9ded75a 100644 --- a/convex/auth.config.ts +++ b/convex/auth.config.ts @@ -3,7 +3,7 @@ export default { providers: [ { - domain: process.env.CONVEX_SITE_URL, + domain: process.env["CONVEX_SITE_URL"], applicationID: "convex", }, ], diff --git a/convex/tsconfig.json b/convex/tsconfig.json index 41bfbb9..e9f48b4 100644 --- a/convex/tsconfig.json +++ b/convex/tsconfig.json @@ -7,6 +7,14 @@ /* These settings are not required by Convex and can be modified. */ "allowJs": true, "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedSideEffectImports": true, + "noImplicitReturns": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": false, + "allowUnreachableCode": false, + "allowUnusedLabels": false, "moduleResolution": "Bundler", "jsx": "react-jsx", "skipLibCheck": true, diff --git a/scripts/build-cloudflare.ts b/scripts/build-cloudflare.ts index 0231bb7..422e347 100644 --- a/scripts/build-cloudflare.ts +++ b/scripts/build-cloudflare.ts @@ -50,7 +50,7 @@ function isEnabled(value: string | undefined) { } function isWorkersBuild(env: NodeJS.ProcessEnv) { - return isEnabled(env.WORKERS_CI); + return isEnabled(env["WORKERS_CI"]); } function readDeployKey(args: { @@ -79,7 +79,7 @@ async function ensureConvexAuth(env: NodeJS.ProcessEnv) { } export function selectConvexDeployPlan(env: NodeJS.ProcessEnv): ConvexDeployPlan { - const branch = env.WORKERS_CI_BRANCH; + const branch = env["WORKERS_CI_BRANCH"]; if (!branch) { if (isWorkersBuild(env)) { diff --git a/scripts/deploy-cloudflare.ts b/scripts/deploy-cloudflare.ts index 0acd6fb..5c20644 100644 --- a/scripts/deploy-cloudflare.ts +++ b/scripts/deploy-cloudflare.ts @@ -23,7 +23,7 @@ function isDryRunFlag(value: string) { } function readWorkerName(env: NodeJS.ProcessEnv) { - const workerName = env.WRANGLER_CI_OVERRIDE_NAME ?? env.CLOUDFLARE_WORKER_NAME; + const workerName = env["WRANGLER_CI_OVERRIDE_NAME"] ?? env["CLOUDFLARE_WORKER_NAME"]; if (!workerName) { throw new Error( @@ -89,7 +89,7 @@ export function selectCloudflareDeployPlan( } const workerName = readWorkerName(env); - const isWorkersBuild = env.WORKERS_CI === "1" || env.WORKERS_CI === "true"; + const isWorkersBuild = env["WORKERS_CI"] === "1" || env["WORKERS_CI"] === "true"; const isDryRun = extraArgs.some(isDryRunFlag); return { diff --git a/scripts/verify-current-branch-head.ts b/scripts/verify-current-branch-head.ts index c064cea..39f22e0 100644 --- a/scripts/verify-current-branch-head.ts +++ b/scripts/verify-current-branch-head.ts @@ -21,11 +21,11 @@ export function verifyCurrentBranchHead( }, readCheckoutHead = () => execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(), ) { - if (env.WORKERS_CI !== "1" && env.WORKERS_CI !== "true") { + if (env["WORKERS_CI"] !== "1" && env["WORKERS_CI"] !== "true") { return; } - const branch = env.WORKERS_CI_BRANCH; + const branch = env["WORKERS_CI_BRANCH"]; if (!branch) { throw new Error("Workers Builds must provide WORKERS_CI_BRANCH before Convex deploys."); diff --git a/src/lib/convex.tsx b/src/lib/convex.tsx index c02b3cf..045e142 100644 --- a/src/lib/convex.tsx +++ b/src/lib/convex.tsx @@ -2,7 +2,7 @@ import { ConvexAuthProvider } from "@convex-dev/auth/react"; import { ConvexReactClient } from "convex/react"; import type { ReactNode } from "react"; -const convexUrl = import.meta.env.VITE_CONVEX_URL; +const convexUrl = import.meta.env["VITE_CONVEX_URL"]; const convexClient = convexUrl ? new ConvexReactClient(convexUrl) : null; export function ConvexClientProvider({ children }: Readonly<{ children: ReactNode }>) { diff --git a/tsconfig.json b/tsconfig.json index 7027d47..afee219 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,15 @@ "types": ["vite/client", "node"], "skipLibCheck": true, "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedSideEffectImports": true, + "noImplicitReturns": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": false, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "forceConsistentCasingInFileNames": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, diff --git a/tsconfig.node.json b/tsconfig.node.json index 4694f48..d46e7e2 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -10,6 +10,15 @@ "moduleDetection": "force", "noEmit": true, "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedSideEffectImports": true, + "noImplicitReturns": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": false, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "forceConsistentCasingInFileNames": true, "noUnusedLocals": true, "noUnusedParameters": true, "erasableSyntaxOnly": true,