Skip to content

Commit

Permalink
chore(examples): Update examples/expo with the new starter template (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
alco authored Nov 1, 2023
1 parent bcb5a1b commit 6cc92da
Show file tree
Hide file tree
Showing 14 changed files with 915 additions and 83 deletions.
6 changes: 4 additions & 2 deletions examples/expo/backend/compose/.envrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Docker images for the local stack
export POSTGRESQL_IMAGE=postgres:14-alpine
export ELECTRIC_IMAGE=electricsql/electric:latest
export APP_NAME=expo
export APP_NAME=expo
export ELECTRIC_PORT=5133
export ELECTRIC_PROXY_PORT=65432
export ELECTRIC_IMAGE=electricsql/electric@sha256:ac68b8a4adbf6ea14db8ba0027ec6bc28264887b51e293914d9f94b0cd0bec09
9 changes: 6 additions & 3 deletions examples/expo/backend/compose/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
version: "3.8"
name: "${APP_NAME:-electric}"

configs:
postgres_config:
Expand All @@ -13,7 +14,7 @@ services:
environment:
POSTGRES_DB: ${APP_NAME:-electric}
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_PASSWORD: pg_password
command:
- -c
- config_file=/etc/postgresql.conf
Expand All @@ -33,10 +34,12 @@ services:
image: "${ELECTRIC_IMAGE:-electricsql/electric:latest}"
init: true
environment:
DATABASE_URL: postgresql://postgres:password@postgres:5432/${APP_NAME:-electric}
DATABASE_URL: postgresql://postgres:pg_password@postgres:5432/${APP_NAME:-electric}
PG_PROXY_PASSWORD: proxy_password
LOGICAL_PUBLISHER_HOST: electric
AUTH_MODE: insecure
ports:
- 5133:5133
- ${ELECTRIC_PORT:-5133}:5133
- ${ELECTRIC_PROXY_PORT:-65432}:65432
depends_on:
- postgres
16 changes: 16 additions & 0 deletions examples/expo/backend/startCompose.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const { dockerCompose } = require('../util/util.js')
const process = require('process')

const cliArguments = process.argv.slice(2)

dockerCompose('up', cliArguments, (code) => {
if (code !== 0) {
console.error(
'\x1b[31m',
'Failed to start the Electric backend. Check the output from `docker compose` above.\n' +
'If the error message mentions a port already being allocated or address being already in use,\n' +
'execute `yarn ports:configure` to run Electric on another port.',
'\x1b[0m'
)
}
})
69 changes: 57 additions & 12 deletions examples/expo/backend/startElectric.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,81 @@
const shell = require('shelljs')

let db = process.env.DATABASE_URL
let electricPort = process.env.ELECTRIC_PORT ?? 5133
let electricProxyPort = process.env.ELECTRIC_PROXY_PORT ?? 65432

if (process.argv.length === 4) {
const command = process.argv[2]
let args = process.argv.slice(2)

if (command !== '-db') {
console.error(`Unsupported option ${command}. Only '-db' option is supported.`)
while (args.length > 0) {
// There are arguments to parse
const flag = args[0]
const value = args[1]

process.exit(1)
args = args.slice(2)

const checkValue = () => {
if (typeof value === 'undefined') {
error(`Missing value for option '${flag}'.`)
}
}

db = process.argv[3]
switch (flag) {
case '-db':
checkValue()
db = value
break
case '--electric-port':
checkValue()
electricPort = parsePort(value)
break
case '--electric-proxy-port':
checkValue()
electricProxyPort = parsePort(value)
break
default:
error(`Unrecognized option: '${flag}'.`)
}
}
else if (process.argv.length !== 2) {
console.log('Wrong number of arguments provided. Only one optional argument `-db <Postgres connection url>` is supported.')

function parsePort(port) {
// checks that the number is between 0 and 65535
const portRegex = /^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/
if (!portRegex.test(port)) {
error(`Invalid port '${port}. Port should be between 0 and 65535.'`)
}
return port
}

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 pull electricsql/electric:latest')
shell.exec(
// 5433 is the logical publisher port
// it is exposed because PG must be able to connect to Electric
const res = shell.exec(
`docker run \
-e "DATABASE_URL=${db}" \
-e "LOGICAL_PUBLISHER_HOST=localhost" \
-e "AUTH_MODE=insecure" \
-p 5133:5133 \
-p ${electricPort}:5133 \
-p ${electricProxyPort}:65432 \
-p 5433:5433 ${electric}`
)

if (res.code !== 0 && res.stderr.includes('port is already allocated')) {
// inform the user that they should change ports
console.error(
'\x1b[31m',
'Could not start Electric because the port seems to be taken.\n' +
'To run Electric on another port execute `yarn ports:configure`',
'\x1b[0m'
)
}

function error(err) {
console.error('\x1b[31m', err + '\nyarn electric:start [-db <Postgres connection url>] [--electric-port <port>] [--electric-proxy-port <port>]', '\x1b[0m')
process.exit(1)
}
99 changes: 99 additions & 0 deletions examples/expo/builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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((serveResult) => {
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: serveResult.port, 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
const url = "http://localhost:3001"
if (clients.length === 0) spawn(op[ptf][0], [...[op[ptf].slice(1)], url])
console.info(`Your app is running at ${url}`)
}, 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,
define: {
__DEBUG_MODE__: JSON.stringify(process.env.DEBUG_MODE === 'true'),
__ELECTRIC_URL__: JSON.stringify(process.env.ELECTRIC_URL ?? 'ws://localhost:5133'),
},
external: ["fs", "path"],
plugins: [inlineImage()],
};

(async () => {
fs.removeSync("dist");
fs.copySync("public", "dist");

if (shouldServe) {
liveServer(buildParams)
}
else {
await build(buildParams)

process.exit(0)
}
})();
135 changes: 135 additions & 0 deletions examples/expo/change-ports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
const portUsed = require('tcp-port-used')
const prompt = require('prompt')
const path = require('path')
const fs = require('fs/promises')
const { findFirstMatchInFile, fetchConfiguredElectricPort, fetchConfiguredElectricProxyPort } = require('./util/util.js')

// Regex to check that a number is between 0 and 65535
const portRegex = /^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/

init()

// Wrap code in an anonymous async function
// such that we can await
async function init() {
// Find the old ports for Electric, its proxy, and the webserver
// such that we know which ports to replace
const webserverPortRegex = /listen\(([0-9]+)\)/

const packageJsonFile = path.join(__dirname, 'package.json')
const builderFile = path.join(__dirname, 'builder.js')

const oldElectricPort = await fetchConfiguredElectricPort()
const oldElectricProxyPort = await fetchConfiguredElectricProxyPort()
const oldWebserverPort = await findFirstMatchInFile(webserverPortRegex, builderFile, 'Could not find current webserver port in builder.js')

prompt.start()
let electricPort = oldElectricPort
let electricProxyPort = oldElectricProxyPort
let webserverPort = oldWebserverPort

const userInput = await prompt.get({
properties: {
electricPort: {
description: 'Choose a port for Electric',
type: 'number',
pattern: portRegex,
message: 'Port should be between 0 and 65535.',
default: oldElectricPort
},
electricProxyPort: {
description: "Choose a port for Electric's DB proxy",
type: 'number',
pattern: portRegex,
message: 'Port should be between 0 and 65535.',
default: electricProxyPort
},
webserverPort: {
description: 'Choose a port the webserver',
type: 'number',
pattern: portRegex,
message: 'Port should be between 0 and 65535.',
default: oldWebserverPort
},
}
})

electricPort = await checkPort(userInput.electricPort, 'Electric', 5133)
electricProxyPort = await checkPort(userInput.electricProxyPort, "Electric's proxy", 65432)
webserverPort = await checkPort(userInput.webserverPort, 'the web server', 3001)

// Update port in package.json file
await findAndReplaceInFile(`http://localhost:${oldElectricPort}`, `http://localhost:${electricPort}`, packageJsonFile)
await findAndReplaceInFile(
`postgresql://prisma:proxy_password@localhost:${oldElectricProxyPort}/electric`,
`postgresql://prisma:proxy_password@localhost:${electricProxyPort}/electric`, packageJsonFile
)

// Update the port on which Electric runs in the builder.js file
await findAndReplaceInFile(`ws://localhost:${oldElectricPort}`, `ws://localhost:${electricPort}`, builderFile)

// Update the port on which Electric and its proxy run in startElectric.js file
const startElectricFile = path.join(__dirname, 'backend', 'startElectric.js')
await findAndReplaceInFile(oldElectricPort, `${electricPort}`, startElectricFile)
await findAndReplaceInFile(oldElectricProxyPort, `${electricProxyPort}`, startElectricFile)

// Update the port of the web server of the example in the builder.js file
await findAndReplaceInFile(`listen(${oldWebserverPort})`, `listen(${webserverPort})`, builderFile)
await findAndReplaceInFile(`http://localhost:${oldWebserverPort}`, `http://localhost:${webserverPort}`, builderFile)

// Update the port for Electric and its proxy in .envrc
const envrcFile = path.join(__dirname, 'backend', 'compose', '.envrc')
await findAndReplaceInFile(`export ELECTRIC_PORT=${oldElectricPort}`, `export ELECTRIC_PORT=${electricPort}`, envrcFile)
await findAndReplaceInFile(`export ELECTRIC_PROXY_PORT=${oldElectricProxyPort}`, `export ELECTRIC_PROXY_PORT=${electricProxyPort}`, envrcFile)

console.info(`⚡️ Success! Your project is now configured to run Electric on port ${electricPort}, the DB proxy on port ${electricProxyPort}, and the webserver on port ${webserverPort}.`)
}

/*
* Replaces the first occurence of `find` by `replace` in the file `file`.
* If `find` is a regular expression that sets the `g` flag, then it replaces all occurences.
*/
async function findAndReplaceInFile(find, replace, file) {
const content = await fs.readFile(file, 'utf8')
const replacedContent = content.replace(find, replace)
await fs.writeFile(file, replacedContent)
}

/**
* Checks if the given port is open.
* If not, it will ask the user if
* they want to choose another port.
* @returns The chosen port.
*/
async function checkPort(port, process, defaultPort) {
const portOccupied = await portUsed.check(port)
if (!portOccupied) {
return port
}

// Warn the user that the chosen port is occupied
console.warn(`Port ${port} for ${process} is already in use.`)
// Propose user to change port
prompt.start()

const { port: newPort } = (await prompt.get({
properties: {
port: {
description: 'Hit Enter to keep it or enter a different port number',
type: 'number',
pattern: portRegex,
message: 'Please choose a port between 0 and 65535',
default: port,
}
}
}))

if (newPort === port) {
// user chose not to change port
return newPort
}
else {
// user changed port, check that it is free
return checkPort(newPort, process, defaultPort)
}
}
Loading

0 comments on commit 6cc92da

Please sign in to comment.