-
Notifications
You must be signed in to change notification settings - Fork 152
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(examples): starter template (#314)
This PR introduces a package containing a NodeJS script to bootstrap Electric apps. --------- Co-authored-by: James Arthur <[email protected]>
- Loading branch information
Showing
33 changed files
with
1,874 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"create-electric-app": patch | ||
--- | ||
|
||
Starter template for bootstrapping Electric applications. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules | ||
template/node_modules | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
|
||
# ElectricSQL Starter App | ||
|
||
This is a starter app template. You can use it to generate an ElectricSQL application. The app is setup to match the example code you can see in the [Quickstart](https://electric-sql.com/docs/quickstart). | ||
|
||
## Pre-reqs | ||
|
||
- Docker (with Compose V2) | ||
- Node >= 16.11.0 | ||
|
||
## Usage | ||
|
||
```sh | ||
npx create-electric-app my-app | ||
``` | ||
|
||
Change directory into the created folder (`./my-app` in the example command above) and then follow the instructions in the generated README. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"name": "create-electric-app", | ||
"version": "0.1.0", | ||
"author": "ElectricSQL", | ||
"license": "Apache-2.0", | ||
"main": "dist/index.js", | ||
"type": "module", | ||
"platform": "node", | ||
"files": [ | ||
"dist" | ||
], | ||
"bin": { | ||
"create-electric-app": "dist/index.js" | ||
}, | ||
"scripts": { | ||
"build": "rm -rf ./dist && tsc && tsmodule build" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^18.8.4", | ||
"@tsmodule/tsmodule": "42" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
#!/usr/bin/env node | ||
|
||
// Usage: npx create-electric-app my-app | ||
|
||
import { spawn } from 'child_process' | ||
import * as fs from 'fs/promises' | ||
import { fileURLToPath } from 'url' | ||
import path from 'path' | ||
|
||
// The first argument will be the project name | ||
const projectName = process.argv[2] | ||
|
||
// Create a project directory with the project name | ||
const currentDir = process.cwd() | ||
const projectDir = path.resolve(currentDir, projectName) | ||
await fs.mkdir(projectDir, { recursive: true }) | ||
|
||
// Copy the app template to the project's directory | ||
const __dirname = path.dirname(fileURLToPath(import.meta.url)) // because __dirname is not defined when using modules | ||
const templateDir = path.resolve(__dirname, '..', 'template') | ||
await fs.cp(templateDir, projectDir, { recursive: true }) | ||
|
||
// The template stores dotfiles without the dot | ||
// such that they do not get picked by gitignore. | ||
// Now that we copies all files, we rename those | ||
// dotfiles to their right name | ||
await fs.rename( | ||
path.join(projectDir, 'dot_gitignore'), | ||
path.join(projectDir, '.gitignore') | ||
) | ||
await fs.rename( | ||
path.join(projectDir, 'dot_npmrc'), | ||
path.join(projectDir, '.npmrc') | ||
) | ||
const envrcFile = path.join(projectDir, 'backend', 'compose', '.envrc') | ||
await fs.rename( | ||
path.join(projectDir, 'backend', 'compose', 'dot_envrc'), | ||
envrcFile | ||
) | ||
|
||
// import package.json and deep copy it | ||
// otherwise we can't edit it because | ||
// the JSON object is not extensible | ||
const projectPackageJson = (await import(path.join(projectDir, 'package.json'), { assert: { type: "json" } })).default | ||
|
||
// Update the project's package.json with the new project name | ||
projectPackageJson.name = projectName | ||
|
||
await fs.writeFile( | ||
path.join(projectDir, 'package.json'), | ||
JSON.stringify(projectPackageJson, null, 2) | ||
) | ||
|
||
// Update the project's title in the index.html file | ||
const indexFile = path.join(projectDir, 'public', 'index.html') | ||
const index = await fs.readFile(indexFile, 'utf8') | ||
const newIndex = index.replace('ElectricSQL starter template', projectName) | ||
await fs.writeFile(indexFile, newIndex) | ||
|
||
// Store the app's name in .envrc | ||
// db name must start with a letter | ||
// and contain only alphanumeric characters and underscores | ||
// so we let the name start at the first letter | ||
// and replace non-alphanumeric characters with _ | ||
const name = projectName.match(/[a-zA-Z].*/)?.[0] // strips prefix of non-alphanumeric characters | ||
if (name) { | ||
const dbName = name.replace(/[\W_]+/g, '_') | ||
await fs.appendFile(envrcFile, `export APP_NAME=${dbName}`) | ||
} | ||
|
||
// Run `yarn install` in the project directory to install the dependencies | ||
const proc = spawn('yarn install', [], { stdio: 'inherit', cwd: projectDir, shell: true }) | ||
|
||
proc.on('close', (code) => { | ||
if (code === 0) { | ||
console.log(`Success! Your ElectricSQL app is ready at \`./${projectName}\``) | ||
} | ||
else { | ||
console.log(`Could not install project dependencies. Nevertheless the template for your app can be found at \`./${projectName}\``) | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
|
||
# Welcome to your ElectricSQL app! | ||
|
||
Start the backend: | ||
|
||
```shell | ||
yarn backend:start | ||
``` | ||
|
||
Open a new tab in your terminal. Navigate back to the same folder. Apply the migrations (defined in `./db/migrations`): | ||
|
||
```shell | ||
yarn db:migrate | ||
``` | ||
|
||
Generate your client: | ||
|
||
```sh | ||
yarn client:generate | ||
``` | ||
|
||
Start your app: | ||
|
||
```sh | ||
yarn start | ||
``` | ||
|
||
Open [localhost:3001](http://localhost:3001) in your web browser. | ||
|
||
## Changing your database schema | ||
|
||
You can watch for database schema changes and automatically generate a new client using: | ||
|
||
```sh | ||
yarn client:watch | ||
``` | ||
|
||
## Notes | ||
|
||
- `yarn backend:start` uses Docker Compose to start Postgres and the [Electric sync service](https://electric-sql.com/docs/api/service). See [running the examples](https://electric-sql.com/docs/examples/notes/running#running-your-own-postgres) for information about configuring the Electric sync service to run against an existing Postgres database. | ||
- `yarn client:watch` calls `npx electric-sql generate --watch` under the hood. See [https://electric-sql.com/docs/api/generator](https://electric-sql.com/docs/api/generator) for more details. | ||
|
||
## More information | ||
|
||
- [Documentation](https://electric-sql.com/docs) | ||
- [Quickstart](https://electric-sql.com/docs/quickstart) | ||
- [Usage guide](https://electric-sql.com/docs/usage) |
52 changes: 52 additions & 0 deletions
52
examples/starter/template/backend/compose/docker-compose.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
version: "3.8" | ||
|
||
configs: | ||
postgres_config: | ||
file: "./postgres/postgres.conf" | ||
|
||
volumes: | ||
pg_data: | ||
electric_data: | ||
|
||
services: | ||
postgres: | ||
image: "${POSTGRESQL_IMAGE:-postgres:14-alpine}" | ||
environment: | ||
POSTGRES_DB: ${APP_NAME:-electric} | ||
POSTGRES_USER: postgres | ||
POSTGRES_PASSWORD: password | ||
command: | ||
- -c | ||
- config_file=/etc/postgresql.conf | ||
configs: | ||
- source: postgres_config | ||
target: /etc/postgresql.conf | ||
healthcheck: | ||
test: ["CMD-SHELL", "pg_isready -U postgres"] | ||
extra_hosts: | ||
- "host.docker.internal:host-gateway" | ||
ports: | ||
- 5432 | ||
volumes: | ||
- pg_data:/var/lib/postgresql/data | ||
|
||
electric: | ||
image: "${ELECTRIC_IMAGE:-electricsql/electric:latest}" | ||
init: true | ||
environment: | ||
DATABASE_URL: postgresql://postgres:password@postgres:5432/${APP_NAME:-electric} | ||
LOGICAL_PUBLISHER_HOST: electric | ||
# Currently published version ([email protected]) | ||
# still uses `ELECTRIC_HOST` variable | ||
# but future versions will use `LOGICAL_PUBLISHER_HOST` | ||
# TODO: Remove `ELECTRIC_HOST` when next version is published. | ||
ELECTRIC_HOST: electric | ||
OFFSET_STORAGE_FILE: "/app/data/offset_storage.dat" | ||
AUTH_MODE: insecure | ||
ports: | ||
- 5050:5050 | ||
- 5133:5133 | ||
volumes: | ||
- electric_data:/app/data | ||
depends_on: | ||
- postgres |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Docker images for the local stack | ||
export POSTGRESQL_IMAGE=postgres:14-alpine | ||
export ELECTRIC_IMAGE=electricsql/electric:latest |
4 changes: 4 additions & 0 deletions
4
examples/starter/template/backend/compose/postgres/postgres.conf
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
listen_addresses = '*' | ||
wal_level = 'logical' | ||
# log_min_messages = 'debug1' | ||
# log_min_error_statement = 'debug1' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
const shell = require('shelljs') | ||
|
||
let db = process.env.DATABASE_URL | ||
|
||
if (process.argv.length === 4) { | ||
const command = process.argv[2] | ||
|
||
if (command !== '-db') { | ||
console.error(`Unsupported option ${command}. Only '-db' option is supported.`) | ||
|
||
process.exit(1) | ||
} | ||
|
||
db = process.argv[3] | ||
} | ||
else if (process.argv.length !== 2) { | ||
console.log('Wrong number of arguments provided. Only one optional argument `-db <Postgres connection url>` is supported.') | ||
} | ||
|
||
if (db === undefined) { | ||
console.error(`Database URL is not provided. Please provide one using the DATABASE_URL environment variable.`) | ||
|
||
process.exit(1) | ||
} | ||
|
||
const electric = process.env.ELECTRIC_IMAGE ?? "electricsql/electric:latest" | ||
|
||
shell.exec( | ||
`docker run \ | ||
-e "DATABASE_URL=${db}" \ | ||
-e "ELECTRIC_HOST=localhost" \ | ||
-e "LOGICAL_PUBLISHER_HOST=localhost" \ | ||
-e "AUTH_MODE=insecure" \ | ||
-p 5050:5050 \ | ||
-p 5133:5133 \ | ||
-p 5433:5433 ${electric}` | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
const { build, serve } = require('esbuild') | ||
|
||
const { createServer, request } = require('http') | ||
const { spawn } = require('child_process') | ||
|
||
const fs = require('fs-extra') | ||
const inlineImage = require('esbuild-plugin-inline-image') | ||
|
||
const shouldMinify = process.env.NODE_ENV === 'production' | ||
const shouldServe = process.env.SERVE === 'true' | ||
|
||
// https://github.com/evanw/esbuild/issues/802#issuecomment-819578182 | ||
const liveServer = (buildOpts) => { | ||
const clients = [] | ||
|
||
build( | ||
{ | ||
...buildOpts, | ||
banner: { js: ' (() => new EventSource("/esbuild").onmessage = () => location.reload())();' }, | ||
watch: { | ||
onRebuild(error, result) { | ||
clients.forEach((res) => res.write('data: update\n\n')) | ||
clients.length = 0 | ||
console.log(error ? error : '...') | ||
}, | ||
} | ||
} | ||
).catch(() => process.exit(1)) | ||
|
||
serve({servedir: 'dist' }, {}) | ||
.then(() => { | ||
createServer((req, res) => { | ||
const { url, method, headers } = req | ||
|
||
if (url === '/esbuild') | ||
return clients.push( | ||
res.writeHead(200, { | ||
'Content-Type': 'text/event-stream', | ||
'Cache-Control': 'no-cache', | ||
Connection: 'keep-alive', | ||
}) | ||
) | ||
|
||
const path = ~url.split('/').pop().indexOf('.') ? url : `/index.html` //for PWA with router | ||
req.pipe( | ||
request({ hostname: '0.0.0.0', port: 8000, path, method, headers }, (prxRes) => { | ||
res.writeHead(prxRes.statusCode, prxRes.headers) | ||
prxRes.pipe(res, { end: true }) | ||
}), | ||
{ end: true } | ||
) | ||
}).listen(3001) | ||
|
||
setTimeout(() => { | ||
const op = { darwin: ['open'], linux: ['xdg-open'], win32: ['cmd', '/c', 'start'] } | ||
const ptf = process.platform | ||
if (clients.length === 0) spawn(op[ptf][0], [...[op[ptf].slice(1)], `http://localhost:3001`]) | ||
}, 500) // open the default browser only if it is not opened yet | ||
}) | ||
} | ||
|
||
/** | ||
* ESBuild Params | ||
* @link https://esbuild.github.io/api/#build-api | ||
*/ | ||
let buildParams = { | ||
color: true, | ||
entryPoints: ["src/index.tsx"], | ||
loader: { ".ts": "tsx" }, | ||
outdir: "dist", | ||
minify: shouldMinify, | ||
format: "cjs", | ||
bundle: true, | ||
sourcemap: true, | ||
logLevel: "error", | ||
incremental: true, | ||
external: ["fs", "path"], | ||
plugins: [inlineImage()], | ||
}; | ||
|
||
(async () => { | ||
fs.removeSync("dist"); | ||
fs.copySync("public", "dist"); | ||
|
||
if (shouldServe) { | ||
liveServer(buildParams) | ||
} | ||
else { | ||
await build(buildParams) | ||
|
||
process.exit(0) | ||
} | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
const { copyFile } = require('node:fs/promises') | ||
const path = require('node:path') | ||
|
||
// Copies the wasm files needed for wa-sqlite | ||
// from `/node_modules/wa-sqlite/dist` into `public` | ||
const waSqlitePath = path.join('node_modules', 'wa-sqlite', 'dist') | ||
const publicFolder = 'public' | ||
|
||
const mjsFileName = 'wa-sqlite-async.mjs' | ||
const mjsFile = path.join(waSqlitePath, mjsFileName) | ||
const mjsDest = path.join(publicFolder, mjsFileName) | ||
|
||
const wasmFileName = 'wa-sqlite-async.wasm' | ||
const wasmFile = path.join(waSqlitePath, wasmFileName) | ||
const wasmDest = path.join(publicFolder, wasmFileName) | ||
|
||
try { | ||
copyFile(mjsFile, mjsDest) | ||
copyFile(wasmFile, wasmDest) | ||
} catch { | ||
console.error('Could not copy wasm files required for wa-sqlite. Did you forget to run `npm install` ?') | ||
} |
Oops, something went wrong.