diff --git a/.changeset/warm-panthers-thank.md b/.changeset/warm-panthers-thank.md new file mode 100644 index 0000000000..7a1312c7cd --- /dev/null +++ b/.changeset/warm-panthers-thank.md @@ -0,0 +1,5 @@ +--- +'@electric-sql/start': major +--- + +feat: publish the @electric-sql/start package diff --git a/examples/tanstack-db-web-starter/.env.example b/examples/tanstack-db-web-starter/.env.example index 74a014d84b..8863847b30 100644 --- a/examples/tanstack-db-web-starter/.env.example +++ b/examples/tanstack-db-web-starter/.env.example @@ -6,7 +6,12 @@ DATABASE_URL=postgresql://postgres:password@localhost:54321/electric # Generate a strong secret (minimum 32 characters) BETTER_AUTH_SECRET= -# Production Electric Cloud Configuration (optional) +# Electric Cloud Configuration (optional) # Get these from https://dashboard.electric-sql.cloud/ +# or use `npx @electric-sql/start` to provision automatically. +# If not set, it will try to connect to local Electric at `http://localhost:30000` +# and you can start it using `pnpm backend:up` +# +# ELECTRIC_URL=https://api.electric-sql.cloud # ELECTRIC_SOURCE_ID=your-source-id -# ELECTRIC_SOURCE_SECRET=your-source-secret +# ELECTRIC_SECRET=your-source-secret diff --git a/examples/tanstack-db-web-starter/.stackblitzrc b/examples/tanstack-db-web-starter/.stackblitzrc new file mode 100644 index 0000000000..1e55ff7ba6 --- /dev/null +++ b/examples/tanstack-db-web-starter/.stackblitzrc @@ -0,0 +1,4 @@ +{ + "installDependencies": false, + "startCommand": "npx @electric-sql/start . && pnpm dev" +} diff --git a/examples/tanstack-db-web-starter/.tool-versions b/examples/tanstack-db-web-starter/.tool-versions index 6264dcc052..e18227d526 100644 --- a/examples/tanstack-db-web-starter/.tool-versions +++ b/examples/tanstack-db-web-starter/.tool-versions @@ -1 +1,2 @@ -nodejs 24.11.1 \ No newline at end of file +nodejs 24.11.1 +caddy 2.10.2 diff --git a/examples/tanstack-db-web-starter/AGENTS.md b/examples/tanstack-db-web-starter/AGENTS.md index f583988582..ae296f1e0a 100644 --- a/examples/tanstack-db-web-starter/AGENTS.md +++ b/examples/tanstack-db-web-starter/AGENTS.md @@ -10,13 +10,26 @@ We sync normalized data from tables into TanStack DB collections in the client & ## General Usage -Read https://electric-sql.com/AGENTS.md for general information on developing wth Electric and TanStack DB. +You MUST read https://electric-sql.com/AGENTS.md for general information on developing wth Electric and TanStack DB. ## Starter template specifics +You CAN choose to read the `README.md` for this project if useful. Much of it is summarized below. + +### Pre-reqs + +Docker, Caddy (with root cert installed using `caddy trust`), Node and pnpm. Versions in `.tool-versions`. + ### Initial setup -Before you started, all necessary package install is done via `pnpm install` and a dev server is started with `pnpm dev`. +```sh +pnpm install # install deps +pnpm backend:up # run backend services using docker compose +pnpm migrate # apply migrations +pnpm dev # run dev server +``` + +The dev server runs over HTTPS via a CaddyPlugin in the vite config. This supports HTTP/2 which is essential to avoid slow shapes for Electric. ### Linting and formatting @@ -26,11 +39,11 @@ This command will also report linter errors that were not automatically fixable. ### Build/Test Commands -- `pnpm run dev` - Start development server with Docker services -- `pnpm run build` - Build for production -- `pnpm run test` - Run all Vitest tests +- `pnpm dev` - Start development server with Docker services +- `pnpm build` - Build for production +- `pnpm test` - Run all Vitest tests - `vitest run ` - Run single test file -- `pnpm run start` - Start production server +- `pnpm start` - Start production server ### Architecture diff --git a/examples/tanstack-db-web-starter/README.md b/examples/tanstack-db-web-starter/README.md index b7dccda7e2..a37c40fecc 100644 --- a/examples/tanstack-db-web-starter/README.md +++ b/examples/tanstack-db-web-starter/README.md @@ -1,10 +1,34 @@ Welcome to your new TanStack [Start](https://tanstack.com/start/latest) / [DB](https://tanstack.com/db/latest) + [Electric](https://electric-sql.com/) app! -# Getting Started +# Getting started -## Create a new project +## Pre-requisites + +You need: + +- [Docker](https://www.docker.com) +- [Caddy](https://caddyserver.com) +- [Node](https://nodejs.org/en) with [pnpm](https://pnpm.io) + +You can see compatible versions in the `.tool-versions` file. + +### Docker + +Make sure you have [Docker](https://www.docker.com) running. Docker is used to run the [Postgres](https://www.postgresql.org) and [Electric](https://electric-sql.com) services defined in `docker-compose.yaml`. -To create a new project based on this starter, run the following commands: +### Caddy + +Make sure you have [Caddy installed](https://caddyserver.com/docs/install) and have [installed its root certificate](https://caddyserver.com/docs/command-line#caddy-trust) using: + +```sh +caddy trust # may require sudo +``` + +Electric [benefits significantly from `HTTP/2` multiplexing](https://electric-sql.com/docs/guides/troubleshooting#slow-shapes-mdash-why-are-my-shapes-slow-in-the-browser-in-local-development). `HTTP/2` requires `HTTPS`. Caddy is necessary for `HTTPS` to work in local development. + +## Quickstart + +Create a new project based on this starter: ```sh npx gitpick electric-sql/electric/tree/main/examples/tanstack-db-web-starter my-tanstack-db-project @@ -17,42 +41,45 @@ Copy the `.env.example` file to `.env`: cp .env.example .env ``` -_You can edit the values in the `.env` file, although the default values are fine for local development (with the `DATABASE_URL` defaulting to the development Postgres docker container and the `BETTER_AUTH_SECRET` not required)._ +> [!Tip] +> You can edit the values in the `.env` file. The default values are configured for local development with Docker. You can run against a different Postgres and Electric, for example using the Electric Cloud, by changing the `DATABASE_URL` and `ELECTRIC_URL`. -## Quick Start +Install the dependencies: -Follow these steps in order for a smooth first-time setup: +```sh +pnpm install +``` -1. **Install dependencies:** +Start the backend services (Postgres and Electric) running in the background using Docker: - ```sh - pnpm install - ``` +```sh +pnpm backend:up +``` -2. **Start Docker services:** +Apply the database migrations: - ```sh - pnpm run dev - ``` +```sh +pnpm migrate +``` - This starts the dev server, Docker Compose (Postgres + Electric), and Caddy automatically. +Start the dev server: -3. **Run database migrations** (in a new terminal): +```sh +pnpm dev +``` - ```sh - pnpm run migrate - ``` +Open the application on [https://localhost:5173](https://localhost:5173). -4. **Visit the application:** - Open [https://tanstack-start-db-electric-starter.localhost](https://tanstack-start-db-electric-starter.localhost) +> [!Tip] +> If you run into any issues, see the [troubleshooting](#troubleshooting) section below. -If you run into issues, see the [pre-reqs](#pre-requisites) and [troubleshooting](#common-pitfalls) sections below. +# Developing your app ## Adding a New Table Here's how to add a new table to your app (using a "categories" table as an example): -### 1. Define the Drizzle Schema +### 1. Define the Drizzle schema Add your table to `src/db/schema.ts`: @@ -75,7 +102,7 @@ export const createCategorySchema = createInsertSchema(categoriesTable).omit({ export const updateCategorySchema = createUpdateSchema(categoriesTable) ``` -### 2. Generate & Run Migration +### 2. Generate and run migrations ```sh # Generate migration file @@ -85,7 +112,7 @@ pnpm migrate:generate pnpm migrate ``` -### 3. Add Electric Shape Route +### 3. Expose an Electric shape route Create `src/routes/api/categories.ts`: @@ -117,7 +144,7 @@ export const ServerRoute = createServerFileRoute("/api/categories").methods({ }) ``` -### 4. Add tRPC Router +### 4. Add a tRPC router Create `src/lib/trpc/categories.ts`: @@ -150,7 +177,7 @@ export const categoriesRouter = router({ }) ``` -### 5. Wire Up tRPC Router +### 5. Wire up the tRPC router Add to `src/routes/api/trpc/$.ts`: @@ -163,7 +190,7 @@ export const appRouter = router({ }) ``` -### 6. Add Collection +### 6. Add a TanStack DB collection Add to `src/lib/collections.ts`: @@ -192,7 +219,7 @@ export const categoriesCollection = createCollection( ) ``` -### 7. Use in Routes +### 7. Use the collection in your routes Preload in route loaders and use with `useLiveQuery`: @@ -212,17 +239,9 @@ const { data: categories } = useLiveQuery((q) => That's it! Your new table is now fully integrated with Electric sync, tRPC mutations, and TanStack DB queries. -## Pre-requisites - -This project uses [Docker](https://www.docker.com), [Node](https://nodejs.org/en) with [pnpm](https://pnpm.io) and [Caddy](https://caddyserver.com/). You can see compatible versions in the `.tool-versions` file. - -### Docker - -Make sure you have Docker running. Docker is used to run the Postgres and Electric services defined in `docker-compose.yaml`. - -### Caddy +## Notes -#### Why Caddy? +### About Caddy Electric SQL's shape delivery benefits significantly from **HTTP/2 multiplexing**. Without HTTP/2, each shape subscription creates a new HTTP/1.1 connection, which browsers limit to 6 concurrent connections per domain. This creates a bottleneck that makes shapes appear slow. @@ -315,7 +334,7 @@ caddy start To build this application for production: ```bash -pnpm run build +pnpm build ``` ### Production Deployment Checklist diff --git a/examples/tanstack-db-web-starter/drizzle.config.ts b/examples/tanstack-db-web-starter/drizzle.config.ts index 6a6f388fd3..b92e533ed5 100644 --- a/examples/tanstack-db-web-starter/drizzle.config.ts +++ b/examples/tanstack-db-web-starter/drizzle.config.ts @@ -1,4 +1,4 @@ -import "dotenv/config" +import "@dotenvx/dotenvx/config" import { defineConfig } from "drizzle-kit" export default defineConfig({ diff --git a/examples/tanstack-db-web-starter/package.json b/examples/tanstack-db-web-starter/package.json index 0a6bf5da6c..f3f86a4b9d 100644 --- a/examples/tanstack-db-web-starter/package.json +++ b/examples/tanstack-db-web-starter/package.json @@ -7,11 +7,15 @@ "node": ">=20.19.0 || >=22.12.0" }, "scripts": { - "dev": "docker compose up -d && vite dev", + "dev": "vite dev", + "backend:up": "docker compose up -d", + "backend:down": "docker compose down", + "backend:clear": "docker compose down -v", "start": "node .output/server/index.mjs", "build": "vite build", "migrate": "drizzle-kit migrate", "migrate:generate": "drizzle-kit generate", + "psql": "dotenvx run -- sh -c 'psql \"$DATABASE_URL\"'", "serve": "vite preview", "test": "echo 'No tests yet'", "lint:check": "eslint .", @@ -43,6 +47,7 @@ "zod": "^4.0.14" }, "devDependencies": { + "@dotenvx/dotenvx": "^1.51.2", "@eslint/compat": "^1.3.1", "@eslint/js": "^9.32.0", "@tanstack/devtools-vite": "^0.3.11", @@ -56,12 +61,12 @@ "@typescript-eslint/parser": "^8.46.0", "@vitejs/plugin-react": "^5.0.4", "drizzle-kit": "^0.31.4", - "dotenv": "^17.2.1", "eslint": "^9.32.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.3", "eslint-plugin-react": "^7.37.5", "jsdom": "^27.0.0", + "open-cli": "^8.0.0", "prettier": "^3.6.2", "tsx": "^4.20.3", "typescript": "^5.7.2", diff --git a/examples/tanstack-db-web-starter/src/db/connection.ts b/examples/tanstack-db-web-starter/src/db/connection.ts index 5a8ea8beb0..8ca900512c 100644 --- a/examples/tanstack-db-web-starter/src/db/connection.ts +++ b/examples/tanstack-db-web-starter/src/db/connection.ts @@ -1,4 +1,10 @@ -import "dotenv/config" +import "@dotenvx/dotenvx/config" import { drizzle } from "drizzle-orm/node-postgres" +import { Pool } from "pg" -export const db = drizzle(process.env.DATABASE_URL!, { casing: `snake_case` }) +const databaseUrl = process.env.DATABASE_URL +if (!databaseUrl) { + throw new Error(`DATABASE_URL is not set`) +} +const pool = new Pool({ connectionString: databaseUrl }) +export const db = drizzle({ client: pool, casing: `snake_case` }) diff --git a/examples/tanstack-db-web-starter/src/lib/electric-proxy.ts b/examples/tanstack-db-web-starter/src/lib/electric-proxy.ts index 75261e43eb..d1ea9b7a29 100644 --- a/examples/tanstack-db-web-starter/src/lib/electric-proxy.ts +++ b/examples/tanstack-db-web-starter/src/lib/electric-proxy.ts @@ -1,5 +1,17 @@ +import "@dotenvx/dotenvx/config" import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from "@electric-sql/client" +/** + * Gets the Electric SQL endpoint URL based on environment configuration. + * + * If running in production, or `USE_ELECTRIC_URL` is set to `true`, the `ELECTRIC_URL` env var is used, + * if available, otherwise the default cloud endpoint is used. + * Otherwise, the local docker endpoint is used, assuming default port 30000. + */ +function getElectricUrl(): string { + return process.env.ELECTRIC_URL || `http://localhost:30000` +} + /** * Prepares the Electric SQL proxy URL from a request URL * Copies over Electric-specific query params and adds auth if configured @@ -8,10 +20,7 @@ import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from "@electric-sql/client" */ export function prepareElectricUrl(requestUrl: string): URL { const url = new URL(requestUrl) - const electricUrl = - process.env.NODE_ENV === `production` - ? `https://api.electric-sql.cloud` - : `http://localhost:30000` + const electricUrl = getElectricUrl() const originUrl = new URL(`${electricUrl}/v1/shape`) // Copy Electric-specific query params @@ -22,9 +31,9 @@ export function prepareElectricUrl(requestUrl: string): URL { }) // Add Electric Cloud authentication if configured - if (process.env.ELECTRIC_SOURCE_ID && process.env.ELECTRIC_SOURCE_SECRET) { + if (process.env.ELECTRIC_SOURCE_ID && process.env.ELECTRIC_SECRET) { originUrl.searchParams.set(`source_id`, process.env.ELECTRIC_SOURCE_ID) - originUrl.searchParams.set(`secret`, process.env.ELECTRIC_SOURCE_SECRET) + originUrl.searchParams.set(`secret`, process.env.ELECTRIC_SECRET) } return originUrl diff --git a/examples/tanstack-db-web-starter/src/vite-plugin-caddy.ts b/examples/tanstack-db-web-starter/src/vite-plugin-caddy.ts index 1c2abd1f3b..18d339e2de 100644 --- a/examples/tanstack-db-web-starter/src/vite-plugin-caddy.ts +++ b/examples/tanstack-db-web-starter/src/vite-plugin-caddy.ts @@ -1,11 +1,10 @@ -import { spawn, type ChildProcess } from "child_process" +import { spawn, spawnSync, type ChildProcess } from "child_process" import { writeFileSync } from "fs" -import { readFileSync } from "fs" -import { networkInterfaces } from "os" import type { Plugin } from "vite" interface CaddyPluginOptions { host?: string + httpsPort?: number encoding?: boolean autoStart?: boolean configPath?: string @@ -14,6 +13,7 @@ interface CaddyPluginOptions { export function caddyPlugin(options: CaddyPluginOptions = {}): Plugin { const { host = `localhost`, + httpsPort = 5173, encoding = true, autoStart = true, configPath = `Caddyfile`, @@ -23,36 +23,8 @@ export function caddyPlugin(options: CaddyPluginOptions = {}): Plugin { let vitePort: number | undefined let caddyStarted = false - const generateCaddyfile = (projectName: string, vitePort: number) => { - // Get network IP for network access - const nets = networkInterfaces() - let networkIP = `192.168.1.1` // fallback - - for (const name of Object.keys(nets)) { - const netInterfaces = nets[name] - if (netInterfaces) { - for (const net of netInterfaces) { - if (net.family === `IPv4` && !net.internal) { - networkIP = net.address - break - } - } - } - } - - const config = `${projectName}.localhost { - reverse_proxy ${host}:${vitePort}${ - encoding - ? ` - encode { - gzip - }` - : `` - } -} - -# Network access -${networkIP} { + const generateCaddyfile = (vitePort: number) => { + const config = `localhost:${httpsPort} { reverse_proxy ${host}:${vitePort}${ encoding ? ` @@ -121,12 +93,31 @@ ${networkIP} { } } - const startCaddyIfReady = (projectName: string) => { + const startCaddyIfReady = () => { if (autoStart && vitePort && !caddyStarted) { caddyStarted = true + + // Check if `caddy` binary is available before starting (sync) + try { + const check = spawnSync(`caddy`, [`--version`], { stdio: `ignore` }) + if (check.error || check.status !== 0) { + throw new Error( + `\`caddy\` binary not found or is not working. Please ensure Caddy is installed and available in your PATH.` + ) + } + } catch (_err) { + console.error( + `\`caddy\` binary not found or is not working. Please ensure Caddy is installed and available in your PATH.`, + `\nCaddy is required to be able to serve local development with HTTP2 support.`, + `\n - Install Caddy: https://caddyserver.com/docs/install`, + `\n - If you have \`asdf\`, run \`asdf install\`` + ) + process.exit(1) + } // Generate Caddyfile - const caddyConfig = generateCaddyfile(projectName, vitePort) + const caddyConfig = generateCaddyfile(vitePort) writeFileSync(configPath, caddyConfig) + // Start Caddy startCaddy(configPath) } @@ -135,62 +126,31 @@ ${networkIP} { return { name: `vite-plugin-caddy`, configureServer(server) { - let projectName = `app` - - // Get project name from package.json - try { - const packageJsonContent = readFileSync( - process.cwd() + `/package.json`, - `utf8` - ) - const packageJson = JSON.parse(packageJsonContent) - projectName = packageJson.name || `app` - } catch (_error) { - console.warn( - `Could not read package.json for project name, using "app"` - ) - } - // Override Vite's printUrls function server.printUrls = function () { - // Get network IP - const nets = networkInterfaces() - let networkIP = `192.168.1.1` // fallback - - for (const name of Object.keys(nets)) { - const netInterfaces = nets[name] - if (netInterfaces) { - for (const net of netInterfaces) { - if (net.family === `IPv4` && !net.internal) { - networkIP = net.address - break - } - } - } - } - console.log() - console.log(` ➜ Local: https://${projectName}.localhost/`) - console.log(` ➜ Network: https://${networkIP}/`) - console.log(` ➜ press h + enter to show help`) + console.log(` ➜ Local: https://localhost:${httpsPort}/`) console.log() + console.log( + ` Note: running through Caddy. You might be prompted for password to install HTTPS certificates for local development.` + ) } server.middlewares.use((_req, _res, next) => { if (!vitePort && server.config.server.port) { vitePort = server.config.server.port - startCaddyIfReady(projectName) + startCaddyIfReady() } next() }) const originalListen = server.listen - server.listen = function (port?: number, ...args: unknown[]) { + server.listen = function (port?: number, isRestart?: boolean) { if (port) { vitePort = port } - const result = originalListen.call(this, port, ...args) + const result = originalListen.call(this, port, isRestart) // Try to start Caddy after server is listening if (result && typeof result.then === `function`) { @@ -199,10 +159,10 @@ ${networkIP} { if (!vitePort && server.config.server.port) { vitePort = server.config.server.port } - startCaddyIfReady(projectName) + startCaddyIfReady() }) } else { - startCaddyIfReady(projectName) + startCaddyIfReady() } return result diff --git a/examples/tanstack-db-web-starter/vite.config.ts b/examples/tanstack-db-web-starter/vite.config.ts index ef9ccf112d..82d86a5701 100644 --- a/examples/tanstack-db-web-starter/vite.config.ts +++ b/examples/tanstack-db-web-starter/vite.config.ts @@ -16,11 +16,7 @@ const config = defineConfig({ }), caddyPlugin(), tailwindcss(), - tanstackStart({ - router: { - srcDirectory: `src`, - }, - }), + tanstackStart(), viteReact(), ], optimizeDeps: { diff --git a/package.json b/package.json index 86b7f88a28..04a9abf131 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@changesets/cli": "^2.27.10", "dotenv-cli": "^7.4.2" }, - "packageManager": "pnpm@9.15.0", + "packageManager": "pnpm@10.12.1", "private": true, "scripts": { "ci:publish": "pnpm '/^ci:publish:.+/' && pnpm exec changeset tag", diff --git a/packages/start/.eslintrc.cjs b/packages/start/.eslintrc.cjs new file mode 100644 index 0000000000..6df77a988f --- /dev/null +++ b/packages/start/.eslintrc.cjs @@ -0,0 +1,41 @@ +module.exports = { + env: { + browser: true, + es2021: true, + node: true, + }, + extends: [ + `eslint:recommended`, + `plugin:@typescript-eslint/recommended`, + `plugin:prettier/recommended`, + ], + parserOptions: { + ecmaVersion: 2022, + requireConfigFile: false, + sourceType: `module`, + ecmaFeatures: { + jsx: true, + }, + }, + parser: `@typescript-eslint/parser`, + plugins: [`prettier`], + rules: { + quotes: [`error`, `backtick`], + 'no-unused-vars': `off`, + '@typescript-eslint/no-unused-vars': [ + `error`, + { + argsIgnorePattern: `^_`, + varsIgnorePattern: `^_`, + caughtErrorsIgnorePattern: `^_`, + }, + ], + }, + ignorePatterns: [ + `**/node_modules/**`, + `**/dist/**`, + `tsup.config.ts`, + `vitest.config.ts`, + `.eslintrc.js`, + ], +} diff --git a/packages/start/.prettierrc b/packages/start/.prettierrc new file mode 100644 index 0000000000..f685078fff --- /dev/null +++ b/packages/start/.prettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "semi": false, + "tabWidth": 2, + "singleQuote": true +} diff --git a/packages/start/README.md b/packages/start/README.md new file mode 100644 index 0000000000..7292840a60 --- /dev/null +++ b/packages/start/README.md @@ -0,0 +1,79 @@ +# @electric-sql/start + +CLI package for the [ElectricSQL Quickstart](https://electric-sql.com/docs/quickstart). + +## Usage + +Create a new app using [Electric](https://electric-sql.com/product/electric) with [TanStack DB](https://tanstack.com/db), based on the [examples/tanstack-db-web-starter](https://github.com/electric-sql/electric/tree/main/examples/tanstack-db-web-starter) [TanStack Start](http://tanstack.com/start) template app: + +```bash +pnpx @electric-sql/start my-electric-app +``` + +This command will: + +1. pull in the template app using gitpick +2. provision cloud resources + - a Postgres database using Neon + - an Electric sync service using Electric Cloud + - fetch their access credentials +3. configure the local `.env` to use the credentials +4. add `psql`, `claim` and `deploy` commands to the package.json + - also using the generated credentials + +## Environment Variables + +The CLI automatically generates these environment variables: + +- `DATABASE_URL` - PostgreSQL connection string +- `ELECTRIC_SECRET` - Electric Cloud authentication secret +- `ELECTRIC_SOURCE_ID` - Electric sync service identifier + +## Commands + +```bash +pnpm dev # Start development server +pnpm psql # Connect to PostgreSQL database +pnpm claim # Claim temporary resources +pnpm deploy # Deploy to Netlify +``` + +### `pnpm psql` + +Connect directly to your PostgreSQL database using the configured `DATABASE_URL`: + +### `pnpm claim` + +Claim temporary resources to move them to your permanent Electric Cloud and Neon accounts. + +### `pnpm deploy` + +Deploy your app to Netlify with all environment variables configured. + +## Development + +This package is part of the Electric monorepo. To work on it: + +```bash +# From the monorepo root +pnpm install # Install all workspace dependencies +pnpm build # Build all packages + +# From packages/quickstart +pnpm build # Compile TypeScript +pnpm dev # Build and test locally +``` + +### Testing Against Different API Environments + +The Electric API base URL can be configured via the `ELECTRIC_API_BASE_URL` environment variable. This is useful for testing against staging or development API servers. + +```bash +# Default (production) +pnpm test + +# Against a custom API server +ELECTRIC_API_BASE_URL=https://api.staging.electric-sql.cloud pnpm test +``` + +The default API base URL is `https://api.electric-sql.cloud`. diff --git a/packages/start/package.json b/packages/start/package.json new file mode 100644 index 0000000000..3fcf7665be --- /dev/null +++ b/packages/start/package.json @@ -0,0 +1,67 @@ +{ + "name": "@electric-sql/start", + "version": "0.0.0", + "description": "CLI package for the ElectricSQL Quickstart.", + "type": "module", + "main": "./dist/index.js", + "bin": { + "start": "./dist/cli.js" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "scripts": { + "build": "shx rm -rf dist && tsup", + "prepack": "pnpm build", + "dev": "pnpm run build && node dist/cli.js", + "format": "eslint . --fix", + "stylecheck": "eslint . --quiet", + "test": "pnpm exec vitest", + "coverage": "pnpm exec vitest --coverage", + "typecheck": "tsc -p tsconfig.json" + }, + "dependencies": {}, + "devDependencies": { + "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^7.14.1", + "@typescript-eslint/parser": "^7.14.1", + "@vitest/coverage-istanbul": "4.0.15", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.3.2", + "shx": "^0.3.4", + "tsup": "^8.0.1", + "typescript": "^5.5.2", + "vitest": "^4.0.15" + }, + "files": [ + "dist", + "src" + ], + "keywords": [ + "cli", + "db", + "electric", + "electric-sql", + "start", + "starter", + "tanstack" + ], + "author": "ElectricSQL team and contributors.", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/electric-sql/electric.git" + }, + "bugs": { + "url": "https://github.com/electric-sql/electric/issues" + }, + "homepage": "https://electric-sql.com" +} diff --git a/packages/start/src/cli.ts b/packages/start/src/cli.ts new file mode 100644 index 0000000000..5397192713 --- /dev/null +++ b/packages/start/src/cli.ts @@ -0,0 +1,108 @@ +#!/usr/bin/env node + +import { execSync } from 'child_process' +import { provisionElectricResources } from './electric-api.js' +import { setupTemplate } from './template-setup.js' +import { join } from 'path' + +function printNextSteps(appName: string, fullSetup: boolean = false) { + console.log(`Next steps:`) + if (appName !== `.`) { + console.log(` cd ${appName}`) + } + + if (fullSetup) { + console.log(` pnpm install`) + console.log(` pnpm migrate`) + } + + console.log(` pnpm dev`) + console.log(``) + console.log(`Commands:`) + console.log(` pnpm psql # Connect to database`) + console.log(` pnpm claim # Claim cloud resources`) + console.log(` pnpm deploy:netlify # Deploy to Netlify`) + console.log(``) + console.log(`Tutorial: https://electric-sql.com/docs`) +} + +async function main() { + const args = process.argv.slice(2) + + if (args.length === 0) { + console.error(`Usage: npx @electric-sql/start `) + console.error( + ` npx @electric-sql/start . (configure current directory)` + ) + + process.exit(1) + } + + const appName = args[0] + + // Validate app name (skip validation for "." which means current directory) + if (appName !== `.` && !/^[a-zA-Z0-9-_]+$/.test(appName)) { + console.error( + `App name must contain only letters, numbers, hyphens, and underscores` + ) + + process.exit(1) + } + + if (appName === `.`) { + console.log(`Configuring current directory...`) + } else { + console.log(`Creating app: ${appName}`) + } + + try { + const credentials = await provisionElectricResources() + + // Step 2: Setup TanStack Start template + console.log(`Setting up template...`) + await setupTemplate(appName, credentials) + + console.log(`Installing dependencies...`) + try { + execSync(`pnpm install`, { + stdio: `inherit`, + cwd: appName === `.` ? process.cwd() : join(process.cwd(), appName), + }) + } catch (_error) { + console.log(`Failed to install dependencies`) + printNextSteps(appName, true) + process.exit(1) + } + + console.log(`Running migrations...`) + try { + execSync(`pnpm migrate`, { + stdio: `inherit`, + cwd: appName === `.` ? process.cwd() : join(process.cwd(), appName), + }) + } catch (_error) { + console.log(`Failed to apply migrations`) + printNextSteps(appName, true) + process.exit(1) + } + + // Step 3: Display completion message + console.log(`Setup complete`) + printNextSteps(appName) + } catch (error) { + console.error( + `Setup failed:`, + error instanceof Error ? error.message : error + ) + process.exit(1) + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((error) => { + console.error(`Unexpected error:`, error) + process.exit(1) + }) +} + +export { main } diff --git a/packages/start/src/electric-api.ts b/packages/start/src/electric-api.ts new file mode 100644 index 0000000000..b66e6eccba --- /dev/null +++ b/packages/start/src/electric-api.ts @@ -0,0 +1,189 @@ +// Using native fetch (Node.js 18+) + +export interface ElectricCredentials { + source_id: string + secret: string + DATABASE_URL: string +} + +export interface ClaimableSourceResponse { + claimId: string +} + +interface ClaimableSourceStatus { + state: `pending` | `ready` | `failed` + source: { + source_id: string + secret: string + } + connection_uri: string + claim_link?: string + project_id?: string + error: string | null +} + +export const DEFAULT_ELECTRIC_API_BASE = `https://dashboard.electric-sql.cloud/api` +export const DEFAULT_ELECTRIC_URL = `https://api.electric-sql.cloud` +export const DEFAULT_ELECTRIC_DASHBOARD_URL = `https://dashboard.electric-sql.cloud` + +export function getElectricApiBase(): string { + return process.env.ELECTRIC_API_BASE_URL ?? DEFAULT_ELECTRIC_API_BASE +} + +export function getElectricUrl(): string { + return process.env.ELECTRIC_URL ?? DEFAULT_ELECTRIC_URL +} + +export function getElectricDashboardUrl(): string { + return process.env.ELECTRIC_DASHBOARD_URL ?? DEFAULT_ELECTRIC_DASHBOARD_URL +} + +const POLL_INTERVAL_MS = 1000 // Poll every 1 second +const MAX_POLL_ATTEMPTS = 60 // Max 60 seconds + +async function pollClaimableSource( + claimId: string, + maxAttempts: number = MAX_POLL_ATTEMPTS +): Promise { + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const response = await fetch( + `${getElectricApiBase()}/public/v1/claimable-sources/${claimId}`, + { + method: `GET`, + headers: { + 'User-Agent': `@electric-sql/start`, + }, + } + ) + + // Handle 404 as "still being provisioned" - continue polling + if (response.status === 404) { + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + continue + } + + // For other non-OK responses, throw an error + if (!response.ok) { + throw new Error( + `Electric API error: ${response.status} ${response.statusText}` + ) + } + + const status = (await response.json()) as ClaimableSourceStatus + + if (status.state === `ready`) { + return status + } + + if (status.state === `failed` || status.error) { + throw new Error( + `Resource provisioning failed${status.error ? `: ${status.error}` : ``}` + ) + } + + // Wait before polling again + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + } + + throw new Error( + `Timeout waiting for resources to be provisioned after ${maxAttempts} attempts` + ) +} + +export async function provisionElectricResources(): Promise< + ElectricCredentials & ClaimableSourceResponse +> { + console.log(`Provisioning resources...`) + try { + // Step 1: POST to create claimable source and get claimId + const response = await fetch( + `${getElectricApiBase()}/public/v1/claimable-sources`, + { + method: `POST`, + headers: { + 'Content-Type': `application/json`, + 'User-Agent': `@electric-sql/start`, + }, + body: JSON.stringify({}), + } + ) + + if (!response.ok) { + throw new Error( + `Electric API error: ${response.status} ${response.statusText}` + ) + } + + const { claimId } = (await response.json()) as ClaimableSourceResponse + + if (!claimId) { + throw new Error(`Invalid response from Electric API - missing claimId`) + } + + // Step 2: Poll until state === 'ready' + const status = await pollClaimableSource(claimId) + + // Step 3: Extract and validate credentials + if ( + !status.source?.source_id || + !status.source?.secret || + !status.connection_uri + ) { + throw new Error( + `Invalid response from Electric API - missing required credentials` + ) + } + + return { + source_id: status.source.source_id, + secret: status.source.secret, + DATABASE_URL: status.connection_uri, + claimId, + } + } catch (error) { + if (error instanceof Error) { + throw new Error( + `Failed to provision Electric resources: ${error.message}` + ) + } + throw new Error(`Failed to provision Electric resources: Unknown error`) + } +} + +export async function claimResources( + sourceId: string, + secret: string +): Promise<{ claimUrl: string }> { + try { + const response = await fetch(`${getElectricApiBase()}/v1/claim`, { + method: `POST`, + headers: { + 'Content-Type': `application/json`, + Authorization: `Bearer ${secret}`, + 'User-Agent': `@electric-sql/start`, + }, + body: JSON.stringify({ + source_id: sourceId, + }), + }) + + if (!response.ok) { + throw new Error( + `Electric API error: ${response.status} ${response.statusText}` + ) + } + + const result = (await response.json()) as { claimUrl: string } + + if (!result.claimUrl) { + throw new Error(`Invalid response from Electric API - missing claim URL`) + } + + return result + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to initiate resource claim: ${error.message}`) + } + throw new Error(`Failed to initiate resource claim: Unknown error`) + } +} diff --git a/packages/start/src/index.ts b/packages/start/src/index.ts new file mode 100644 index 0000000000..789907aba1 --- /dev/null +++ b/packages/start/src/index.ts @@ -0,0 +1,2 @@ +export * from './electric-api.js' +export * from './template-setup.js' diff --git a/packages/start/src/template-setup.ts b/packages/start/src/template-setup.ts new file mode 100644 index 0000000000..6e4115c5e2 --- /dev/null +++ b/packages/start/src/template-setup.ts @@ -0,0 +1,95 @@ +import { execSync } from 'child_process' +import { randomBytes } from 'crypto' +import { writeFileSync, readFileSync, existsSync } from 'fs' +import { join } from 'path' +import { + ElectricCredentials, + ClaimableSourceResponse, + getElectricUrl, + getElectricDashboardUrl, +} from './electric-api' + +/** + * Generates a cryptographically secure random string for use as a secret + * @param length - The length of the secret in bytes (will be hex encoded, so output is 2x length) + * @returns A random hex string + */ +function generateSecret(length: number = 32): string { + return randomBytes(length).toString(`hex`) +} + +export async function setupTemplate( + appName: string, + credentials: ElectricCredentials & ClaimableSourceResponse +): Promise { + const appPath = appName === `.` ? process.cwd() : join(process.cwd(), appName) + + try { + // Step 1: Pull TanStack Start template using gitpick (skip for current directory) + if (appName !== `.`) { + console.log(`Pulling template...`) + execSync( + `npx gitpick electric-sql/electric/tree/main/examples/tanstack-db-web-starter ${appName}`, + { stdio: `inherit` } + ) + } + + // Step 2: Generate .env file with credentials + console.log(`Configuring environment...`) + const betterAuthSecret = generateSecret(32) + const electricUrl = getElectricUrl() + const envContent = `# Electric SQL Configuration +# Generated by @electric-sql/start +# DO NOT COMMIT THIS FILE + +# Database +DATABASE_URL=${credentials.DATABASE_URL} + +# Electric Cloud +ELECTRIC_URL=${electricUrl} +ELECTRIC_SOURCE_ID=${credentials.source_id} +ELECTRIC_SECRET=${credentials.secret} + +# Authentication +BETTER_AUTH_SECRET=${betterAuthSecret} +` + + writeFileSync(join(appPath, `.env`), envContent) + + // Step 3: Ensure .gitignore includes .env + console.log(`Updating .gitignore...`) + const gitignorePath = join(appPath, `.gitignore`) + let gitignoreContent = `` + + if (existsSync(gitignorePath)) { + gitignoreContent = readFileSync(gitignorePath, `utf8`) + } + + if (!gitignoreContent.includes(`.env`)) { + gitignoreContent += `\n# Environment variables\n.env\n.env.local\n.env.*.local\n` + writeFileSync(gitignorePath, gitignoreContent) + } + + console.log(`Adding Electric commands...`) + const packageJsonPath = join(appPath, `package.json`) + + if (existsSync(packageJsonPath)) { + const packageJson = JSON.parse(readFileSync(packageJsonPath, `utf8`)) + + // Add/update scripts for cloud mode and Electric commands + packageJson.scripts = { + ...packageJson.scripts, + claim: `npx open-cli "${getElectricDashboardUrl()}/claim?uuid=${credentials.claimId}"`, + 'deploy:netlify': `NODE_ENV=production NITRO_PRESET=netlify pnpm build && NODE_ENV=production npx netlify deploy --no-build --prod --dir=dist --functions=.netlify/functions-internal && npx netlify env:import .env`, + } + + writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)) + } + + console.log(`Template setup complete`) + } catch (error) { + throw new Error( + `Template setup failed: ${error instanceof Error ? error.message : error}` + ) + } +} diff --git a/packages/start/test/cli.test.ts b/packages/start/test/cli.test.ts new file mode 100644 index 0000000000..48f20419bf --- /dev/null +++ b/packages/start/test/cli.test.ts @@ -0,0 +1,270 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +// Mock the electric-api module +vi.mock(`../src/electric-api.js`, () => ({ + provisionElectricResources: vi.fn(), + DEFAULT_ELECTRIC_API_BASE: `https://api.electric-sql.cloud`, +})) + +// Mock the template-setup module +vi.mock(`../src/template-setup.js`, () => ({ + setupTemplate: vi.fn(), +})) + +// Mock child_process execSync +vi.mock(`child_process`, () => ({ + execSync: vi.fn(), +})) + +describe(`cli`, () => { + const mockCredentials = { + source_id: `test-source-id`, + secret: `test-secret`, + DATABASE_URL: `postgresql://test:test@localhost:5432/test`, + claimId: `test-claim-id`, + } + + let mockProvisionElectricResources: ReturnType + let mockSetupTemplate: ReturnType + let originalArgv: string[] + let consoleLogSpy: ReturnType + let consoleErrorSpy: ReturnType + let processExitSpy: ReturnType + + beforeEach(async () => { + vi.clearAllMocks() + + // Store original argv + originalArgv = [...process.argv] + + // Spy on console methods + consoleLogSpy = vi.spyOn(console, `log`).mockImplementation(() => {}) + consoleErrorSpy = vi.spyOn(console, `error`).mockImplementation(() => {}) + + // Mock process.exit to throw instead of exiting + processExitSpy = vi + .spyOn(process, `exit`) + .mockImplementation((code?: string | number | null | undefined) => { + throw new Error(`process.exit(${code})`) + }) + + // Get mocked functions + const electricApi = await import(`../src/electric-api.js`) + const templateSetup = await import(`../src/template-setup.js`) + + mockProvisionElectricResources = + electricApi.provisionElectricResources as unknown as ReturnType< + typeof vi.fn + > + mockSetupTemplate = templateSetup.setupTemplate as unknown as ReturnType< + typeof vi.fn + > + }) + + afterEach(() => { + // Restore original argv + process.argv = originalArgv + + // Restore spies + consoleLogSpy.mockRestore() + consoleErrorSpy.mockRestore() + processExitSpy.mockRestore() + + vi.resetAllMocks() + }) + + describe(`main`, () => { + it(`should exit with error when no app name provided`, async () => { + process.argv = [`node`, `cli.js`] + + const { main } = await import(`../src/cli.js`) + + await expect(main()).rejects.toThrow(`process.exit(1)`) + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Usage: npx @electric-sql/start ` + ) + }) + + it(`should exit with error for invalid app name`, async () => { + process.argv = [`node`, `cli.js`, `my app with spaces`] + + const { main } = await import(`../src/cli.js`) + + await expect(main()).rejects.toThrow(`process.exit(1)`) + expect(consoleErrorSpy).toHaveBeenCalledWith( + `App name must contain only letters, numbers, hyphens, and underscores` + ) + }) + + it(`should exit with error for app name with special characters`, async () => { + process.argv = [`node`, `cli.js`, `my@app!`] + + const { main } = await import(`../src/cli.js`) + + await expect(main()).rejects.toThrow(`process.exit(1)`) + expect(consoleErrorSpy).toHaveBeenCalledWith( + `App name must contain only letters, numbers, hyphens, and underscores` + ) + }) + + it(`should provision resources and setup template for valid app name`, async () => { + process.argv = [`node`, `cli.js`, `my-valid-app`] + mockProvisionElectricResources.mockResolvedValue(mockCredentials) + mockSetupTemplate.mockResolvedValue(undefined) + + const { main } = await import(`../src/cli.js`) + + await main() + + expect(mockProvisionElectricResources).toHaveBeenCalledTimes(1) + expect(mockSetupTemplate).toHaveBeenCalledWith( + `my-valid-app`, + mockCredentials + ) + expect(consoleLogSpy).toHaveBeenCalledWith(`Creating app: my-valid-app`) + expect(consoleLogSpy).toHaveBeenCalledWith(`Setup complete`) + }) + + it(`should handle provisioning errors`, async () => { + process.argv = [`node`, `cli.js`, `my-app`] + mockProvisionElectricResources.mockRejectedValue( + new Error(`API connection failed`) + ) + + const { main } = await import(`../src/cli.js`) + + await expect(main()).rejects.toThrow(`process.exit(1)`) + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Setup failed:`, + `API connection failed` + ) + expect(mockSetupTemplate).not.toHaveBeenCalled() + }) + + it(`should handle template setup errors`, async () => { + process.argv = [`node`, `cli.js`, `my-app`] + mockProvisionElectricResources.mockResolvedValue(mockCredentials) + mockSetupTemplate.mockRejectedValue(new Error(`Template download failed`)) + + const { main } = await import(`../src/cli.js`) + + await expect(main()).rejects.toThrow(`process.exit(1)`) + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Setup failed:`, + `Template download failed` + ) + }) + + it(`should display next steps after successful setup`, async () => { + process.argv = [`node`, `cli.js`, `my-app`] + mockProvisionElectricResources.mockResolvedValue(mockCredentials) + mockSetupTemplate.mockResolvedValue(undefined) + + const { main } = await import(`../src/cli.js`) + + await main() + + expect(consoleLogSpy).toHaveBeenCalledWith(`Next steps:`) + expect(consoleLogSpy).toHaveBeenCalledWith(` cd my-app`) + // pnpm install and pnpm migrate are no longer shown because they run automatically + expect(consoleLogSpy).not.toHaveBeenCalledWith(` pnpm install`) + expect(consoleLogSpy).not.toHaveBeenCalledWith(` pnpm migrate`) + expect(consoleLogSpy).toHaveBeenCalledWith(` pnpm dev`) + }) + + it(`should display available commands after successful setup`, async () => { + process.argv = [`node`, `cli.js`, `my-app`] + mockProvisionElectricResources.mockResolvedValue(mockCredentials) + mockSetupTemplate.mockResolvedValue(undefined) + + const { main } = await import(`../src/cli.js`) + + await main() + + expect(consoleLogSpy).toHaveBeenCalledWith(`Commands:`) + expect(consoleLogSpy).toHaveBeenCalledWith( + ` pnpm psql # Connect to database` + ) + expect(consoleLogSpy).toHaveBeenCalledWith( + ` pnpm claim # Claim cloud resources` + ) + expect(consoleLogSpy).toHaveBeenCalledWith( + ` pnpm deploy:netlify # Deploy to Netlify` + ) + }) + + it(`should accept app names with underscores`, async () => { + process.argv = [`node`, `cli.js`, `my_valid_app`] + mockProvisionElectricResources.mockResolvedValue(mockCredentials) + mockSetupTemplate.mockResolvedValue(undefined) + + const { main } = await import(`../src/cli.js`) + + await main() + + expect(mockSetupTemplate).toHaveBeenCalledWith( + `my_valid_app`, + mockCredentials + ) + }) + + it(`should accept app names with numbers`, async () => { + process.argv = [`node`, `cli.js`, `my-app-123`] + mockProvisionElectricResources.mockResolvedValue(mockCredentials) + mockSetupTemplate.mockResolvedValue(undefined) + + const { main } = await import(`../src/cli.js`) + + await main() + + expect(mockSetupTemplate).toHaveBeenCalledWith( + `my-app-123`, + mockCredentials + ) + }) + + it(`should accept "." as app name for current directory mode`, async () => { + process.argv = [`node`, `cli.js`, `.`] + mockProvisionElectricResources.mockResolvedValue(mockCredentials) + mockSetupTemplate.mockResolvedValue(undefined) + + const { main } = await import(`../src/cli.js`) + + await main() + + expect(mockProvisionElectricResources).toHaveBeenCalledTimes(1) + expect(mockSetupTemplate).toHaveBeenCalledWith(`.`, mockCredentials) + expect(consoleLogSpy).toHaveBeenCalledWith( + `Configuring current directory...` + ) + expect(consoleLogSpy).toHaveBeenCalledWith(`Setup complete`) + }) + + it(`should not display "cd" instruction when using "."`, async () => { + process.argv = [`node`, `cli.js`, `.`] + mockProvisionElectricResources.mockResolvedValue(mockCredentials) + mockSetupTemplate.mockResolvedValue(undefined) + + const { main } = await import(`../src/cli.js`) + + await main() + + expect(consoleLogSpy).not.toHaveBeenCalledWith(` cd .`) + // pnpm install runs automatically, so it's not shown in next steps + expect(consoleLogSpy).toHaveBeenCalledWith(` pnpm dev`) + }) + + it(`should handle non-Error thrown values`, async () => { + process.argv = [`node`, `cli.js`, `my-app`] + mockProvisionElectricResources.mockRejectedValue(`string error`) + + const { main } = await import(`../src/cli.js`) + + await expect(main()).rejects.toThrow(`process.exit(1)`) + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Setup failed:`, + `string error` + ) + }) + }) +}) diff --git a/packages/start/test/electric-api.test.ts b/packages/start/test/electric-api.test.ts new file mode 100644 index 0000000000..6ed2100915 --- /dev/null +++ b/packages/start/test/electric-api.test.ts @@ -0,0 +1,306 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { + provisionElectricResources, + claimResources, +} from '../src/electric-api.js' + +describe(`electric-api`, () => { + let mockFetch: ReturnType + + beforeEach(() => { + mockFetch = vi.fn() + vi.stubGlobal(`fetch`, mockFetch) + vi.clearAllMocks() + }) + + afterEach(() => { + vi.unstubAllGlobals() + vi.resetAllMocks() + }) + + // Helper to extract path from URL for assertions + function getUrlPath(url: string | URL | Request): string { + const urlString = url instanceof Request ? url.url : url.toString() + return new URL(urlString).pathname + } + + describe(`provisionElectricResources`, () => { + it(`should successfully provision resources`, async () => { + const testClaimId = `test-claim-id` + const mockCredentials = { + source_id: `test-source-id`, + secret: `test-secret`, + DATABASE_URL: `postgresql://test:test@localhost:5432/test`, + claimId: testClaimId, + } + + // Mock the two-step process: POST to create, then GET to poll + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ claimId: testClaimId }), + status: 200, + statusText: `OK`, + }) + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + state: `ready`, + source: { + source_id: mockCredentials.source_id, + secret: mockCredentials.secret, + }, + connection_uri: mockCredentials.DATABASE_URL, + }), + status: 200, + statusText: `OK`, + }) + + const result = await provisionElectricResources() + + // Verify first call (POST to create claimable source) + expect(mockFetch).toHaveBeenCalledTimes(2) + const firstCall = mockFetch.mock.calls[0] + expect(getUrlPath(firstCall[0])).toMatch( + /\/public\/v1\/claimable-sources$/ + ) + expect(firstCall[1]).toEqual({ + method: `POST`, + headers: { + 'Content-Type': `application/json`, + 'User-Agent': `@electric-sql/start`, + }, + body: JSON.stringify({}), + }) + + // Verify second call (GET to poll status) + const secondCall = mockFetch.mock.calls[1] + expect(getUrlPath(secondCall[0])).toMatch( + new RegExp(`/public/v1/claimable-sources/${testClaimId}$`) + ) + expect(secondCall[1]).toEqual({ + method: `GET`, + headers: { + 'User-Agent': `@electric-sql/start`, + }, + }) + + expect(result).toEqual(mockCredentials) + }) + + it(`should handle API errors`, async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 500, + statusText: `Internal Server Error`, + }) + + await expect(provisionElectricResources()).rejects.toThrow( + `Failed to provision Electric resources: Electric API error: 500 Internal Server Error` + ) + }) + + it(`should handle missing claimId in response`, async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: () => Promise.resolve({}), // Missing claimId + status: 200, + statusText: `OK`, + }) + + await expect(provisionElectricResources()).rejects.toThrow( + `Failed to provision Electric resources: Invalid response from Electric API - missing claimId` + ) + }) + + it(`should handle missing credentials in ready response`, async () => { + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ claimId: `test-claim-id` }), + status: 200, + statusText: `OK`, + }) + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + state: `ready`, + source: { source_id: `test` }, // Missing secret + // Missing connection_uri + }), + status: 200, + statusText: `OK`, + }) + + await expect(provisionElectricResources()).rejects.toThrow( + `Failed to provision Electric resources: Invalid response from Electric API - missing required credentials` + ) + }) + + it(`should handle network errors`, async () => { + mockFetch.mockRejectedValue(new Error(`Network error`)) + + await expect(provisionElectricResources()).rejects.toThrow( + `Failed to provision Electric resources: Network error` + ) + }) + + it(`should handle unknown errors`, async () => { + mockFetch.mockRejectedValue(`Unknown error`) + + await expect(provisionElectricResources()).rejects.toThrow( + `Failed to provision Electric resources: Unknown error` + ) + }) + + it(`should poll until ready state`, async () => { + const testClaimId = `test-claim-id` + const mockCredentials = { + source_id: `test-source-id`, + secret: `test-secret`, + DATABASE_URL: `postgresql://test:test@localhost:5432/test`, + claimId: testClaimId, + } + + mockFetch + // Initial POST + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ claimId: testClaimId }), + status: 200, + statusText: `OK`, + }) + // First poll - pending + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ state: `pending` }), + status: 200, + statusText: `OK`, + }) + // Second poll - ready + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + state: `ready`, + source: { + source_id: mockCredentials.source_id, + secret: mockCredentials.secret, + }, + connection_uri: mockCredentials.DATABASE_URL, + }), + status: 200, + statusText: `OK`, + }) + + const result = await provisionElectricResources() + + expect(mockFetch).toHaveBeenCalledTimes(3) + expect(result).toEqual(mockCredentials) + }) + + it(`should handle failed provisioning state`, async () => { + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ claimId: `test-claim-id` }), + status: 200, + statusText: `OK`, + }) + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + state: `failed`, + error: `Database provisioning failed`, + }), + status: 200, + statusText: `OK`, + }) + + await expect(provisionElectricResources()).rejects.toThrow( + `Failed to provision Electric resources: Resource provisioning failed: Database provisioning failed` + ) + }) + }) + + describe(`claimResources`, () => { + const testSourceId = `test-source-id` + const testSecret = `test-secret` + + it(`should successfully claim resources`, async () => { + const mockClaimResponse = { + claimUrl: `https://electric-sql.com/claim/test-claim-url`, + } + + mockFetch.mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockClaimResponse), + status: 200, + statusText: `OK`, + }) + + const result = await claimResources(testSourceId, testSecret) + + expect(mockFetch).toHaveBeenCalledTimes(1) + const call = mockFetch.mock.calls[0] + expect(getUrlPath(call[0])).toMatch(/\/v1\/claim$/) + expect(call[1]).toEqual({ + method: `POST`, + headers: { + 'Content-Type': `application/json`, + Authorization: `Bearer ${testSecret}`, + 'User-Agent': `@electric-sql/start`, + }, + body: JSON.stringify({ + source_id: testSourceId, + }), + }) + + expect(result).toEqual(mockClaimResponse) + }) + + it(`should handle API errors during claim`, async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 401, + statusText: `Unauthorized`, + }) + + await expect(claimResources(testSourceId, testSecret)).rejects.toThrow( + `Failed to initiate resource claim: Electric API error: 401 Unauthorized` + ) + }) + + it(`should handle missing claim URL in response`, async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: () => Promise.resolve({}), // Missing claimUrl + status: 200, + statusText: `OK`, + }) + + await expect(claimResources(testSourceId, testSecret)).rejects.toThrow( + `Failed to initiate resource claim: Invalid response from Electric API - missing claim URL` + ) + }) + + it(`should handle network errors during claim`, async () => { + mockFetch.mockRejectedValue(new Error(`Connection timeout`)) + + await expect(claimResources(testSourceId, testSecret)).rejects.toThrow( + `Failed to initiate resource claim: Connection timeout` + ) + }) + + it(`should handle unknown errors during claim`, async () => { + mockFetch.mockRejectedValue(`Unknown claim error`) + + await expect(claimResources(testSourceId, testSecret)).rejects.toThrow( + `Failed to initiate resource claim: Unknown error` + ) + }) + }) +}) diff --git a/packages/start/test/simple-cli.test.ts b/packages/start/test/simple-cli.test.ts new file mode 100644 index 0000000000..e6b2b421dd --- /dev/null +++ b/packages/start/test/simple-cli.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'vitest' + +describe(`cli (unit)`, () => { + it(`should export main function`, async () => { + const { main } = await import(`../src/cli.js`) + expect(typeof main).toBe(`function`) + }) + + it(`should validate app name regex`, () => { + const validNames = [`my-app`, `my_app`, `myapp123`, `My-App_123`] + const invalidNames = [ + `my app`, + `my@app`, + `my.app`, + `my/app`, + `my\\app`, + `my app!`, + ] + + validNames.forEach((name) => { + expect(/^[a-zA-Z0-9-_]+$/.test(name)).toBe(true) + }) + + invalidNames.forEach((name) => { + expect(/^[a-zA-Z0-9-_]+$/.test(name)).toBe(false) + }) + }) + + it(`should handle process.argv correctly`, () => { + const testArgv = [`node`, `cli.js`, `test-app`] + expect(testArgv.slice(2)).toEqual([`test-app`]) + expect(testArgv.slice(2)[0]).toBe(`test-app`) + }) +}) diff --git a/packages/start/test/simple-template-setup.test.ts b/packages/start/test/simple-template-setup.test.ts new file mode 100644 index 0000000000..a22e6d5eab --- /dev/null +++ b/packages/start/test/simple-template-setup.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from 'vitest' +import type { ElectricCredentials } from '../src/electric-api.js' + +// Simple test for basic functionality +describe(`template-setup (unit)`, () => { + const mockCredentials: ElectricCredentials = { + source_id: `test-source-id`, + secret: `test-secret`, + DATABASE_URL: `postgresql://test:test@localhost:5432/test`, + } + + it(`should export setupTemplate function`, async () => { + const { setupTemplate } = await import(`../src/template-setup.js`) + expect(typeof setupTemplate).toBe(`function`) + }) + + it(`should validate credentials structure`, () => { + expect(mockCredentials).toHaveProperty(`source_id`) + expect(mockCredentials).toHaveProperty(`secret`) + expect(mockCredentials).toHaveProperty(`DATABASE_URL`) + expect(mockCredentials.source_id).toBe(`test-source-id`) + expect(mockCredentials.secret).toBe(`test-secret`) + expect(mockCredentials.DATABASE_URL).toContain(`postgresql://`) + }) + + it(`should validate app name format`, () => { + const validNames = [`my-app`, `my_app`, `myapp123`, `My-App_123`] + const invalidNames = [`my app`, `my@app`, `my.app`, `my/app`] + + validNames.forEach((name) => { + expect(/^[a-zA-Z0-9-_]+$/.test(name)).toBe(true) + }) + + invalidNames.forEach((name) => { + expect(/^[a-zA-Z0-9-_]+$/.test(name)).toBe(false) + }) + }) +}) diff --git a/packages/start/test/support/test-utils.ts b/packages/start/test/support/test-utils.ts new file mode 100644 index 0000000000..867787e611 --- /dev/null +++ b/packages/start/test/support/test-utils.ts @@ -0,0 +1,120 @@ +import { vi } from 'vitest' +import { spawn } from 'child_process' +import { mkdtempSync, rmSync, existsSync } from 'fs' +import { join } from 'path' +import { tmpdir } from 'os' + +/** + * Creates a temporary directory for testing + */ +export function createTempDir(): string { + return mkdtempSync(join(tmpdir(), `quickstart-test-`)) +} + +/** + * Cleans up a temporary directory + */ +export function cleanupTempDir(dir: string): void { + if (existsSync(dir)) { + rmSync(dir, { recursive: true, force: true }) + } +} + +/** + * Mock fetch responses for Electric API + */ +export function mockElectricApiResponses() { + const mockFetch = vi.fn() + + // Mock successful provision response + const mockProvisionSuccess = { + source_id: `test-source-id`, + secret: `test-secret`, + DATABASE_URL: `postgresql://test:test@localhost:5432/test`, + } + + // Mock successful claim response + const mockClaimSuccess = { + claimUrl: `https://electric-sql.com/claim/test-claim-url`, + } + + mockFetch.mockImplementation((url: string) => { + if (url.includes(`/v1/provision`)) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockProvisionSuccess), + status: 200, + statusText: `OK`, + }) + } + + if (url.includes(`/v1/claim`)) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockClaimSuccess), + status: 200, + statusText: `OK`, + }) + } + + return Promise.resolve({ + ok: false, + status: 404, + statusText: `Not Found`, + }) + }) + + return { + mockFetch, + mockProvisionSuccess, + mockClaimSuccess, + } +} + +/** + * Mock execSync for testing CLI commands + */ +export function mockExecSync() { + return vi.fn().mockImplementation((command: string) => { + if (command.includes(`gitpick`)) { + // Simulate successful gitpick execution + return Buffer.from(`Template downloaded successfully`) + } + return Buffer.from(`Command executed`) + }) +} + +/** + * Execute CLI command for testing + */ +export function execCli(args: string[]): Promise<{ + stdout: string + stderr: string + exitCode: number | null +}> { + return new Promise((resolve) => { + const cliPath = join(__dirname, `../../dist/cli.js`) + const child = spawn(`node`, [cliPath, ...args], { + stdio: [`pipe`, `pipe`, `pipe`], + }) + + let stdout = `` + let stderr = `` + + child.stdout?.on(`data`, (data) => { + stdout += data.toString() + }) + + child.stderr?.on(`data`, (data) => { + stderr += data.toString() + }) + + child.on(`close`, (code) => { + resolve({ + stdout, + stderr, + exitCode: code, + }) + }) + }) +} diff --git a/packages/start/test/template-setup.test.ts b/packages/start/test/template-setup.test.ts new file mode 100644 index 0000000000..3e62c70cd8 --- /dev/null +++ b/packages/start/test/template-setup.test.ts @@ -0,0 +1,293 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +// Mock child_process +vi.mock(`child_process`, () => ({ + execSync: vi.fn(), +})) + +// Mock crypto +vi.mock(`crypto`, () => ({ + randomBytes: vi.fn(() => ({ + toString: () => `mock-random-secret-0123456789abcdef0123456789abcdef`, + })), +})) + +// Mock fs +vi.mock(`fs`, () => ({ + writeFileSync: vi.fn(), + readFileSync: vi.fn(), + existsSync: vi.fn(), +})) + +describe(`template-setup`, () => { + const mockCredentials = { + source_id: `test-source-id`, + secret: `test-secret`, + DATABASE_URL: `postgresql://test:test@localhost:5432/test`, + claimId: `test-claim-id`, + } + + let mockExecSync: ReturnType + let mockWriteFileSync: ReturnType + let mockReadFileSync: ReturnType + let mockExistsSync: ReturnType + + beforeEach(async () => { + vi.clearAllMocks() + + const childProcess = await import(`child_process`) + const fs = await import(`fs`) + + mockExecSync = childProcess.execSync as unknown as ReturnType + mockWriteFileSync = fs.writeFileSync as unknown as ReturnType + mockReadFileSync = fs.readFileSync as unknown as ReturnType + mockExistsSync = fs.existsSync as unknown as ReturnType + }) + + afterEach(() => { + vi.resetAllMocks() + }) + + describe(`setupTemplate`, () => { + it(`should pull template using gitpick`, async () => { + mockExistsSync.mockReturnValue(true) + mockReadFileSync.mockReturnValue(`{}`) + + const { setupTemplate } = await import(`../src/template-setup.js`) + await setupTemplate(`my-app`, mockCredentials) + + expect(mockExecSync).toHaveBeenCalledWith( + `npx gitpick electric-sql/electric/tree/main/examples/tanstack-db-web-starter my-app`, + { stdio: `inherit` } + ) + }) + + it(`should generate .env file with credentials`, async () => { + mockExistsSync.mockReturnValue(true) + mockReadFileSync.mockReturnValue(`{}`) + + const { setupTemplate } = await import(`../src/template-setup.js`) + await setupTemplate(`my-app`, mockCredentials) + + // Find the .env write call + const envWriteCall = mockWriteFileSync.mock.calls.find( + (call: unknown[]) => (call[0] as string).endsWith(`.env`) + ) + + expect(envWriteCall).toBeDefined() + const envContent = envWriteCall![1] as string + expect(envContent).toContain( + `DATABASE_URL=${mockCredentials.DATABASE_URL}` + ) + expect(envContent).toContain(`ELECTRIC_SECRET=${mockCredentials.secret}`) + expect(envContent).toContain( + `ELECTRIC_SOURCE_ID=${mockCredentials.source_id}` + ) + expect(envContent).toMatch(/ELECTRIC_URL=https?:\/\//) + expect(envContent).toContain(`BETTER_AUTH_SECRET=`) + expect(envContent).toContain(`DO NOT COMMIT THIS FILE`) + }) + + it(`should update .gitignore to include .env`, async () => { + mockExistsSync.mockImplementation((path: string) => { + if (path.endsWith(`.gitignore`)) return true + if (path.endsWith(`package.json`)) return true + if (path.endsWith(`tsconfig.json`)) return true + return false + }) + mockReadFileSync.mockImplementation((path: string) => { + if (path.endsWith(`.gitignore`)) return `node_modules\n` + if (path.endsWith(`package.json`)) return `{"scripts":{}}` + return `` + }) + + const { setupTemplate } = await import(`../src/template-setup.js`) + await setupTemplate(`my-app`, mockCredentials) + + const gitignoreWriteCall = mockWriteFileSync.mock.calls.find( + (call: unknown[]) => (call[0] as string).endsWith(`.gitignore`) + ) + + expect(gitignoreWriteCall).toBeDefined() + const gitignoreContent = gitignoreWriteCall![1] as string + expect(gitignoreContent).toContain(`.env`) + }) + + it(`should not duplicate .env in .gitignore if already present`, async () => { + mockExistsSync.mockReturnValue(true) + mockReadFileSync.mockImplementation((path: string) => { + if (path.endsWith(`.gitignore`)) return `node_modules\n.env\n` + if (path.endsWith(`package.json`)) return `{"scripts":{}}` + return `` + }) + + const { setupTemplate } = await import(`../src/template-setup.js`) + await setupTemplate(`my-app`, mockCredentials) + + const gitignoreWriteCall = mockWriteFileSync.mock.calls.find( + (call: unknown[]) => (call[0] as string).endsWith(`.gitignore`) + ) + + // Should not write to .gitignore since .env is already present + expect(gitignoreWriteCall).toBeUndefined() + }) + + it(`should patch package.json with Electric scripts`, async () => { + mockExistsSync.mockReturnValue(true) + mockReadFileSync.mockImplementation((path: string) => { + if (path.endsWith(`package.json`)) { + return JSON.stringify({ + name: `my-app`, + scripts: { + dev: `vinxi dev`, + build: `vinxi build`, + }, + }) + } + if (path.endsWith(`.gitignore`)) return `.env\n` + return `` + }) + + const { setupTemplate } = await import(`../src/template-setup.js`) + await setupTemplate(`my-app`, mockCredentials) + + const packageJsonWriteCall = mockWriteFileSync.mock.calls.find( + (call: unknown[]) => (call[0] as string).endsWith(`package.json`) + ) + + expect(packageJsonWriteCall).toBeDefined() + const packageJson = JSON.parse(packageJsonWriteCall![1] as string) + + // Electric-specific commands + expect(packageJson.scripts).toHaveProperty(`claim`) + expect(packageJson.scripts).toHaveProperty(`deploy:netlify`) + + expect(packageJson.scripts.build).toBe(`vinxi build`) + }) + + it(`should not overwrite existing tsconfig.json`, async () => { + mockExistsSync.mockReturnValue(true) + mockReadFileSync.mockImplementation((path: string) => { + if (path.endsWith(`package.json`)) return `{"scripts":{}}` + if (path.endsWith(`.gitignore`)) return `.env\n` + return `` + }) + + const { setupTemplate } = await import(`../src/template-setup.js`) + await setupTemplate(`my-app`, mockCredentials) + + const tsconfigWriteCall = mockWriteFileSync.mock.calls.find( + (call: unknown[]) => (call[0] as string).endsWith(`tsconfig.json`) + ) + + // Should not write tsconfig if it exists + expect(tsconfigWriteCall).toBeUndefined() + }) + + it(`should throw error when gitpick fails`, async () => { + mockExecSync.mockImplementation(() => { + throw new Error(`gitpick failed`) + }) + + const { setupTemplate } = await import(`../src/template-setup.js`) + + await expect(setupTemplate(`my-app`, mockCredentials)).rejects.toThrow( + `Template setup failed: gitpick failed` + ) + }) + + it(`should handle missing package.json gracefully`, async () => { + mockExistsSync.mockImplementation((path: string) => { + if (path.endsWith(`package.json`)) return false + if (path.endsWith(`.gitignore`)) return true + if (path.endsWith(`tsconfig.json`)) return true + return false + }) + mockReadFileSync.mockImplementation((path: string) => { + if (path.endsWith(`.gitignore`)) return `.env\n` + return `` + }) + + const { setupTemplate } = await import(`../src/template-setup.js`) + + // Should not throw - just skip package.json patching + await expect( + setupTemplate(`my-app`, mockCredentials) + ).resolves.toBeUndefined() + + // Verify package.json was not written + const packageJsonWriteCall = mockWriteFileSync.mock.calls.find( + (call: unknown[]) => (call[0] as string).endsWith(`package.json`) + ) + expect(packageJsonWriteCall).toBeUndefined() + }) + + it(`should skip gitpick when appName is "."`, async () => { + mockExistsSync.mockReturnValue(true) + mockReadFileSync.mockImplementation((path: string) => { + if (path.endsWith(`package.json`)) return `{"scripts":{}}` + if (path.endsWith(`.gitignore`)) return `.env\n` + return `` + }) + + const { setupTemplate } = await import(`../src/template-setup.js`) + await setupTemplate(`.`, mockCredentials) + + // gitpick should NOT be called + expect(mockExecSync).not.toHaveBeenCalled() + }) + + it(`should use current directory when appName is "."`, async () => { + mockExistsSync.mockReturnValue(true) + mockReadFileSync.mockImplementation((path: string) => { + if (path.endsWith(`package.json`)) return `{"scripts":{}}` + if (path.endsWith(`.gitignore`)) return `.env\n` + return `` + }) + + const { setupTemplate } = await import(`../src/template-setup.js`) + await setupTemplate(`.`, mockCredentials) + + // .env should be written to current directory (not a subdirectory) + const envWriteCall = mockWriteFileSync.mock.calls.find( + (call: unknown[]) => (call[0] as string).endsWith(`.env`) + ) + expect(envWriteCall).toBeDefined() + // Path should be process.cwd()/.env, not process.cwd()/./.env + expect(envWriteCall![0]).not.toContain(`/./.env`) + }) + + it(`should still generate .env and patch package.json when appName is "."`, async () => { + mockExistsSync.mockReturnValue(true) + mockReadFileSync.mockImplementation((path: string) => { + if (path.endsWith(`package.json`)) { + return JSON.stringify({ + name: `existing-app`, + scripts: { dev: `vite dev` }, + }) + } + if (path.endsWith(`.gitignore`)) return `.env\n` + return `` + }) + + const { setupTemplate } = await import(`../src/template-setup.js`) + await setupTemplate(`.`, mockCredentials) + + // .env should be generated + const envWriteCall = mockWriteFileSync.mock.calls.find( + (call: unknown[]) => (call[0] as string).endsWith(`.env`) + ) + expect(envWriteCall).toBeDefined() + expect(envWriteCall![1]).toContain(`DATABASE_URL=`) + + // package.json should be patched + const packageJsonWriteCall = mockWriteFileSync.mock.calls.find( + (call: unknown[]) => (call[0] as string).endsWith(`package.json`) + ) + expect(packageJsonWriteCall).toBeDefined() + const packageJson = JSON.parse(packageJsonWriteCall![1] as string) + expect(packageJson.scripts).toHaveProperty(`claim`) + expect(packageJson.scripts).toHaveProperty(`deploy:netlify`) + }) + }) +}) diff --git a/packages/start/tsconfig.build.json b/packages/start/tsconfig.build.json new file mode 100644 index 0000000000..eab6af53ea --- /dev/null +++ b/packages/start/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*"], + "exclude": ["node_modules", "tests", "dist"], + "compilerOptions": { + "target": "es2020", + "lib": ["ESNext"], + "module": "esnext", + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noEmit": false + } +} diff --git a/packages/start/tsconfig.json b/packages/start/tsconfig.json new file mode 100644 index 0000000000..bbcd2307be --- /dev/null +++ b/packages/start/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "es2020", + "lib": ["ESNext"], + "module": "esnext", + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/start/tsup.config.ts b/packages/start/tsup.config.ts new file mode 100644 index 0000000000..2c50ddbce3 --- /dev/null +++ b/packages/start/tsup.config.ts @@ -0,0 +1,24 @@ +import type { Options } from 'tsup' +import { defineConfig } from 'tsup' + +export default defineConfig((options) => { + const commonOptions: Partial = { + entry: { + index: `src/index.ts`, + cli: `src/cli.ts`, + }, + tsconfig: `./tsconfig.build.json`, + sourcemap: true, + ...options, + } + + return [ + // ESM build + { + ...commonOptions, + format: [`esm`], + outExtension: () => ({ js: `.js` }), + clean: true, + }, + ] +}) diff --git a/packages/start/vitest.config.ts b/packages/start/vitest.config.ts new file mode 100644 index 0000000000..3eaa911d43 --- /dev/null +++ b/packages/start/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { enabled: true }, + coverage: { + provider: `istanbul`, + reporter: [`text`, `json`, `html`, `lcov`], + include: [`**/src/**`], + }, + reporters: [`default`, `junit`], + outputFile: `./junit/test-report.junit.xml`, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad66c08cd1..31e608b51f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: patchedDependencies: '@microsoft/fetch-event-source': - hash: lgwcujj3mimdfutlwueisfm32u + hash: 46f4e76dd960e002a542732bb4323817a24fce1673cb71e2f458fe09776fa188 path: patches/@microsoft__fetch-event-source.patch importers: @@ -811,10 +811,6 @@ importers: redis: specifier: ^4.6.14 version: 4.7.0 - optionalDependencies: - '@rollup/rollup-darwin-arm64': - specifier: ^4.18.1 - version: 4.24.4 devDependencies: '@databases/pg-migrations': specifier: ^5.0.3 @@ -858,6 +854,10 @@ importers: typescript: specifier: ^5.5.2 version: 5.6.3 + optionalDependencies: + '@rollup/rollup-darwin-arm64': + specifier: ^4.18.1 + version: 4.24.4 examples/remix: dependencies: @@ -1048,7 +1048,7 @@ importers: version: 17.1.7(expo@53.0.20(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.80.1(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0)))(react-native@0.80.1(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.80.1(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0)) expo-router: specifier: ~5.1.4 - version: 5.1.4(y4jtnr2g2s4ue3hjudtn7mmiri) + version: 5.1.4(4385c7d2ebe6e555b6505e0ac211d063) expo-status-bar: specifier: ~2.2.0 version: 2.2.3(react-native@0.80.1(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) @@ -1171,6 +1171,9 @@ importers: specifier: ^4.0.14 version: 4.1.11 devDependencies: + '@dotenvx/dotenvx': + specifier: ^1.51.2 + version: 1.51.2 '@eslint/compat': specifier: ^1.3.1 version: 1.3.1(eslint@9.32.0(jiti@2.6.1)) @@ -1207,9 +1210,6 @@ importers: '@vitejs/plugin-react': specifier: ^5.0.4 version: 5.1.1(vite@7.1.7(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) - dotenv: - specifier: ^17.2.1 - version: 17.2.1 eslint: specifier: ^9.32.0 version: 9.32.0(jiti@2.6.1) @@ -1225,6 +1225,9 @@ importers: jsdom: specifier: ^27.0.0 version: 27.2.0 + open-cli: + specifier: ^8.0.0 + version: 8.0.0 prettier: specifier: ^3.6.2 version: 3.6.2 @@ -1522,10 +1525,6 @@ importers: packages/elixir-client: {} packages/experimental: - optionalDependencies: - '@rollup/rollup-darwin-arm64': - specifier: ^4.18.1 - version: 4.24.4 devDependencies: '@electric-sql/client': specifier: workspace:* @@ -1581,6 +1580,10 @@ importers: vitest: specifier: ^4.0.15 version: 4.0.15(@types/node@22.19.1)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) + optionalDependencies: + '@rollup/rollup-darwin-arm64': + specifier: ^4.18.1 + version: 4.24.4 packages/react-hooks: dependencies: @@ -1661,17 +1664,52 @@ importers: specifier: ^4.0.15 version: 4.0.15(@types/node@22.19.1)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) + packages/start: + devDependencies: + '@types/node': + specifier: ^20.10.0 + version: 20.17.6 + '@typescript-eslint/eslint-plugin': + specifier: ^7.14.1 + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^7.14.1 + version: 7.18.0(eslint@8.57.1)(typescript@5.8.3) + '@vitest/coverage-istanbul': + specifier: 4.0.15 + version: 4.0.15(vitest@4.0.15(@types/node@20.17.6)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) + eslint: + specifier: ^8.57.0 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-prettier: + specifier: ^5.1.3 + version: 5.5.3(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2) + prettier: + specifier: ^3.3.2 + version: 3.6.2 + shx: + specifier: ^0.3.4 + version: 0.3.4 + tsup: + specifier: ^8.0.1 + version: 8.3.5(@swc/core@1.9.1(@swc/helpers@0.5.5))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.1) + typescript: + specifier: ^5.5.2 + version: 5.8.3 + vitest: + specifier: ^4.0.15 + version: 4.0.15(@types/node@20.17.6)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) + packages/sync-service: {} packages/typescript-client: dependencies: '@microsoft/fetch-event-source': specifier: ^2.0.1 - version: 2.0.1(patch_hash=lgwcujj3mimdfutlwueisfm32u) - optionalDependencies: - '@rollup/rollup-darwin-arm64': - specifier: ^4.18.1 - version: 4.24.4 + version: 2.0.1(patch_hash=46f4e76dd960e002a542732bb4323817a24fce1673cb71e2f458fe09776fa188) devDependencies: '@types/pg': specifier: ^8.11.6 @@ -1733,6 +1771,10 @@ importers: vitest-localstorage-mock: specifier: ^0.1.2 version: 0.1.2(vitest@4.0.15(@types/node@22.19.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) + optionalDependencies: + '@rollup/rollup-darwin-arm64': + specifier: ^4.18.1 + version: 4.24.4 packages/y-electric: dependencies: @@ -3268,9 +3310,19 @@ packages: search-insights: optional: true + '@dotenvx/dotenvx@1.51.2': + resolution: {integrity: sha512-+693mNflujDZxudSEqSNGpn92QgFhJlBn9q2mDQ9yGWyHuz3hZ8B5g3EXCwdAz4DMJAI+OFCIbfEFZS+YRdrEA==} + hasBin: true + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@ecies/ciphers@0.2.5': + resolution: {integrity: sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + peerDependencies: + '@noble/ciphers': ^1.0.0 + '@electric-sql/client@1.0.0-beta.3': resolution: {integrity: sha512-x3bzYlX+IRwBAILPxzu3ARkXzmrAQtVOuJCKCxlSqENuJa4zvLPF4f8vC6HMOiiJiHPAntJjfI3Hb0lrt2PTxA==} @@ -4791,10 +4843,22 @@ packages: cpu: [x64] os: [win32] + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + '@noble/ciphers@2.0.1': resolution: {integrity: sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g==} engines: {node: '>= 20.19.0'} + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@noble/hashes@2.0.1': resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} engines: {node: '>= 20.19.0'} @@ -6858,6 +6922,9 @@ packages: '@tiptap/starter-kit@2.9.1': resolution: {integrity: sha512-nsw6UF/7wDpPfHRhtGOwkj1ipIEiWZS1VGw+c14K61vM1CNj0uQ4jogbHwHZqN1dlL5Hh+FCqUHDPxG6ECbijg==} + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@trpc/client@11.7.2': resolution: {integrity: sha512-OQxqUMfpDvjcszo9dbnqWQXnW2L5IbrKSz2H7l8s+mVM3EvYw7ztQ/gjFIN3iy0NcamiQfd4eE6qjcb9Lm+63A==} peerDependencies: @@ -8021,6 +8088,13 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + bundle-require@5.0.0: resolution: {integrity: sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -8288,6 +8362,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -8442,6 +8520,10 @@ packages: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} + crypto-random-string@4.0.0: + resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} + engines: {node: '>=12'} + css-box-model@1.2.1: resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} @@ -8647,6 +8729,14 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.4.0: + resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==} + engines: {node: '>=18'} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -8658,6 +8748,10 @@ packages: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -8775,8 +8869,8 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - dotenv@17.2.1: - resolution: {integrity: sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} drizzle-kit@0.30.6: @@ -8990,6 +9084,10 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + eciesjs@0.4.16: + resolution: {integrity: sha512-dS5cbA9rA2VR4Ybuvhg6jvdmp46ubLn3E+px8cG/35aEDNclrqoCjg6mt0HYZ/M+OoESS3jSkCrqk1kWAEhWAw==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -9561,6 +9659,10 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + exec-async@2.2.0: resolution: {integrity: sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==} @@ -9738,14 +9840,6 @@ packages: fbjs@3.0.5: resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} - fdir@6.4.2: - resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -9773,6 +9867,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-type@18.7.0: + resolution: {integrity: sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==} + engines: {node: '>=14.16'} + filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -9984,6 +10082,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} + engines: {node: '>=12'} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -10415,6 +10517,11 @@ packages: engines: {node: '>=8'} hasBin: true + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} @@ -10456,6 +10563,11 @@ packages: eslint: '*' typescript: '>=4.7.4' + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -10536,6 +10648,10 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} @@ -10591,6 +10707,10 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -11299,6 +11419,10 @@ packages: memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} @@ -11762,6 +11886,7 @@ packages: next@14.2.17: resolution: {integrity: sha512-hNo/Zy701DDO3nzKkPmsLRlDfNCtb1OJxFUvjGEl04u7SFa3zwC6hqsOUzMajcaEOEV8ey1GjvByvrg0Qr5AiQ==} engines: {node: '>=18.17.0'} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -11948,6 +12073,10 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} + object-treeify@1.1.33: + resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} + engines: {node: '>= 10'} + object.assign@4.1.5: resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} engines: {node: '>= 0.4'} @@ -12015,6 +12144,15 @@ packages: resolution: {integrity: sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==} deprecated: use oniguruma-to-es instead + open-cli@8.0.0: + resolution: {integrity: sha512-3muD3BbfLyzl+aMVSEfn2FfOqGdPYR0O4KNnxXsLEPE2q9OSjBfJAaB6XKbrUzLgymoSMejvb5jpXJfru/Ko2A==} + engines: {node: '>=18'} + hasBin: true + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + open@7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} @@ -12191,6 +12329,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + peek-readable@5.4.2: + resolution: {integrity: sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==} + engines: {node: '>=14.16'} + peek-stream@1.1.3: resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} @@ -12579,6 +12721,10 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -13009,6 +13155,14 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readable-web-to-node-stream@3.0.4: + resolution: {integrity: sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==} + engines: {node: '>=8'} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -13266,6 +13420,10 @@ packages: rst-selector-parser@2.2.3: resolution: {integrity: sha512-nDG1rZeP6oFTLN6yNDV/uiAvs1+FS/KlrEwh7+y7dpuApDBy6bI2HTBcc0/V8lv9OTqfyD34eF7au2pm8aBbhA==} + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-async@3.0.0: resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} engines: {node: '>=0.12.0'} @@ -13831,6 +13989,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strtok3@7.1.1: + resolution: {integrity: sha512-mKX8HA/cdBqMKUr0MMZAFssCkIGoZeSCMXgnt79yKxNFguMLVFgRe6wB+fsL0NmoHDbeyZXczy7vEPSoo3rkzg==} + engines: {node: '>=16'} + structured-headers@0.4.1: resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} @@ -13960,10 +14122,18 @@ packages: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} + temp-dir@3.0.0: + resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} + engines: {node: '>=14.16'} + tempy@0.6.0: resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} engines: {node: '>=10'} + tempy@3.1.0: + resolution: {integrity: sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==} + engines: {node: '>=14.16'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -14067,6 +14237,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + token-types@5.0.1: + resolution: {integrity: sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==} + engines: {node: '>=14.16'} + tokenx@1.1.0: resolution: {integrity: sha512-KCjtiC2niPwTSuz4ktM82Ki5bjqBwYpssiHDsGr5BpejN/B3ksacRvrsdoxljdMIh2nCX78alnDkeemBmYUmTA==} @@ -14216,6 +14390,14 @@ packages: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -14341,6 +14523,10 @@ packages: resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} engines: {node: '>=8'} + unique-string@3.0.0: + resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} + engines: {node: '>=12'} + unist-util-generated@2.0.1: resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==} @@ -15104,6 +15290,10 @@ packages: utf-8-validate: optional: true + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + xcode@3.0.1: resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==} engines: {node: '>=10.0.0'} @@ -15572,13 +15762,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.4)': + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 '@babel/traverse': 7.28.4 semver: 6.3.1 @@ -15766,15 +15956,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-member-expression-to-functions': 7.27.1 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.4 - transitivePeerDependencies: - - supports-color - '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -16019,6 +16200,11 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 @@ -16079,6 +16265,11 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -16359,14 +16550,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -16692,14 +16875,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) transitivePeerDependencies: - supports-color @@ -16849,14 +17032,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.27.1(@babel/core@7.28.4)': + '@babel/preset-typescript@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.5) transitivePeerDependencies: - supports-color @@ -17402,21 +17585,37 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' + '@dotenvx/dotenvx@1.51.2': + dependencies: + commander: 11.1.0 + dotenv: 17.2.3 + eciesjs: 0.4.16 + execa: 5.1.1 + fdir: 6.5.0(picomatch@4.0.3) + ignore: 5.3.2 + object-treeify: 1.1.33 + picomatch: 4.0.3 + which: 4.0.0 + '@drizzle-team/brocli@0.10.2': {} + '@ecies/ciphers@0.2.5(@noble/ciphers@1.3.0)': + dependencies: + '@noble/ciphers': 1.3.0 + '@electric-sql/client@1.0.0-beta.3': optionalDependencies: '@rollup/rollup-darwin-arm64': 4.24.4 '@electric-sql/client@1.0.7': dependencies: - '@microsoft/fetch-event-source': 2.0.1(patch_hash=lgwcujj3mimdfutlwueisfm32u) + '@microsoft/fetch-event-source': 2.0.1(patch_hash=46f4e76dd960e002a542732bb4323817a24fce1673cb71e2f458fe09776fa188) optionalDependencies: '@rollup/rollup-darwin-arm64': 4.46.1 '@electric-sql/client@1.2.0': dependencies: - '@microsoft/fetch-event-source': 2.0.1(patch_hash=lgwcujj3mimdfutlwueisfm32u) + '@microsoft/fetch-event-source': 2.0.1(patch_hash=46f4e76dd960e002a542732bb4323817a24fce1673cb71e2f458fe09776fa188) optionalDependencies: '@rollup/rollup-darwin-arm64': 4.46.1 @@ -18649,7 +18848,7 @@ snapshots: dependencies: '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': dependencies: @@ -18747,7 +18946,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@microsoft/fetch-event-source@2.0.1(patch_hash=lgwcujj3mimdfutlwueisfm32u)': {} + '@microsoft/fetch-event-source@2.0.1(patch_hash=46f4e76dd960e002a542732bb4323817a24fce1673cb71e2f458fe09776fa188)': {} '@napi-rs/wasm-runtime@1.0.7': dependencies: @@ -18785,8 +18984,16 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.17': optional: true + '@noble/ciphers@1.3.0': {} + '@noble/ciphers@2.0.1': {} + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + '@noble/hashes@2.0.1': {} '@nodelib/fs.scandir@2.1.5': @@ -20862,10 +21069,10 @@ snapshots: '@tanstack/router-utils@1.139.0': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/generator': 7.28.3 - '@babel/parser': 7.28.4 - '@babel/preset-typescript': 7.27.1(@babel/core@7.28.4) + '@babel/parser': 7.28.5 + '@babel/preset-typescript': 7.27.1(@babel/core@7.28.5) ansis: 4.1.0 diff: 8.0.2 pathe: 2.0.3 @@ -20876,12 +21083,12 @@ snapshots: '@tanstack/server-functions-plugin@1.139.0(vite@7.1.7(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: '@babel/code-frame': 7.27.1 - '@babel/core': 7.28.4 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) '@babel/template': 7.27.2 '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@tanstack/directive-functions-plugin': 1.139.0(vite@7.1.7(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) babel-dead-code-elimination: 1.0.10 tiny-invariant: 1.3.3 @@ -20900,8 +21107,8 @@ snapshots: '@tanstack/start-plugin-core@1.139.9(@tanstack/react-router@1.139.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(crossws@0.4.1(srvx@0.9.6))(vite@7.1.7(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: '@babel/code-frame': 7.26.2 - '@babel/core': 7.28.4 - '@babel/types': 7.28.4 + '@babel/core': 7.28.5 + '@babel/types': 7.28.5 '@rolldown/pluginutils': 1.0.0-beta.40 '@tanstack/router-core': 1.139.7 '@tanstack/router-generator': 1.139.7 @@ -21160,6 +21367,8 @@ snapshots: '@tiptap/extension-text-style': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) '@tiptap/pm': 2.9.1 + '@tokenizer/token@0.3.0': {} + '@trpc/client@11.7.2(@trpc/server@11.7.2(typescript@5.7.2))(typescript@5.7.2)': dependencies: '@trpc/server': 11.7.2(typescript@5.7.2) @@ -21496,6 +21705,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 7.18.0 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/eslint-plugin@8.18.0(@typescript-eslint/parser@8.46.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -21573,6 +21800,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.3.7(supports-color@5.5.0) + eslint: 8.57.1 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.46.0(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@typescript-eslint/scope-manager': 8.46.0 @@ -21710,6 +21950,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.8.3) + debug: 4.4.3 + eslint: 8.57.1 + ts-api-utils: 1.4.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/type-utils@8.18.0(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.6.3) @@ -21797,6 +22049,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.4.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/typescript-estree@8.18.0(typescript@5.6.3)': dependencies: '@typescript-eslint/types': 8.18.0 @@ -21900,6 +22167,17 @@ snapshots: - supports-color - typescript + '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) + eslint: 8.57.1 + transitivePeerDependencies: + - supports-color + - typescript + '@typescript-eslint/utils@8.18.0(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) @@ -22141,6 +22419,23 @@ snapshots: vite: 5.4.10(@types/node@22.19.1)(lightningcss@1.30.1)(terser@5.44.0) vue: 3.5.12(typescript@5.8.3) + '@vitest/coverage-istanbul@4.0.15(vitest@4.0.15(@types/node@20.17.6)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))': + dependencies: + '@istanbuljs/schema': 0.1.3 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magicast: 0.5.1 + obug: 2.1.1 + tinyrainbow: 3.0.3 + vitest: 4.0.15(@types/node@20.17.6)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + '@vitest/coverage-istanbul@4.0.15(vitest@4.0.15(@types/node@22.19.1)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: '@istanbuljs/schema': 0.1.3 @@ -22201,6 +22496,14 @@ snapshots: chai: 6.2.1 tinyrainbow: 3.0.3 + '@vitest/mocker@4.0.15(vite@7.1.7(@types/node@20.17.6)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 4.0.15 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.1.7(@types/node@20.17.6)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) + '@vitest/mocker@4.0.15(vite@7.1.7(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.0.15 @@ -22596,10 +22899,10 @@ snapshots: babel-dead-code-elimination@1.0.10: dependencies: - '@babel/core': 7.28.4 - '@babel/parser': 7.28.4 + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -22919,6 +23222,15 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + bundle-require@5.0.0(esbuild@0.24.0): dependencies: esbuild: 0.24.0 @@ -23199,6 +23511,8 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@11.1.0: {} + commander@12.1.0: {} commander@14.0.1: {} @@ -23362,6 +23676,10 @@ snapshots: crypto-random-string@2.0.0: {} + crypto-random-string@4.0.0: + dependencies: + type-fest: 1.4.0 + css-box-model@1.2.1: dependencies: tiny-invariant: 1.3.3 @@ -23538,6 +23856,13 @@ snapshots: deepmerge@4.3.1: {} + default-browser-id@5.0.1: {} + + default-browser@5.4.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + defaults@1.0.4: dependencies: clone: 1.0.4 @@ -23550,6 +23875,8 @@ snapshots: define-lazy-prop@2.0.0: {} + define-lazy-prop@3.0.0: {} + define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -23646,7 +23973,7 @@ snapshots: dotenv@16.6.1: {} - dotenv@17.2.1: {} + dotenv@17.2.3: {} drizzle-kit@0.30.6: dependencies: @@ -23713,6 +24040,13 @@ snapshots: dependencies: safe-buffer: 5.2.1 + eciesjs@0.4.16: + dependencies: + '@ecies/ciphers': 0.2.5(@noble/ciphers@1.3.0) + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + ee-first@1.1.1: {} ejs@3.1.10: @@ -24270,6 +24604,15 @@ snapshots: optionalDependencies: eslint-config-prettier: 10.1.8(eslint@9.32.0(jiti@2.6.1)) + eslint-plugin-prettier@5.5.3(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2): + dependencies: + eslint: 8.57.1 + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + eslint-config-prettier: 9.1.0(eslint@8.57.1) + eslint-plugin-react-compiler@19.0.0-beta-37ed2a7-20241206(eslint@8.57.1): dependencies: '@babel/core': 7.26.0 @@ -24649,6 +24992,8 @@ snapshots: eventemitter3@5.0.1: {} + events@3.3.0: {} + exec-async@2.2.0: {} execa@5.1.1: @@ -24734,7 +25079,7 @@ snapshots: dependencies: invariant: 2.2.4 - expo-router@5.1.4(y4jtnr2g2s4ue3hjudtn7mmiri): + expo-router@5.1.4(4385c7d2ebe6e555b6505e0ac211d063): dependencies: '@expo/metro-runtime': 5.0.4(react-native@0.80.1(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0)) '@expo/server': 0.6.3 @@ -24942,10 +25287,6 @@ snapshots: transitivePeerDependencies: - encoding - fdir@6.4.2(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -24967,6 +25308,12 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-type@18.7.0: + dependencies: + readable-web-to-node-stream: 3.0.4 + strtok3: 7.1.1 + token-types: 5.0.1 + filelist@1.0.4: dependencies: minimatch: 5.1.6 @@ -25206,6 +25553,8 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stdin@9.0.0: {} + get-stream@6.0.1: {} get-symbol-description@1.1.0: @@ -25676,6 +26025,8 @@ snapshots: is-docker@2.2.1: {} + is-docker@3.0.0: {} + is-extendable@0.1.1: {} is-extglob@2.1.1: {} @@ -25712,6 +26063,10 @@ snapshots: transitivePeerDependencies: - supports-color + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-interactive@1.0.0: {} is-map@2.0.3: {} @@ -25773,6 +26128,8 @@ snapshots: is-stream@2.0.1: {} + is-stream@3.0.0: {} + is-string@1.0.7: dependencies: has-tostringtag: 1.0.2 @@ -25823,6 +26180,10 @@ snapshots: dependencies: is-docker: 2.2.1 + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + isarray@1.0.0: {} isarray@2.0.5: {} @@ -25841,7 +26202,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/parser': 7.28.5 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 @@ -26678,6 +27039,8 @@ snapshots: memoize-one@6.0.0: {} + meow@12.1.1: {} + merge-descriptors@1.0.3: {} merge-descriptors@2.0.0: {} @@ -27625,6 +27988,8 @@ snapshots: object-keys@1.1.1: {} + object-treeify@1.1.33: {} + object.assign@4.1.5: dependencies: call-bind: 1.0.7 @@ -27702,6 +28067,21 @@ snapshots: dependencies: regex: 4.4.0 + open-cli@8.0.0: + dependencies: + file-type: 18.7.0 + get-stdin: 9.0.0 + meow: 12.1.1 + open: 10.2.0 + tempy: 3.1.0 + + open@10.2.0: + dependencies: + default-browser: 5.4.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + open@7.4.2: dependencies: is-docker: 2.2.1 @@ -27922,6 +28302,8 @@ snapshots: pathe@2.0.3: {} + peek-readable@5.4.2: {} + peek-stream@1.1.3: dependencies: buffer-from: 1.1.2 @@ -28254,6 +28636,8 @@ snapshots: process-nextick-args@2.0.1: {} + process@0.11.10: {} + progress@2.0.3: {} promise-inflight@1.0.1: {} @@ -28798,6 +29182,18 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readable-web-to-node-stream@3.0.4: + dependencies: + readable-stream: 4.7.0 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -29176,6 +29572,8 @@ snapshots: lodash.flattendeep: 4.4.0 nearley: 2.20.1 + run-applescript@7.1.0: {} + run-async@3.0.0: {} run-parallel@1.2.0: @@ -29779,6 +30177,11 @@ snapshots: strip-json-comments@3.1.1: {} + strtok3@7.1.1: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 5.4.2 + structured-headers@0.4.1: {} style-mod@4.1.2: {} @@ -29961,6 +30364,8 @@ snapshots: temp-dir@2.0.0: {} + temp-dir@3.0.0: {} + tempy@0.6.0: dependencies: is-stream: 2.0.1 @@ -29968,6 +30373,13 @@ snapshots: type-fest: 0.16.0 unique-string: 2.0.0 + tempy@3.1.0: + dependencies: + is-stream: 3.0.0 + temp-dir: 3.0.0 + type-fest: 2.19.0 + unique-string: 3.0.0 + term-size@2.2.1: {} terminal-link@2.1.1: @@ -30024,7 +30436,7 @@ snapshots: tinyglobby@0.2.10: dependencies: - fdir: 6.4.2(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 tinyglobby@0.2.15: @@ -30070,6 +30482,11 @@ snapshots: toidentifier@1.0.1: {} + token-types@5.0.1: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + tokenx@1.1.0: {} toml@3.0.0: {} @@ -30116,6 +30533,10 @@ snapshots: dependencies: typescript: 5.6.3 + ts-api-utils@1.4.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + ts-api-utils@2.1.0(typescript@5.6.3): dependencies: typescript: 5.6.3 @@ -30207,6 +30628,34 @@ snapshots: - tsx - yaml + tsup@8.3.5(@swc/core@1.9.1(@swc/helpers@0.5.5))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.1): + dependencies: + bundle-require: 5.0.0(esbuild@0.24.0) + cac: 6.7.14 + chokidar: 4.0.1 + consola: 3.2.3 + debug: 4.3.7(supports-color@5.5.0) + esbuild: 0.24.0 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.3)(yaml@2.8.1) + resolve-from: 5.0.0 + rollup: 4.24.4 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.1 + tinyglobby: 0.2.10 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.9.1(@swc/helpers@0.5.5) + postcss: 8.5.6 + typescript: 5.8.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsx@4.19.2: dependencies: esbuild: 0.23.1 @@ -30237,6 +30686,10 @@ snapshots: type-fest@0.7.1: {} + type-fest@1.4.0: {} + + type-fest@2.19.0: {} + type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -30372,6 +30825,10 @@ snapshots: dependencies: crypto-random-string: 2.0.0 + unique-string@3.0.0: + dependencies: + crypto-random-string: 4.0.0 + unist-util-generated@2.0.1: {} unist-util-is@5.2.1: @@ -30688,6 +31145,23 @@ snapshots: lightningcss: 1.30.1 terser: 5.44.0 + vite@7.1.7(@types/node@20.17.6)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1): + dependencies: + esbuild: 0.25.8 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.46.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.17.6 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.1 + terser: 5.44.0 + tsx: 4.20.3 + yaml: 2.8.1 + vite@7.1.7(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: esbuild: 0.25.8 @@ -30786,6 +31260,44 @@ snapshots: dependencies: vitest: 4.0.15(@types/node@22.19.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) + vitest@4.0.15(@types/node@20.17.6)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1): + dependencies: + '@vitest/expect': 4.0.15 + '@vitest/mocker': 4.0.15(vite@7.1.7(@types/node@20.17.6)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) + '@vitest/pretty-format': 4.0.15 + '@vitest/runner': 4.0.15 + '@vitest/snapshot': 4.0.15 + '@vitest/spy': 4.0.15 + '@vitest/utils': 4.0.15 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.1.7(@types/node@20.17.6)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.17.6 + jsdom: 27.2.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + vitest@4.0.15(@types/node@22.19.1)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.15 @@ -31239,6 +31751,10 @@ snapshots: ws@8.18.3: {} + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 + xcode@3.0.1: dependencies: simple-plist: 1.3.1 diff --git a/scripts/comment-on-release.mjs b/scripts/comment-on-release.mjs index 9cdeb5c4c4..c7a66e4f3c 100644 --- a/scripts/comment-on-release.mjs +++ b/scripts/comment-on-release.mjs @@ -8,17 +8,17 @@ import { execSync } from 'child_process' * and posts comments on both. */ -const REPO = process.env.GITHUB_REPOSITORY || 'electric-sql/electric' +const REPO = process.env.GITHUB_REPOSITORY || `electric-sql/electric` async function main() { - const publishedPackages = JSON.parse(process.env.PUBLISHED_PACKAGES || '[]') + const publishedPackages = JSON.parse(process.env.PUBLISHED_PACKAGES || `[]`) if (publishedPackages.length === 0) { - console.log('No published packages found') + console.log(`No published packages found`) return } - console.log('Published packages:', publishedPackages) + console.log(`Published packages:`, publishedPackages) // Map to collect PRs and their associated packages const prToPackages = new Map() @@ -91,19 +91,19 @@ async function main() { function findChangelogPath(packageName) { // Map package names to their directories const packageDirs = [ - 'packages/typescript-client', - 'packages/react-hooks', - 'packages/experimental', - 'packages/sync-service', - 'packages/elixir-client', - 'packages/y-electric', + `packages/typescript-client`, + `packages/react-hooks`, + `packages/experimental`, + `packages/sync-service`, + `packages/elixir-client`, + `packages/y-electric`, ] for (const dir of packageDirs) { const pkgJsonPath = `${dir}/package.json` if (existsSync(pkgJsonPath)) { try { - const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) + const pkgJson = JSON.parse(readFileSync(pkgJsonPath, `utf8`)) if (pkgJson.name === packageName) { const changelogPath = `${dir}/CHANGELOG.md` if (existsSync(changelogPath)) { @@ -120,7 +120,7 @@ function findChangelogPath(packageName) { } function extractCommitsFromChangelog(changelogPath, version) { - const changelog = readFileSync(changelogPath, 'utf8') + const changelog = readFileSync(changelogPath, `utf8`) const commits = [] // Find the section for this version @@ -159,10 +159,10 @@ async function findPRForCommit(commitHash) { // Use gh CLI to find PR associated with commit const result = execSync( `gh api repos/${REPO}/commits/${commitHash}/pulls --jq '.[0].number'`, - { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } + { encoding: `utf8`, stdio: [`pipe`, `pipe`, `pipe`] } ).trim() - if (result && result !== 'null') { + if (result && result !== `null`) { console.log(` Commit ${commitHash} -> PR #${result}`) return parseInt(result, 10) } @@ -176,7 +176,7 @@ async function findPRForCommit(commitHash) { async function commentOnPR(prNumber, packages) { const packageList = packages .map((p) => `- \`${p.name}@${p.version}\``) - .join('\n') + .join(`\n`) const body = `This PR has been released! :rocket: @@ -190,7 +190,7 @@ Thanks for contributing to Electric!` // Check if we already commented on this PR const existingComments = execSync( `gh api repos/${REPO}/issues/${prNumber}/comments --jq '[.[] | select(.body | contains("This PR has been released!"))] | length'`, - { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } + { encoding: `utf8`, stdio: [`pipe`, `pipe`, `pipe`] } ).trim() if (parseInt(existingComments, 10) > 0) { @@ -201,8 +201,8 @@ Thanks for contributing to Electric!` // Use --body-file with stdin to avoid shell interpretation of backticks execSync(`gh pr comment ${prNumber} --repo ${REPO} --body-file -`, { input: body, - encoding: 'utf8', - stdio: ['pipe', 'inherit', 'inherit'], + encoding: `utf8`, + stdio: [`pipe`, `inherit`, `inherit`], }) console.log(` Commented on PR #${prNumber}`) } catch (e) { @@ -211,7 +211,7 @@ Thanks for contributing to Electric!` } async function findLinkedIssues(prNumber) { - const [owner, repo] = REPO.split('/') + const [owner, repo] = REPO.split(`/`) const query = ` query($owner: String!, $repo: String!, $pr: Int!) { repository(owner: $owner, name: $repo) { @@ -229,12 +229,12 @@ async function findLinkedIssues(prNumber) { try { const result = execSync( `gh api graphql -f query='${query}' -F owner='${owner}' -F repo='${repo}' -F pr=${prNumber} --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number'`, - { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } + { encoding: `utf8`, stdio: [`pipe`, `pipe`, `pipe`] } ).trim() if (result) { - const issues = result.split('\n').map((n) => parseInt(n, 10)) - console.log(` PR #${prNumber} links to issues: ${issues.join(', ')}`) + const issues = result.split(`\n`).map((n) => parseInt(n, 10)) + console.log(` PR #${prNumber} links to issues: ${issues.join(`, `)}`) return issues } } catch (e) { @@ -247,10 +247,10 @@ async function findLinkedIssues(prNumber) { async function commentOnIssue(issueNumber, prNumbers, packages) { const packageList = packages .map((p) => `- \`${p.name}@${p.version}\``) - .join('\n') + .join(`\n`) - const prLinks = prNumbers.map((pr) => `#${pr}`).join(', ') - const prWord = prNumbers.length === 1 ? 'PR' : 'PRs' + const prLinks = prNumbers.map((pr) => `#${pr}`).join(`, `) + const prWord = prNumbers.length === 1 ? `PR` : `PRs` const body = `The ${prWord} fixing this issue (${prLinks}) has been released! :rocket: @@ -264,7 +264,7 @@ Thanks for reporting!` // Check if we already commented on this issue const existingComments = execSync( `gh api repos/${REPO}/issues/${issueNumber}/comments --jq '[.[] | select(.body | contains("A fix for this issue has been released!"))] | length'`, - { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } + { encoding: `utf8`, stdio: [`pipe`, `pipe`, `pipe`] } ).trim() if (parseInt(existingComments, 10) > 0) { @@ -275,8 +275,8 @@ Thanks for reporting!` // Use --body-file with stdin to avoid shell interpretation of backticks execSync(`gh issue comment ${issueNumber} --repo ${REPO} --body-file -`, { input: body, - encoding: 'utf8', - stdio: ['pipe', 'inherit', 'inherit'], + encoding: `utf8`, + stdio: [`pipe`, `inherit`, `inherit`], }) console.log(` Commented on issue #${issueNumber}`) } catch (e) { diff --git a/website/docs/_tutorial.md b/website/docs/_tutorial.md new file mode 100644 index 0000000000..75aa598831 --- /dev/null +++ b/website/docs/_tutorial.md @@ -0,0 +1,32 @@ +Tutorial +- development +- concepts +- integration +- deployment + + +more comprehensive walkthrough app +start from quickstart app +getting started +migration from API-based apps +coding with agents +understanding what's going on under the hood +components and installation +postgres, data model, logical replication +electric +sync engine, shape logs, partial replication +sync protocol, HTTP API, long polling +data delivery and caching +using as part of your stack +proxying through your API / worker +constructing shapes in the backend +authorising shape requests +syncing into TanStack DB +live queries +mutations +ingesting writes +moving to production +security +deployment +monitoring +performance \ No newline at end of file