From e0dcad09a526d074b16bc535313a7895e74716ae Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Wed, 8 Jan 2025 18:36:28 -0500 Subject: [PATCH 01/20] rough in shopify support --- fly.js | 68 ++++++++++++- gdf.js | 24 ++++- index.js | 4 + templates/docker-entrypoint.ejs | 96 +++++++++++-------- test/base/defer-build/docker-entrypoint.js | 3 + .../nuxt-prisma/docker-entrypoint.js | 3 + .../remix-epic/docker-entrypoint.js | 14 ++- .../svelte-prisma/docker-entrypoint.js | 27 +++--- 8 files changed, 176 insertions(+), 63 deletions(-) diff --git a/fly.js b/fly.js index 74b6fb3..b2ddbf7 100755 --- a/fly.js +++ b/fly.js @@ -15,6 +15,10 @@ GDF.extend(class extends GDF { // create volume for sqlite3 if (this.sqlite3) this.flyMakeVolume() + if (this.setupScriptType === 'dbsetup') { + this.flySetCmd() + } + // setup swap if (this.options.swap != null) this.flySetSwap() @@ -22,7 +26,10 @@ GDF.extend(class extends GDF { if (this.litefs) this.flyAttachConsul(this.flyApp) // set secrets, healthcheck for remix apps - if (this.remix) { + if (this.shopify) { + this.flyShopifyEnv(this.flyApp) + this.flyShopifyConfig(this.flyApp) + } else if (this.remix) { this.flyRemixSecrets(this.flyApp) this.flyHealthCheck('/healthcheck') } @@ -62,7 +69,7 @@ GDF.extend(class extends GDF { this.flyToml = fs.readFileSync(this.flyTomlFile, 'utf-8') // parse app name from fly.toml - this.flyApp = this.flyToml.match(/^app\s*=\s*"?([-\w]+)"?/m)?.[1] + this.flyApp = this.flyToml.match(/^app\s*=\s*["']?([-\w]+)["']?/m)?.[1] // see if flyctl is in the path const paths = (process.env.PATH || '') @@ -159,6 +166,27 @@ GDF.extend(class extends GDF { } } + // override command in fly.toml to include dbsetup.js + flySetCmd() { + if (this.flyToml.includes('[processes]')) return + + let cmd = this.startCommand + + const dockerfile = fs.readFileSync('Dockerfile', 'utf8') + + const match = dockerfile.match(/^\s*CMD\s+(\[.*\]|".*")/mi) + if (match) { + try { + cmd = JSON.parse(match[1]) + } catch { } + } + + if (Array.isArray(cmd)) cmd = cmd.join(' ') + cmd = `${this.bun ? 'bun' : 'node'} ./dbsetup.js ${cmd}` + this.flyToml += `\n[processes]\n app = ${JSON.stringify(cmd)}\n` + fs.writeFileSync(this.flyTomlFile, this.flyToml) + } + // add volume to fly.toml and create it if app exists flyAttachConsul(app) { if (!app) return @@ -283,6 +311,42 @@ GDF.extend(class extends GDF { }) } + // set environment and secrets for Shopify apps + flyShopifyEnv(app) { + const env = { + PORT: 3000, + SHOPIFY_APP_URL: `https://${app}.fly.dev` + } + + try { + const stdout = execSync('shopify app env show', { encoding: 'utf8' }) + for (const match of stdout.matchAll(/^\s*(\w+)=(.*)/mg)) { + if (match[1] === 'SHOPIFY_API_SECRET') { + console.log(`${chalk.bold.green('execute'.padStart(11))} flyctl secrets set SHOPIFY_API_SECRET`) + execSync(`${this.flyctl} secrets set SHOPIFY_API_SECRET=${match[2]} --app ${app}`, { stdio: 'inherit' }) + } else { + env[match[1]] = match[2] + } + } + } catch { } + + if (this.flyToml.includes('[env]')) return + this.flyToml += '\n[env]\n' + Object.entries(env).map(([key, value]) => ` ${key} = ${JSON.stringify(value)}`).join('\n') + '\n' + fs.writeFileSync(this.flyTomlFile, this.flyToml) + } + + // update config for Shopify apps + flyShopifyConfig(app) { + const original = fs.readFileSync('shopify.app.toml', 'utf-8') + const config = original.replaceAll(/"https:\/\/[-\w.]+/g, `"https://${app}.fly.dev`) + if (original !== config) { + console.log(`${chalk.bold.green('update'.padStart(11, ' '))} shopify.app.toml`) + fs.writeFileSync('shopify.app.toml', config) + console.log(`${chalk.bold.green('execute'.padStart(11))} shopify app deploy`) + execSync('shopify app deploy', { stdio: 'inherit' }) + } + } + // prep for deployment via GitHub actions, including setting up a staging app flyGitHubPrep() { const deploy = fs.readFileSync('.github/workflows/deploy.yml', 'utf-8') diff --git a/gdf.js b/gdf.js index 5f25176..91a0edf 100755 --- a/gdf.js +++ b/gdf.js @@ -82,6 +82,9 @@ export class GDF { // exit code #exitCode = 0 + // dockerfile exists at the time of invocation + #dockerfileExists = false + get variant() { return this.options.alpine ? 'alpine' : 'slim' } @@ -147,6 +150,11 @@ export class GDF { this.#pj.dependencies?.['@remix-run/node']) } + // Does this application use shopify? + get shopify() { + return fs.existsSync(path.join(this._appdir, 'shopify.app.toml')) + } + // Is this an EpicStack application? get epicStack() { return !!this.#pj['epic-stack'] @@ -934,6 +942,7 @@ export class GDF { this.options = options this._appdir = appdir this.#pj = JSON.parse(fs.readFileSync(path.join(appdir, 'package.json'), 'utf-8')) + this.#dockerfileExists = fs.existsSync(path.join(appdir, 'Dockerfile')) // backwards compatibility with previous definition of --build=defer if (options.build === 'defer') { @@ -967,7 +976,11 @@ export class GDF { } if (this.entrypoint) { - templates['docker-entrypoint.ejs'] = `${this.configDir}docker-entrypoint.js` + if (!this.#dockerfileExists) { + templates['docker-entrypoint.ejs'] = `${this.configDir}docker-entrypoint.js` + } else if (this.options.skip && fs.existsSync(path.join(appdir, 'fly.toml'))) { + templates['docker-entrypoint.ejs'] = `${this.configDir}dbsetup.js` + } } if (this.litefs) { @@ -1028,7 +1041,11 @@ export class GDF { runner.apply(this) } - process.exit(this.#exitCode) + if (this.#exitCode) process.exit(this.#exitCode) + } + + get setupScriptType() { + return (this.options.skip && this.#dockerfileExists) ? 'dbsetup' : 'docker' } setExit(code) { @@ -1046,6 +1063,9 @@ export class GDF { if (current === proposed) { console.log(`${chalk.bold.blue('identical'.padStart(11))} ${name}`) return dest + } else if (this.options.skip) { + console.log(`${chalk.bold.yellow('skip'.padStart(11))} ${name}`) + return current } let prompt diff --git a/index.js b/index.js index 3256609..512c5b8 100755 --- a/index.js +++ b/index.js @@ -89,6 +89,10 @@ const options = yargs((hideBin(process.argv))) describe: 'expose port', type: 'integer' }) + .option('skip', { + describe: 'skip overwrite of existing files', + type: 'boolean' + }) .option('swap', { alias: 's', describe: 'allocate swap space (eg. 1G, 1GiB, 1024M)', diff --git a/templates/docker-entrypoint.ejs b/templates/docker-entrypoint.ejs index 9271ca6..c361132 100755 --- a/templates/docker-entrypoint.ejs +++ b/templates/docker-entrypoint.ejs @@ -42,76 +42,96 @@ if (process.env.DATABASE_URL) { } <% } -%> +<% n = typeModule ? 0 : 1 -%> +<% if (!typeModule) { -%> ;(async() => { +<% } -%> <% if (options.swap && !flySetup()) { -%> -<%= tab(1) %>// allocate swap space -<%= tab(1) %>await exec('fallocate -l <%= options.swap %> /swapfile') -<%= tab(1) %>await exec('chmod 0600 /swapfile') -<%= tab(1) %>await exec('mkswap /swapfile') -<%= tab(1) %>writeFileSync('/proc/sys/vm/swappiness', '10') -<%= tab(1) %>await exec('swapon /swapfile') -<%= tab(1) %>writeFileSync('/proc/sys/vm/overcommit_memory', '1') +<%= tab(n) %>// allocate swap space +<%= tab(n) %>await exec('fallocate -l <%= options.swap %> /swapfile') +<%= tab(n) %>await exec('chmod 0600 /swapfile') +<%= tab(n) %>await exec('mkswap /swapfile') +<%= tab(n) %>writeFileSync('/proc/sys/vm/swappiness', '10') +<%= tab(n) %>await exec('swapon /swapfile') +<%= tab(n) %>writeFileSync('/proc/sys/vm/overcommit_memory', '1') <% } -%> <% if (prisma || (build && options.deferBuild) || nextjsGeneration) { -%> +<% if (setupScriptType == 'docker') { -%> <% if (prisma && sqlite3) { -%> -<%= tab(1) %>// If running the web server then migrate existing database +<%= tab(n) %>// If running the web server then migrate existing database <% } else { -%> -<%= tab(1) %>// If running the web server then prerender pages +<%= tab(n) %>// If running the web server then prerender pages <% } -%> -<%= tab(1) %>if (process.argv.slice(2).join(' ') === '<%- +<%= tab(n) %>if (process.argv.slice(2).join(' ') === '<%- Array.isArray(startCommand) ? startCommand.join(" ") : startCommand %>'<% if (litefs) { %> && process.env.FLY_REGION === process.env.PRIMARY_REGION<% } %>) { +<% n++ -%> +<% } -%> <% if (prisma) { -%> <% if (prismaFile) { -%> -<%= tab(2) %><%= nuxtjs ? 'let' : 'const' %> source = path.resolve('<%- prismaFile %>') -<%= tab(2) %>const target = '/data/' + path.basename(source) -<%= tab(2) %>if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) +<%= tab(n) %>// place Sqlite3 database on volume +<%= tab(n) %><%= nuxtjs ? 'let' : 'const' %> source = path.resolve('<%- prismaFile %>') +<%= tab(n) %>const target = '/data/' + path.basename(source) +<%= tab(n) %>if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) <% if (nuxtjs) { -%> -<%= tab(2) %>source = path.resolve('./.output/server', '<%- prismaFile %>') -<%= tab(2) %>if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) +<%= tab(n) %>source = path.resolve('./.output/server', '<%- prismaFile %>') +<%= tab(n) %>if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) <% } -%> <% } else if (prismaSeed && sqlite3 && prismaEnv) { -%> -<%= tab(2) %>const url = new URL(process.env.<%= prismaEnv %>) -<%= tab(2) %>const target = url.protocol === 'file:' && url.pathname -<%= tab(2) %><%= litestream ? 'let' : 'const' %> newDb = target && !fs.existsSync(target) +<%= tab(n) %>const url = new URL(process.env.<%= prismaEnv %>) +<%= tab(n) %>const target = url.protocol === 'file:' && url.pathname +<% if (litestream && sqlite3 && (prismaFile || prismaEnv)) { -%> + +<%= tab(n) %>// restore database if not present and replica exists <% } -%> -<% if (prismaFile && prismaSeed && sqlite3) { -%> -<%= tab(2) %><%= litestream ? 'let' : 'const' %> newDb = !fs.existsSync(target) <% } -%> -<% if (litestream && prismaSeed && sqlite3 && (prismaFile || prismaEnv)) { -%> -<%= tab(2) %>if (newDb && process.env.BUCKET_NAME) { -<%= tab(3) %>await exec(`litestream restore -config litestream.yml -if-replica-exists ${target}`) -<%= tab(3) %>newDb = !fs.existsSync(target) -<%= tab(2) %>} +<% if (sqlite3 && (prismaSeed || litestream) && (prismaFile || prismaEnv)) { -%> +<%= tab(n) %><%= litestream ? 'let' : 'const' %> newDb = <%- !prismaFile ? 'target && ' : '' %>!fs.existsSync(target) <% } -%> -<% if (sqlite3) { -%> -<%= tab(2) %>await exec('<%= npx %> prisma migrate deploy') +<% if (litestream && sqlite3 && (prismaFile || prismaEnv)) { -%> +<%= tab(n) %>if (newDb && process.env.BUCKET_NAME) { +<%= tab(n+1) %>await exec(`litestream restore -config litestream.yml -if-replica-exists ${target}`) +<% if (prismaSeed && sqlite3 && (prismaFile || prismaEnv)) { -%> +<%= tab(n+1) %>newDb = !fs.existsSync(target) +<% } -%> +<%= tab(n) %>} +<% } -%> +<% if (sqlite3 && !shopify) { -%> +<% if (prismaFile) { -%> + +<%= tab(n) %>// prepare database +<% } -%> +<%= tab(n) %>await exec('<%= npx %> prisma migrate deploy') <% } -%> <% if (prismaSeed && sqlite3 && (prismaFile || prismaEnv)) { -%> -<%= tab(2) %>if (newDb) await exec('npx prisma db seed') +<%= tab(n) %>if (newDb) await exec('npx prisma db seed') <% } -%> <% } -%> <% if (nextjsGeneration) { -%> -<%= tab(2) %>await exec('<%= npx %> next build --experimental-build-mode generate') +<%= tab(n) %>await exec('<%= npx %> next build --experimental-build-mode generate') <% } -%> <% if (build && options.deferBuild) { -%> -<%= tab(2) %>await exec('<%= packager %> run build') +<%= tab(n) %>await exec('<%= packager %> run build') +<% } -%> +<% if (setupScriptType == 'docker') { -%> +<%= tab(--n) %>} <% } -%> -<%= tab(1) %>} <% } -%> -<%= tab(1) %>// launch application +<%= tab(n) %>// launch application <% if (litestream) { -%> -<%= tab(1) %>if (process.env.BUCKET_NAME) { -<%= tab(2) %>await exec(`litestream replicate -config litestream.yml -exec ${JSON.stringify(process.argv.slice(2).join(' '))}`) -<%= tab(1) %>} else { -<%= tab(2) %>await exec(process.argv.slice(2).join(' ')) -<%= tab(1) %>} +<%= tab(n) %>if (process.env.BUCKET_NAME) { +<%= tab(n+1) %>await exec(`litestream replicate -config litestream.yml -exec ${JSON.stringify(process.argv.slice(2).join(' '))}`) +<%= tab(n) %>} else { +<%= tab(n+1) %>await exec(process.argv.slice(2).join(' ')) +<%= tab(n) %>} <% } else { -%> -<%= tab(1) %>await exec(process.argv.slice(2).join(' ')) +<%= tab(n) %>await exec(process.argv.slice(2).join(' ')) <% } -%> +<% if (!typeModule) { -%> })() +<% } -%> <%= tab(0) %>function exec(command) { <%= tab(1) %>const child = spawn(command, { shell: true, stdio: 'inherit', env }) diff --git a/test/base/defer-build/docker-entrypoint.js b/test/base/defer-build/docker-entrypoint.js index 19674de..61392d4 100755 --- a/test/base/defer-build/docker-entrypoint.js +++ b/test/base/defer-build/docker-entrypoint.js @@ -9,9 +9,12 @@ const env = { ...process.env } ;(async() => { // If running the web server then migrate existing database if (process.argv.slice(2).join(' ') === 'npm run start') { + // place Sqlite3 database on volume const source = path.resolve('./dev.db') const target = '/data/' + path.basename(source) if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) + + // prepare database await exec('npx prisma migrate deploy') await exec('npm run build') } diff --git a/test/frameworks/nuxt-prisma/docker-entrypoint.js b/test/frameworks/nuxt-prisma/docker-entrypoint.js index 814e38e..95e8329 100755 --- a/test/frameworks/nuxt-prisma/docker-entrypoint.js +++ b/test/frameworks/nuxt-prisma/docker-entrypoint.js @@ -9,6 +9,7 @@ const env = { ...process.env } ;(async() => { // If running the web server then migrate existing database if (process.argv.slice(2).join(' ') === 'node .output/server/index.mjs') { + // place Sqlite3 database on volume let source = path.resolve('./dev.db') const target = '/data/' + path.basename(source) if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) @@ -19,6 +20,8 @@ const env = { ...process.env } await exec(`litestream restore -config litestream.yml -if-replica-exists ${target}`) newDb = !fs.existsSync(target) } + + // prepare database await exec('npx prisma migrate deploy') if (newDb) await exec('npx prisma db seed') } diff --git a/test/frameworks/remix-epic/docker-entrypoint.js b/test/frameworks/remix-epic/docker-entrypoint.js index 1216dcc..89c096e 100755 --- a/test/frameworks/remix-epic/docker-entrypoint.js +++ b/test/frameworks/remix-epic/docker-entrypoint.js @@ -4,15 +4,13 @@ import { spawn } from 'node:child_process' const env = { ...process.env } -;(async() => { - // If running the web server then migrate existing database - if (process.argv.slice(2).join(' ') === 'npm run start' && process.env.FLY_REGION === process.env.PRIMARY_REGION) { - await exec('npx prisma migrate deploy') - } +// If running the web server then migrate existing database +if (process.argv.slice(2).join(' ') === 'npm run start' && process.env.FLY_REGION === process.env.PRIMARY_REGION) { + await exec('npx prisma migrate deploy') +} - // launch application - await exec(process.argv.slice(2).join(' ')) -})() +// launch application +await exec(process.argv.slice(2).join(' ')) function exec(command) { const child = spawn(command, { shell: true, stdio: 'inherit', env }) diff --git a/test/frameworks/svelte-prisma/docker-entrypoint.js b/test/frameworks/svelte-prisma/docker-entrypoint.js index 1437720..87844c8 100755 --- a/test/frameworks/svelte-prisma/docker-entrypoint.js +++ b/test/frameworks/svelte-prisma/docker-entrypoint.js @@ -6,20 +6,21 @@ import fs from 'node:fs' const env = { ...process.env } -;(async() => { - // If running the web server then migrate existing database - if (process.argv.slice(2).join(' ') === 'node ./build/index.js') { - const source = path.resolve('./dev.db') - const target = '/data/' + path.basename(source) - if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) - const newDb = !fs.existsSync(target) - await exec('npx prisma migrate deploy') - if (newDb) await exec('npx prisma db seed') - } +// If running the web server then migrate existing database +if (process.argv.slice(2).join(' ') === 'node ./build/index.js') { + // place Sqlite3 database on volume + const source = path.resolve('./dev.db') + const target = '/data/' + path.basename(source) + if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) + const newDb = !fs.existsSync(target) - // launch application - await exec(process.argv.slice(2).join(' ')) -})() + // prepare database + await exec('npx prisma migrate deploy') + if (newDb) await exec('npx prisma db seed') +} + +// launch application +await exec(process.argv.slice(2).join(' ')) function exec(command) { const child = spawn(command, { shell: true, stdio: 'inherit', env }) From 2f67fc6ddd70b87e305bf9145ab890227ebb703f Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Wed, 8 Jan 2025 19:43:52 -0500 Subject: [PATCH 02/20] add --force option --- fly.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fly.js b/fly.js index b2ddbf7..3b62f87 100755 --- a/fly.js +++ b/fly.js @@ -342,8 +342,8 @@ GDF.extend(class extends GDF { if (original !== config) { console.log(`${chalk.bold.green('update'.padStart(11, ' '))} shopify.app.toml`) fs.writeFileSync('shopify.app.toml', config) - console.log(`${chalk.bold.green('execute'.padStart(11))} shopify app deploy`) - execSync('shopify app deploy', { stdio: 'inherit' }) + console.log(`${chalk.bold.green('execute'.padStart(11))} shopify app deploy --force`) + execSync('shopify app deploy --force', { stdio: 'inherit' }) } } From 52622df631ebd97c652df898a4c38ed1e76a89fa Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Wed, 8 Jan 2025 20:04:35 -0500 Subject: [PATCH 03/20] make force override skip --- index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.js b/index.js index 512c5b8..b90302f 100755 --- a/index.js +++ b/index.js @@ -338,4 +338,6 @@ if (pj) { } } +if (options.force) options.skip = false + new GDF().run(process.cwd(), { ...defaults, ...options }) From ceeeb1efaaf7630edff7fd8dc702e9a5cd287ddd Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Wed, 8 Jan 2025 20:11:37 -0500 Subject: [PATCH 04/20] create docker-entrypoint.ejs if --force is specified --- gdf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gdf.js b/gdf.js index 91a0edf..ebdd500 100755 --- a/gdf.js +++ b/gdf.js @@ -976,7 +976,7 @@ export class GDF { } if (this.entrypoint) { - if (!this.#dockerfileExists) { + if (!this.#dockerfileExists || this.options.force) { templates['docker-entrypoint.ejs'] = `${this.configDir}docker-entrypoint.js` } else if (this.options.skip && fs.existsSync(path.join(appdir, 'fly.toml'))) { templates['docker-entrypoint.ejs'] = `${this.configDir}dbsetup.js` From f6e98cc9f50f46fcd07a34eaa3fb3a98bca93035 Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Wed, 8 Jan 2025 20:20:16 -0500 Subject: [PATCH 05/20] if using our dockerfile, run migrate --- templates/docker-entrypoint.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/docker-entrypoint.ejs b/templates/docker-entrypoint.ejs index c361132..d30b225 100755 --- a/templates/docker-entrypoint.ejs +++ b/templates/docker-entrypoint.ejs @@ -97,7 +97,7 @@ if (process.env.DATABASE_URL) { <% } -%> <%= tab(n) %>} <% } -%> -<% if (sqlite3 && !shopify) { -%> +<% if (sqlite3 && setupScriptType == 'docker') { -%> <% if (prismaFile) { -%> <%= tab(n) %>// prepare database From 1330133baaf8c3205ddda719056cac4870a62a51 Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Thu, 9 Jan 2025 10:48:52 -0500 Subject: [PATCH 06/20] automatically run shopify app config link when needed --- fly.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/fly.js b/fly.js index 3b62f87..93d9d87 100755 --- a/fly.js +++ b/fly.js @@ -313,12 +313,23 @@ GDF.extend(class extends GDF { // set environment and secrets for Shopify apps flyShopifyEnv(app) { + let toml = "" + if (fs.existsSync('shopify.app.toml')) { + toml = fs.readFileSync('shopify.app.toml', 'utf-8') + } + + if (!toml.includes('client_id')) { + console.log(`${chalk.bold.green('execute'.padStart(11))} shopify app config create`) + execSync('shopify app config link', { encoding: 'utf8' }) + } + const env = { PORT: 3000, SHOPIFY_APP_URL: `https://${app}.fly.dev` } try { + console.log(`${chalk.bold.green('execute'.padStart(11))} shopify app env show`) const stdout = execSync('shopify app env show', { encoding: 'utf8' }) for (const match of stdout.matchAll(/^\s*(\w+)=(.*)/mg)) { if (match[1] === 'SHOPIFY_API_SECRET') { From ab24608eacbab98eda55fec7c5e17c74c40add1f Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Thu, 9 Jan 2025 10:55:35 -0500 Subject: [PATCH 07/20] stdio: 'inherit' --- fly.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fly.js b/fly.js index 93d9d87..2540de5 100755 --- a/fly.js +++ b/fly.js @@ -320,7 +320,7 @@ GDF.extend(class extends GDF { if (!toml.includes('client_id')) { console.log(`${chalk.bold.green('execute'.padStart(11))} shopify app config create`) - execSync('shopify app config link', { encoding: 'utf8' }) + execSync('shopify app config link', { encoding: 'utf8', stdio: 'inherit'}) } const env = { From e7fa77bec00f0f8f3c53bb595ca9376618818f8c Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Thu, 9 Jan 2025 11:05:53 -0500 Subject: [PATCH 08/20] shopify app config create must be run interactively TODO: try pty later? --- fly.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fly.js b/fly.js index 2540de5..84d2d7b 100755 --- a/fly.js +++ b/fly.js @@ -319,8 +319,9 @@ GDF.extend(class extends GDF { } if (!toml.includes('client_id')) { - console.log(`${chalk.bold.green('execute'.padStart(11))} shopify app config create`) - execSync('shopify app config link', { encoding: 'utf8', stdio: 'inherit'}) + this.setExit(42) + console.log(`${chalk.bold.red('shopify.app.toml')} is not complete; run ${chalk.bold.blue('shopify app config create')} first.`) + return } const env = { From 4e90d558c37ec60c4b3650249d27fe4b4376dd01 Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Thu, 9 Jan 2025 11:07:12 -0500 Subject: [PATCH 09/20] appease elint --- fly.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fly.js b/fly.js index 84d2d7b..f212828 100755 --- a/fly.js +++ b/fly.js @@ -313,7 +313,7 @@ GDF.extend(class extends GDF { // set environment and secrets for Shopify apps flyShopifyEnv(app) { - let toml = "" + let toml = '' if (fs.existsSync('shopify.app.toml')) { toml = fs.readFileSync('shopify.app.toml', 'utf-8') } From 6bf521cb1f2a4a6f0c65c6dd0236673d819c3022 Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Fri, 10 Jan 2025 09:12:17 -0500 Subject: [PATCH 10/20] check to see if CMD ends with specified command useful if there are entrypoints --- templates/docker-entrypoint.ejs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/docker-entrypoint.ejs b/templates/docker-entrypoint.ejs index d30b225..60bc261 100755 --- a/templates/docker-entrypoint.ejs +++ b/templates/docker-entrypoint.ejs @@ -63,7 +63,9 @@ if (process.env.DATABASE_URL) { <% } else { -%> <%= tab(n) %>// If running the web server then prerender pages <% } -%> -<%= tab(n) %>if (process.argv.slice(2).join(' ') === '<%- +<%= tab(n) %>if (process.argv.slice(-<%= + Array.isArray(startCommand) ? startCommand.length : startCommand.split(" ").length + %>).join(' ') === '<%- Array.isArray(startCommand) ? startCommand.join(" ") : startCommand %>'<% if (litefs) { %> && process.env.FLY_REGION === process.env.PRIMARY_REGION<% } %>) { <% n++ -%> From 91f677bc66b90a31dda18d3986553b71b1ef6c97 Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Fri, 10 Jan 2025 09:33:29 -0500 Subject: [PATCH 11/20] replace shopify redirect_urls --- fly.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fly.js b/fly.js index f212828..9972753 100755 --- a/fly.js +++ b/fly.js @@ -350,7 +350,10 @@ GDF.extend(class extends GDF { // update config for Shopify apps flyShopifyConfig(app) { const original = fs.readFileSync('shopify.app.toml', 'utf-8') - const config = original.replaceAll(/"https:\/\/[-\w.]+/g, `"https://${app}.fly.dev`) + const url = `"https://${app}.fly.dev` + const config = original.replaceAll(/"https:\/\/[-\w.]+/g, url) + .replace(/(redirect_urls\s*=\s*\[).*?\]/s, + `$1\n ${url}/auth/callback\n ${url}/auth/shopify/callback\n ${url}/api/auth/callback\n]`) if (original !== config) { console.log(`${chalk.bold.green('update'.padStart(11, ' '))} shopify.app.toml`) fs.writeFileSync('shopify.app.toml', config) From 4780c503cc0bf96a9c938b9b280d9049187209a4 Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Fri, 10 Jan 2025 09:38:42 -0500 Subject: [PATCH 12/20] match quotes --- fly.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fly.js b/fly.js index 9972753..bcb81b1 100755 --- a/fly.js +++ b/fly.js @@ -350,10 +350,10 @@ GDF.extend(class extends GDF { // update config for Shopify apps flyShopifyConfig(app) { const original = fs.readFileSync('shopify.app.toml', 'utf-8') - const url = `"https://${app}.fly.dev` - const config = original.replaceAll(/"https:\/\/[-\w.]+/g, url) + const url = `https://${app}.fly.dev` + const config = original.replaceAll(/"https:\/\/[-\w.]+/g, '"' + url) .replace(/(redirect_urls\s*=\s*\[).*?\]/s, - `$1\n ${url}/auth/callback\n ${url}/auth/shopify/callback\n ${url}/api/auth/callback\n]`) + `$1\n "${url}/auth/callback"\n "${url}/auth/shopify/callback"\n "${url}/api/auth/callback"\n]`) if (original !== config) { console.log(`${chalk.bold.green('update'.padStart(11, ' '))} shopify.app.toml`) fs.writeFileSync('shopify.app.toml', config) From fbf0926e4bf2fadec685b6fcde2ff5d0309fc50a Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Fri, 10 Jan 2025 09:40:32 -0500 Subject: [PATCH 13/20] commas too --- fly.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fly.js b/fly.js index bcb81b1..9e5383c 100755 --- a/fly.js +++ b/fly.js @@ -353,7 +353,7 @@ GDF.extend(class extends GDF { const url = `https://${app}.fly.dev` const config = original.replaceAll(/"https:\/\/[-\w.]+/g, '"' + url) .replace(/(redirect_urls\s*=\s*\[).*?\]/s, - `$1\n "${url}/auth/callback"\n "${url}/auth/shopify/callback"\n "${url}/api/auth/callback"\n]`) + `$1\n "${url}/auth/callback",\n "${url}/auth/shopify/callback",\n "${url}/api/auth/callback"\n]`) if (original !== config) { console.log(`${chalk.bold.green('update'.padStart(11, ' '))} shopify.app.toml`) fs.writeFileSync('shopify.app.toml', config) From b66d8c59027a3a89c79b908dd3b711b5baeb93ab Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Sat, 11 Jan 2025 10:59:32 -0500 Subject: [PATCH 14/20] resync tests --- test/base/defer-build/docker-entrypoint.js | 2 +- test/base/windows/docker-entrypoint.js | 2 +- test/frameworks/nuxt-prisma/docker-entrypoint.js | 2 +- test/frameworks/remix-epic/docker-entrypoint.js | 2 +- test/frameworks/remix-indie/docker-entrypoint.js | 2 +- test/frameworks/svelte-prisma/docker-entrypoint.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/base/defer-build/docker-entrypoint.js b/test/base/defer-build/docker-entrypoint.js index 61392d4..b1103fa 100755 --- a/test/base/defer-build/docker-entrypoint.js +++ b/test/base/defer-build/docker-entrypoint.js @@ -8,7 +8,7 @@ const env = { ...process.env } ;(async() => { // If running the web server then migrate existing database - if (process.argv.slice(2).join(' ') === 'npm run start') { + if (process.argv.slice(-3).join(' ') === 'npm run start') { // place Sqlite3 database on volume const source = path.resolve('./dev.db') const target = '/data/' + path.basename(source) diff --git a/test/base/windows/docker-entrypoint.js b/test/base/windows/docker-entrypoint.js index b52ac96..0b7e348 100755 --- a/test/base/windows/docker-entrypoint.js +++ b/test/base/windows/docker-entrypoint.js @@ -7,7 +7,7 @@ const env = { ...process.env } ;(async() => { // If running the web server then migrate existing database - if (process.argv.slice(2).join(' ') === 'npx remix-serve ./build/index.js') { + if (process.argv.slice(-3).join(' ') === 'npx remix-serve ./build/index.js') { const url = new URL(process.env.DATABASE_URL) const target = url.protocol === 'file:' && url.pathname const newDb = target && !fs.existsSync(target) diff --git a/test/frameworks/nuxt-prisma/docker-entrypoint.js b/test/frameworks/nuxt-prisma/docker-entrypoint.js index 95e8329..a638041 100755 --- a/test/frameworks/nuxt-prisma/docker-entrypoint.js +++ b/test/frameworks/nuxt-prisma/docker-entrypoint.js @@ -8,7 +8,7 @@ const env = { ...process.env } ;(async() => { // If running the web server then migrate existing database - if (process.argv.slice(2).join(' ') === 'node .output/server/index.mjs') { + if (process.argv.slice(-2).join(' ') === 'node .output/server/index.mjs') { // place Sqlite3 database on volume let source = path.resolve('./dev.db') const target = '/data/' + path.basename(source) diff --git a/test/frameworks/remix-epic/docker-entrypoint.js b/test/frameworks/remix-epic/docker-entrypoint.js index 89c096e..6e48f89 100755 --- a/test/frameworks/remix-epic/docker-entrypoint.js +++ b/test/frameworks/remix-epic/docker-entrypoint.js @@ -5,7 +5,7 @@ import { spawn } from 'node:child_process' const env = { ...process.env } // If running the web server then migrate existing database -if (process.argv.slice(2).join(' ') === 'npm run start' && process.env.FLY_REGION === process.env.PRIMARY_REGION) { +if (process.argv.slice(-3).join(' ') === 'npm run start' && process.env.FLY_REGION === process.env.PRIMARY_REGION) { await exec('npx prisma migrate deploy') } diff --git a/test/frameworks/remix-indie/docker-entrypoint.js b/test/frameworks/remix-indie/docker-entrypoint.js index b52ac96..0b7e348 100755 --- a/test/frameworks/remix-indie/docker-entrypoint.js +++ b/test/frameworks/remix-indie/docker-entrypoint.js @@ -7,7 +7,7 @@ const env = { ...process.env } ;(async() => { // If running the web server then migrate existing database - if (process.argv.slice(2).join(' ') === 'npx remix-serve ./build/index.js') { + if (process.argv.slice(-3).join(' ') === 'npx remix-serve ./build/index.js') { const url = new URL(process.env.DATABASE_URL) const target = url.protocol === 'file:' && url.pathname const newDb = target && !fs.existsSync(target) diff --git a/test/frameworks/svelte-prisma/docker-entrypoint.js b/test/frameworks/svelte-prisma/docker-entrypoint.js index 87844c8..533caa3 100755 --- a/test/frameworks/svelte-prisma/docker-entrypoint.js +++ b/test/frameworks/svelte-prisma/docker-entrypoint.js @@ -7,7 +7,7 @@ import fs from 'node:fs' const env = { ...process.env } // If running the web server then migrate existing database -if (process.argv.slice(2).join(' ') === 'node ./build/index.js') { +if (process.argv.slice(-2).join(' ') === 'node ./build/index.js') { // place Sqlite3 database on volume const source = path.resolve('./dev.db') const target = '/data/' + path.basename(source) From 60efe5da3f52fe28a6a5cc5bcc1ff1c1b2181824 Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Sat, 11 Jan 2025 13:22:42 -0500 Subject: [PATCH 15/20] install and run litestream from npm if Dockerfile exists --- bun.lockb | Bin 149416 -> 150734 bytes gdf.js | 6 +++- package-lock.json | 55 ++++++++++++++++++++++++++++++-- package.json | 1 + templates/docker-entrypoint.ejs | 2 +- 5 files changed, 60 insertions(+), 4 deletions(-) diff --git a/bun.lockb b/bun.lockb index 7e4957390e37af9a6d55235f886cc492223a232c..4cf4b614d0ecf7a3a2849249a70c014e464651b0 100755 GIT binary patch delta 26524 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 delta 25798 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 CNN2YI diff --git a/gdf.js b/gdf.js index ebdd500..1f99c75 100755 --- a/gdf.js +++ b/gdf.js @@ -641,6 +641,10 @@ export class GDF { modules.push('@sveltejs/adapter-node') } + if (this.litestream && !this.#pj.dependencies?.['@flydotio/litestream']) { + modules.push('@flydotio/litestream') + } + if (modules.length === 0) return const add = this.packager === 'npm' ? 'install' : 'add' for (const module of modules) { @@ -978,7 +982,7 @@ export class GDF { if (this.entrypoint) { if (!this.#dockerfileExists || this.options.force) { templates['docker-entrypoint.ejs'] = `${this.configDir}docker-entrypoint.js` - } else if (this.options.skip && fs.existsSync(path.join(appdir, 'fly.toml'))) { + } else if (this.setupScriptType === 'dbsetup') { templates['docker-entrypoint.ejs'] = `${this.configDir}dbsetup.js` } } diff --git a/package-lock.json b/package-lock.json index 29e7251..4d3a6bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "@flydotio/dockerfile", - "version": "0.5.9", + "version": "0.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@flydotio/dockerfile", - "version": "0.5.9", + "version": "0.6.1", "license": "MIT", "dependencies": { + "@flydotio/litestream": "^1.0.0", "@sveltejs/adapter-node": "^5.2.11", "chalk": "^5.3.0", "diff": "^5.2.0", @@ -516,6 +517,56 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@flydotio/litestream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@flydotio/litestream/-/litestream-1.0.0.tgz", + "integrity": "sha512-RiJCIrQEZ7e93L7/urBfyxdCc13f/3eN/Ik/cYuBNnvEkefRSrJ9uZPJqBgR9MxzXTaLeSh/TZ49M4F26HYKtw==", + "license": "MIT", + "bin": { + "atc": "index.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@flydotio/litestream-darwin-arm64": "1.0.0", + "@flydotio/litestream-darwin-x64": "1.0.0", + "@flydotio/litestream-linux-arm64": "1.0.0", + "@flydotio/litestream-linux-x64": "1.0.0" + } + }, + "node_modules/@flydotio/litestream-darwin-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@flydotio/litestream-darwin-arm64/-/litestream-darwin-arm64-1.0.0.tgz", + "integrity": "sha512-07GwYSCGuPx2BWaKQCbY+/bWq27XvJDRdVSxdg96QKtu5UMmgJ5x71mBxabqKJ9b0oVD2qGa/9ilIIlDwi6vPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@flydotio/litestream-linux-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@flydotio/litestream-linux-arm64/-/litestream-linux-arm64-1.0.0.tgz", + "integrity": "sha512-QEXw1H2dOS+AA2dDG7W6rvnJZW3cDFjM0yKo0tv1pBcE8DPi+0PJp9Fdx7r34LQx2DTX3yRWycTUAqJSxrFhhg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", diff --git a/package.json b/package.json index 27b5acb..b900a43 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "node": ">=16.0.0" }, "dependencies": { + "@flydotio/litestream": "^1.0.0", "@sveltejs/adapter-node": "^5.2.11", "chalk": "^5.3.0", "diff": "^5.2.0", diff --git a/templates/docker-entrypoint.ejs b/templates/docker-entrypoint.ejs index 60bc261..e744ba0 100755 --- a/templates/docker-entrypoint.ejs +++ b/templates/docker-entrypoint.ejs @@ -124,7 +124,7 @@ if (process.env.DATABASE_URL) { <%= tab(n) %>// launch application <% if (litestream) { -%> <%= tab(n) %>if (process.env.BUCKET_NAME) { -<%= tab(n+1) %>await exec(`litestream replicate -config litestream.yml -exec ${JSON.stringify(process.argv.slice(2).join(' '))}`) +<%= tab(n+1) %>await exec(`<% if (setupScriptType == 'dbsetup') { %>npx <% } %>litestream replicate -config litestream.yml -exec ${JSON.stringify(process.argv.slice(2).join(' '))}`) <%= tab(n) %>} else { <%= tab(n+1) %>await exec(process.argv.slice(2).join(' ')) <%= tab(n) %>} From 8473338c86b8b157147c0e4f624fabe4f7456d54 Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Sat, 11 Jan 2025 14:04:29 -0500 Subject: [PATCH 16/20] npx needed for litestream restore too --- package-lock.json | 32 ++++++++++++++++++++++++++++++++ templates/docker-entrypoint.ejs | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 4d3a6bc..5c9c661 100644 --- a/package-lock.json +++ b/package-lock.json @@ -551,6 +551,22 @@ "node": ">=18" } }, + "node_modules/@flydotio/litestream-darwin-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@flydotio/litestream-darwin-x64/-/litestream-darwin-x64-1.0.0.tgz", + "integrity": "sha512-rvX/o3SFFoqpOP2wX7fVDKYqHV6ZX5IWjf4rHAfKFuc0CtrY/AEw4LSoNiRE8CbbmI8OFLmD4FYSJYWIVBez3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@flydotio/litestream-linux-arm64": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@flydotio/litestream-linux-arm64/-/litestream-linux-arm64-1.0.0.tgz", @@ -567,6 +583,22 @@ "node": ">=18" } }, + "node_modules/@flydotio/litestream-linux-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@flydotio/litestream-linux-x64/-/litestream-linux-x64-1.0.0.tgz", + "integrity": "sha512-j3iPCCBv2euujtCbAZvrQvaFNvPpe7hFHci3Oblk1Rj4p2hXJXvFSHTTJXqD74hnt0DUN5qZi7+GWFMbkLastg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", diff --git a/templates/docker-entrypoint.ejs b/templates/docker-entrypoint.ejs index e744ba0..2c6ccbe 100755 --- a/templates/docker-entrypoint.ejs +++ b/templates/docker-entrypoint.ejs @@ -93,7 +93,7 @@ if (process.env.DATABASE_URL) { <% } -%> <% if (litestream && sqlite3 && (prismaFile || prismaEnv)) { -%> <%= tab(n) %>if (newDb && process.env.BUCKET_NAME) { -<%= tab(n+1) %>await exec(`litestream restore -config litestream.yml -if-replica-exists ${target}`) +<%= tab(n+1) %>await exec(`<% if (setupScriptType == 'dbsetup') { %>npx <% } %>litestream restore -config litestream.yml -if-replica-exists ${target}`) <% if (prismaSeed && sqlite3 && (prismaFile || prismaEnv)) { -%> <%= tab(n+1) %>newDb = !fs.existsSync(target) <% } -%> From 4a5d206b3ae2546a92ef890d5bb603b0554e408b Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Sat, 11 Jan 2025 14:36:50 -0500 Subject: [PATCH 17/20] run migrations in dbsetup too --- gdf.js | 4 ++-- templates/docker-entrypoint.ejs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gdf.js b/gdf.js index 1f99c75..19ab7e8 100755 --- a/gdf.js +++ b/gdf.js @@ -1001,9 +1001,9 @@ export class GDF { } for (const [template, filename] of Object.entries(templates)) { - const dest = await this.#writeTemplateFile(template, filename) + await this.#writeTemplateFile(template, filename) - if (template === 'docker-entrypoint.ejs') fs.chmodSync(dest, 0o755) + if (template === 'docker-entrypoint.ejs') fs.chmodSync(filename, 0o755) } // ensure that there is a dockerignore file diff --git a/templates/docker-entrypoint.ejs b/templates/docker-entrypoint.ejs index 2c6ccbe..831373c 100755 --- a/templates/docker-entrypoint.ejs +++ b/templates/docker-entrypoint.ejs @@ -99,7 +99,7 @@ if (process.env.DATABASE_URL) { <% } -%> <%= tab(n) %>} <% } -%> -<% if (sqlite3 && setupScriptType == 'docker') { -%> +<% if (sqlite3) { -%> <% if (prismaFile) { -%> <%= tab(n) %>// prepare database From 56e4dd34fc48dbbeaf37bf703d0c9d143b9aa1ec Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Sat, 11 Jan 2025 16:20:55 -0500 Subject: [PATCH 18/20] attend to the tests --- bun.lockb | Bin 150734 -> 151446 bytes gdf.js | 2 +- package-lock.json | 42 ++++++++++----------- package.json | 2 +- test/base/litestream/docker-entrypoint.js | 44 ++++++++++++++++++++++ 5 files changed, 67 insertions(+), 23 deletions(-) create mode 100755 test/base/litestream/docker-entrypoint.js diff --git a/bun.lockb b/bun.lockb index 4cf4b614d0ecf7a3a2849249a70c014e464651b0..eb0c2bb698fd21be51508799829db5beefbd475e 100755 GIT binary patch delta 14093 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 uOMK+FsexTA^N}0qO^;dRN^;8C3swzS+u5pEoNz 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 diff --git a/gdf.js b/gdf.js index 19ab7e8..c625ae9 100755 --- a/gdf.js +++ b/gdf.js @@ -1003,7 +1003,7 @@ export class GDF { for (const [template, filename] of Object.entries(templates)) { await this.#writeTemplateFile(template, filename) - if (template === 'docker-entrypoint.ejs') fs.chmodSync(filename, 0o755) + if (template === 'docker-entrypoint.ejs') fs.chmodSync(path.join(this._appdir, filename), 0o755) } // ensure that there is a dockerignore file diff --git a/package-lock.json b/package-lock.json index 5c9c661..4fa2115 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.6.1", "license": "MIT", "dependencies": { - "@flydotio/litestream": "^1.0.0", + "@flydotio/litestream": "^1.0.1", "@sveltejs/adapter-node": "^5.2.11", "chalk": "^5.3.0", "diff": "^5.2.0", @@ -518,27 +518,27 @@ } }, "node_modules/@flydotio/litestream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@flydotio/litestream/-/litestream-1.0.0.tgz", - "integrity": "sha512-RiJCIrQEZ7e93L7/urBfyxdCc13f/3eN/Ik/cYuBNnvEkefRSrJ9uZPJqBgR9MxzXTaLeSh/TZ49M4F26HYKtw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@flydotio/litestream/-/litestream-1.0.1.tgz", + "integrity": "sha512-hGIR37D1o8+AKcHa0PmdOG7dPm1RQsFQE3cqgt2udQVo3Q0NI5ruarLeaFWTFB6l+hjs97Xdlt0TgEufxLTzaQ==", "license": "MIT", "bin": { - "atc": "index.js" + "litestream": "index.js" }, "engines": { "node": ">=18" }, "optionalDependencies": { - "@flydotio/litestream-darwin-arm64": "1.0.0", - "@flydotio/litestream-darwin-x64": "1.0.0", - "@flydotio/litestream-linux-arm64": "1.0.0", - "@flydotio/litestream-linux-x64": "1.0.0" + "@flydotio/litestream-darwin-arm64": "1.0.1", + "@flydotio/litestream-darwin-x64": "1.0.1", + "@flydotio/litestream-linux-arm64": "1.0.1", + "@flydotio/litestream-linux-x64": "1.0.1" } }, "node_modules/@flydotio/litestream-darwin-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@flydotio/litestream-darwin-arm64/-/litestream-darwin-arm64-1.0.0.tgz", - "integrity": "sha512-07GwYSCGuPx2BWaKQCbY+/bWq27XvJDRdVSxdg96QKtu5UMmgJ5x71mBxabqKJ9b0oVD2qGa/9ilIIlDwi6vPw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@flydotio/litestream-darwin-arm64/-/litestream-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-LI663pEbO1RZzzkqDbXen6UIDeOBkGJqfyl8FGm4Y+zbcKiLQhopuQLMs4BHlWelGMb2ZnwpcpM+OcimoDhqHA==", "cpu": [ "arm64" ], @@ -552,9 +552,9 @@ } }, "node_modules/@flydotio/litestream-darwin-x64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@flydotio/litestream-darwin-x64/-/litestream-darwin-x64-1.0.0.tgz", - "integrity": "sha512-rvX/o3SFFoqpOP2wX7fVDKYqHV6ZX5IWjf4rHAfKFuc0CtrY/AEw4LSoNiRE8CbbmI8OFLmD4FYSJYWIVBez3Q==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@flydotio/litestream-darwin-x64/-/litestream-darwin-x64-1.0.1.tgz", + "integrity": "sha512-AecPpC1mu5QJ12+UWEaeCZbtNbde+t7qqgxrIYEZ+ZmrfgvCmsuZIqz67/IuRtaXTzr0COKD/uv9KRHIx+xHsQ==", "cpu": [ "x64" ], @@ -568,9 +568,9 @@ } }, "node_modules/@flydotio/litestream-linux-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@flydotio/litestream-linux-arm64/-/litestream-linux-arm64-1.0.0.tgz", - "integrity": "sha512-QEXw1H2dOS+AA2dDG7W6rvnJZW3cDFjM0yKo0tv1pBcE8DPi+0PJp9Fdx7r34LQx2DTX3yRWycTUAqJSxrFhhg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@flydotio/litestream-linux-arm64/-/litestream-linux-arm64-1.0.1.tgz", + "integrity": "sha512-/kukXjY+8xnAVUY3ywZIHIrW2gmKg5dXFVmSR/IQtdEwWZrKqdJ77fbK4u9bkacZqf94xn9jej8Cr4sToy6gNg==", "cpu": [ "arm64" ], @@ -584,9 +584,9 @@ } }, "node_modules/@flydotio/litestream-linux-x64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@flydotio/litestream-linux-x64/-/litestream-linux-x64-1.0.0.tgz", - "integrity": "sha512-j3iPCCBv2euujtCbAZvrQvaFNvPpe7hFHci3Oblk1Rj4p2hXJXvFSHTTJXqD74hnt0DUN5qZi7+GWFMbkLastg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@flydotio/litestream-linux-x64/-/litestream-linux-x64-1.0.1.tgz", + "integrity": "sha512-zmcAR8q2TNiAsWRSWscrN+r4NNQ+FwfatKhcE3G/YticNSkNyIhtCTlvXxIyOG+E+UWa+j441cjbmb7p83JPEw==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index b900a43..1326f39 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "node": ">=16.0.0" }, "dependencies": { - "@flydotio/litestream": "^1.0.0", + "@flydotio/litestream": "^1.0.1", "@sveltejs/adapter-node": "^5.2.11", "chalk": "^5.3.0", "diff": "^5.2.0", diff --git a/test/base/litestream/docker-entrypoint.js b/test/base/litestream/docker-entrypoint.js new file mode 100755 index 0000000..79a4470 --- /dev/null +++ b/test/base/litestream/docker-entrypoint.js @@ -0,0 +1,44 @@ +#!/usr/bin/env node + +const { spawn } = require('node:child_process') +const path = require('node:path') +const fs = require('node:fs') + +const env = { ...process.env } + +;(async() => { + // If running the web server then migrate existing database + if (process.argv.slice(-3).join(' ') === 'npm run start') { + // place Sqlite3 database on volume + const source = path.resolve('./dev.db') + const target = '/data/' + path.basename(source) + if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) + let newDb = !fs.existsSync(target) + if (newDb && process.env.BUCKET_NAME) { + await exec(`litestream restore -config litestream.yml -if-replica-exists ${target}`) + } + + // prepare database + await exec('npx prisma migrate deploy') + } + + // launch application + if (process.env.BUCKET_NAME) { + await exec(`litestream replicate -config litestream.yml -exec ${JSON.stringify(process.argv.slice(2).join(' '))}`) + } else { + await exec(process.argv.slice(2).join(' ')) + } +})() + +function exec(command) { + const child = spawn(command, { shell: true, stdio: 'inherit', env }) + return new Promise((resolve, reject) => { + child.on('exit', code => { + if (code === 0) { + resolve() + } else { + reject(new Error(`${command} failed rc=${code}`)) + } + }) + }) +} From 852bbf15c99db282bdf2ca06d186e01e1d8089b1 Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Sat, 11 Jan 2025 17:59:17 -0500 Subject: [PATCH 19/20] dbsetup is only for sqlite3 --- fly.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fly.js b/fly.js index 9e5383c..16446d1 100755 --- a/fly.js +++ b/fly.js @@ -15,7 +15,7 @@ GDF.extend(class extends GDF { // create volume for sqlite3 if (this.sqlite3) this.flyMakeVolume() - if (this.setupScriptType === 'dbsetup') { + if (this.sqlite3 && this.setupScriptType === 'dbsetup') { this.flySetCmd() } From a27a47e43c882c51843ef2da9987d2195d8e0c6f Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Sat, 11 Jan 2025 18:26:29 -0500 Subject: [PATCH 20/20] appease eslint --- bun.lockb | Bin 151446 -> 151446 bytes templates/docker-entrypoint.ejs | 2 +- test/base/litestream/docker-entrypoint.js | 2 +- .../next-standalone/docker-entrypoint.js | 28 ++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100755 test/frameworks/next-standalone/docker-entrypoint.js diff --git a/bun.lockb b/bun.lockb index eb0c2bb698fd21be51508799829db5beefbd475e..dd856b3c6e7974d54bd405b2ad12419e848d125f 100755 GIT binary patch delta 947 zcmXZZNla5w6vpv;s)ZIQgNU;Ms*4(@NaDtYqHIhECPZ+az#<;+Aha1;!Ol0NK#H}%LqYMA1o|FFizW4R^<-K0pml3F|N6g6x*;noDORuw z!`3Cp-+afE)CG-1oe}$~n!I55Ici-ET{&52%(_}~!n$$m*3$ij-rIzAb>v^rdz-Xw z9r-Yrz-6qg=d495X3DztJ9Gl;Hj>-PI-zx&$aAH}xtw*IIZr{akuP<= zCo6mR>oWbjKDWN7)rdkjO*ct5MK?hoOdq7uf6q!oF&0Qn{HLsJjq4?v(dZMQZ1F!r z$;dn3os%}H@{2jy=To7i+B;Zs0-fl>Nu0uIoWWU~!+Bi5MO?yVbfX7Xa23~lS17xh z4iWWbw%`bwpzlYYRbOc{D?%*zZkwzc-F@Qk~VHo!?f>DfN921yC22;o)K#1$Oft%=LH=?MCIR9r_ zYRd|gPn@rqk%Ru&jO=JmQWPU>C9A6Olk?wridpQ2ZuN0~d&zO9D?Xc%1C=rUQ<~z+ IL;1M;4^h}?hX4Qo delta 957 zcmXZaOKVd>7{>8=E3r*XXtmZuG>WZy55+CnQ z;#;*eldg-fUbWphY4pAmH(<(LswZclGwr$ta)%bojO#X&69{0|bz8{0pfl&XMsnHR z9=(EbqD~`8V{wJl8dopFziW-F9*d^iLIBw}X6` zth2D*+1|XQn)Gn=Fm&_)bi+gTUS6UB{pqk>^CT8%Bc|==yu@nart%{%A+Zg1j{3rPArj>RtKt@jsIIa0vha diff --git a/templates/docker-entrypoint.ejs b/templates/docker-entrypoint.ejs index 831373c..e49d326 100755 --- a/templates/docker-entrypoint.ejs +++ b/templates/docker-entrypoint.ejs @@ -89,7 +89,7 @@ if (process.env.DATABASE_URL) { <% } -%> <% } -%> <% if (sqlite3 && (prismaSeed || litestream) && (prismaFile || prismaEnv)) { -%> -<%= tab(n) %><%= litestream ? 'let' : 'const' %> newDb = <%- !prismaFile ? 'target && ' : '' %>!fs.existsSync(target) +<%= tab(n) %><%= litestream && prismaSeed && sqlite3 ? 'let' : 'const' %> newDb = <%- !prismaFile ? 'target && ' : '' %>!fs.existsSync(target) <% } -%> <% if (litestream && sqlite3 && (prismaFile || prismaEnv)) { -%> <%= tab(n) %>if (newDb && process.env.BUCKET_NAME) { diff --git a/test/base/litestream/docker-entrypoint.js b/test/base/litestream/docker-entrypoint.js index 79a4470..37dbc7a 100755 --- a/test/base/litestream/docker-entrypoint.js +++ b/test/base/litestream/docker-entrypoint.js @@ -13,7 +13,7 @@ const env = { ...process.env } const source = path.resolve('./dev.db') const target = '/data/' + path.basename(source) if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) - let newDb = !fs.existsSync(target) + const newDb = !fs.existsSync(target) if (newDb && process.env.BUCKET_NAME) { await exec(`litestream restore -config litestream.yml -if-replica-exists ${target}`) } diff --git a/test/frameworks/next-standalone/docker-entrypoint.js b/test/frameworks/next-standalone/docker-entrypoint.js new file mode 100755 index 0000000..91b89a3 --- /dev/null +++ b/test/frameworks/next-standalone/docker-entrypoint.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +const { spawn } = require('node:child_process') + +const env = { ...process.env } + +;(async() => { + // If running the web server then prerender pages + if (process.argv.slice(-2).join(' ') === 'node server.js') { + await exec('npx next build --experimental-build-mode generate') + } + + // launch application + await exec(process.argv.slice(2).join(' ')) +})() + +function exec(command) { + const child = spawn(command, { shell: true, stdio: 'inherit', env }) + return new Promise((resolve, reject) => { + child.on('exit', code => { + if (code === 0) { + resolve() + } else { + reject(new Error(`${command} failed rc=${code}`)) + } + }) + }) +}