feat: run WordPress projects on native runtime

This commit is contained in:
reaper
2026-08-22 04:11:44 -05:00
parent 6fc33cc8a4
commit 8356a6949a
12 changed files with 1687 additions and 378 deletions
+735 -146
View File
File diff suppressed because it is too large Load Diff
+190 -29
View File
@@ -1,39 +1,200 @@
import { ipcMain, type WebContents } from 'electron'
import { spawn } from 'child_process'
import { listProjects, describeProject, getProjectRoot, unregisterProject, updateEnvironment, ensureRouter, trustAuroraCA } from '../auroraEngine'
import {
listProjects,
describeProject,
getNativeProjectDefinition,
getProjectRoot,
unregisterProject,
updateEnvironment,
ensureRouter,
trustAuroraCA
} from '../auroraEngine'
import { nativeServiceSpecs, startNativeProject, stopNativeProject } from '../native/nativeProject'
import { runCommandStreamed } from '../commandRunner'
import { runProjectLifecycleHooks } from '../moduleRuntime'
import type { EnvironmentUpdate } from '../../shared/types'
const composeArgs=(root:string,...args:string[]):string[]=>['compose','-f',`${root}/.aurora/compose.yaml`,...args]
const allowedServices = new Set(['web','php','db','node','adminer','redis','mailpit'])
function launchTerminal(command:string,args:string[]):Promise<void>{return new Promise((resolve,reject)=>{const child=spawn(command,args,{detached:true,stdio:'ignore'});child.once('error',reject);child.once('spawn',()=>{child.unref();resolve()})})}
async function startProject(operationId:string,name:string,sender:WebContents,forceRecreate=false):Promise<void>{
const root=await getProjectRoot(name); await updateEnvironment(root,{}); await ensureRouter()
try { await runCommandStreamed(operationId,'docker',composeArgs(root,'up','-d','--build',...(forceRecreate?['--force-recreate']:[]),'--remove-orphans'),sender,{cwd:root,emitExit:false}); await runProjectLifecycleHooks(root,'projectStart',operationId,sender); if(!sender.isDestroyed())sender.send('terminal:exit',{operationId,exitCode:0,cancelled:false}) }
catch(error){if(!sender.isDestroyed())sender.send('terminal:exit',{operationId,exitCode:1,cancelled:false});throw error}
const composeArgs = (root: string, ...args: string[]): string[] => [
'compose',
'-f',
`${root}/.aurora/compose.yaml`,
...args
]
const allowedServices = new Set(['web', 'php', 'db', 'node', 'adminer', 'redis', 'mailpit'])
function launchTerminal(command: string, args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, { detached: true, stdio: 'ignore' })
child.once('error', reject)
child.once('spawn', () => {
child.unref()
resolve()
})
})
}
export function registerProjectsIpc():void{
ipcMain.handle('projects:list',()=>listProjects()); ipcMain.handle('projects:describe',(_e,name:string)=>describeProject(name))
ipcMain.handle('projects:start',(e,id:string,name:string)=>startProject(id,name,e.sender))
ipcMain.handle('projects:stop',async(e,id:string,name:string)=>{const root=await getProjectRoot(name);return runCommandStreamed(id,'docker',composeArgs(root,'down'),e.sender,{cwd:root})})
ipcMain.handle('projects:restart',(e,id:string,name:string)=>startProject(id,name,e.sender,true))
ipcMain.handle('projects:restartService',async(e,id:string,name:string,service:string)=>{if(!allowedServices.has(service))throw new Error('Invalid service');const root=await getProjectRoot(name);return runCommandStreamed(id,'docker',composeArgs(root,'restart',service),e.sender,{cwd:root})})
ipcMain.handle('projects:phpInfo',async(e,id:string,name:string)=>{const root=await getProjectRoot(name);return runCommandStreamed(id,'docker',composeArgs(root,'exec','-T','php','php','-i'),e.sender,{cwd:root})})
ipcMain.handle('projects:openTerminal',async(_e,name:string)=>{const root=await getProjectRoot(name);const candidates: Array<[string,string[]]>=process.platform==='linux'?[['ptyxis',['--new-window','--working-directory',root,'--title',`Aurora · ${name}`]],['x-terminal-emulator',['--new-window','--working-directory',root]],['gnome-terminal',[`--working-directory=${root}`]],['kgx',['--working-directory',root]],['konsole',['--workdir',root]]]:[];for(const [cmd,args] of candidates){try{await launchTerminal(cmd,args);return}catch{/* try the next supported terminal */}}throw new Error('No supported terminal application was found.')})
ipcMain.handle('projects:delete',async(e,id:string,name:string,approot:string,deleteFiles:boolean)=>{
let root=approot
try { root=await getProjectRoot(name) } catch { /* use renderer-provided root for stale entries */ }
try { await runProjectLifecycleHooks(root,'projectRemove',id,e.sender) } catch (error) { console.warn(`Aurora module cleanup for '${name}' failed:`, error) }
async function startProject(
operationId: string,
name: string,
sender: WebContents,
forceRecreate = false
): Promise<void> {
const root = await getProjectRoot(name)
await updateEnvironment(root, {})
const native = await getNativeProjectDefinition(name)
try {
await runCommandStreamed(id,'docker',composeArgs(root,'down','-v','--remove-orphans'),e.sender,{cwd:root})
if (native) {
if (!sender.isDestroyed())
sender.send('terminal:data', {
operationId,
stream: 'stdout',
chunk: 'Starting Aurora Native database, PHP-FPM, and nginx…\n'
})
await startNativeProject(native)
} else {
await ensureRouter()
await runCommandStreamed(
operationId,
'docker',
composeArgs(
root,
'up',
'-d',
'--build',
...(forceRecreate ? ['--force-recreate'] : []),
'--remove-orphans'
),
sender,
{ cwd: root, emitExit: false }
)
}
await runProjectLifecycleHooks(root, 'projectStart', operationId, sender)
if (!sender.isDestroyed())
sender.send('terminal:exit', { operationId, exitCode: 0, cancelled: false })
} catch (error) {
// A malformed/missing compose file must never make a project undeletable.
console.warn(`Aurora cleanup for '${name}' skipped:`, error)
if (!sender.isDestroyed())
sender.send('terminal:exit', { operationId, exitCode: 1, cancelled: false })
throw error
}
await unregisterProject(name,deleteFiles)
})
ipcMain.handle('projects:trustCA',async()=>{ await trustAuroraCA(); await ensureRouter() })
ipcMain.handle('projects:updateEnvironment',async(_e,_id:string,_name:string,root:string,updates:EnvironmentUpdate)=>{
await updateEnvironment(root,updates)
})
}
export function registerProjectsIpc(): void {
ipcMain.handle('projects:list', () => listProjects())
ipcMain.handle('projects:describe', (_e, name: string) => describeProject(name))
ipcMain.handle('projects:start', (e, id: string, name: string) =>
startProject(id, name, e.sender)
)
ipcMain.handle('projects:stop', async (e, id: string, name: string) => {
const native = await getNativeProjectDefinition(name)
if (native) {
await stopNativeProject(native)
if (!e.sender.isDestroyed())
e.sender.send('terminal:exit', { operationId: id, exitCode: 0, cancelled: false })
return
}
const root = await getProjectRoot(name)
return runCommandStreamed(id, 'docker', composeArgs(root, 'down'), e.sender, { cwd: root })
})
ipcMain.handle('projects:restart', async (e, id: string, name: string) => {
const native = await getNativeProjectDefinition(name)
if (native) await stopNativeProject(native)
return startProject(id, name, e.sender, true)
})
ipcMain.handle(
'projects:restartService',
async (e, id: string, name: string, service: string) => {
if (!allowedServices.has(service)) throw new Error('Invalid service')
const native = await getNativeProjectDefinition(name)
if (native) {
await stopNativeProject(native)
return startProject(id, name, e.sender)
}
const root = await getProjectRoot(name)
return runCommandStreamed(id, 'docker', composeArgs(root, 'restart', service), e.sender, {
cwd: root
})
}
)
ipcMain.handle('projects:phpInfo', async (e, id: string, name: string) => {
const native = await getNativeProjectDefinition(name)
if (native) {
const php = nativeServiceSpecs(native)
.find((spec) => spec.id.endsWith(':php'))
?.command.replace(/php-fpm$/, 'php')
if (!php) throw new Error('Native PHP executable not found.')
return runCommandStreamed(id, php, ['-i'], e.sender, { cwd: native.root })
}
const root = await getProjectRoot(name)
return runCommandStreamed(
id,
'docker',
composeArgs(root, 'exec', '-T', 'php', 'php', '-i'),
e.sender,
{ cwd: root }
)
})
ipcMain.handle('projects:openTerminal', async (_e, name: string) => {
const root = await getProjectRoot(name)
const candidates: Array<[string, string[]]> =
process.platform === 'linux'
? [
[
'ptyxis',
['--new-window', '--working-directory', root, '--title', `Aurora · ${name}`]
],
['x-terminal-emulator', ['--new-window', '--working-directory', root]],
['gnome-terminal', [`--working-directory=${root}`]],
['kgx', ['--working-directory', root]],
['konsole', ['--workdir', root]]
]
: []
for (const [cmd, args] of candidates) {
try {
await launchTerminal(cmd, args)
return
} catch {
/* try the next supported terminal */
}
}
throw new Error('No supported terminal application was found.')
})
ipcMain.handle(
'projects:delete',
async (e, id: string, name: string, approot: string, deleteFiles: boolean) => {
let root = approot
try {
root = await getProjectRoot(name)
} catch {
/* use renderer-provided root for stale entries */
}
try {
await runProjectLifecycleHooks(root, 'projectRemove', id, e.sender)
} catch (error) {
console.warn(`Aurora module cleanup for '${name}' failed:`, error)
}
try {
const native = await getNativeProjectDefinition(name).catch(() => null)
if (native) await stopNativeProject(native)
else
await runCommandStreamed(
id,
'docker',
composeArgs(root, 'down', '-v', '--remove-orphans'),
e.sender,
{ cwd: root }
)
} catch (error) {
// A malformed/missing compose file must never make a project undeletable.
console.warn(`Aurora cleanup for '${name}' skipped:`, error)
}
await unregisterProject(name, deleteFiles)
}
)
ipcMain.handle('projects:trustCA', async () => {
await trustAuroraCA()
await ensureRouter()
})
ipcMain.handle(
'projects:updateEnvironment',
async (_e, _id: string, _name: string, root: string, updates: EnvironmentUpdate) => {
await updateEnvironment(root, updates)
}
)
}
+149 -37
View File
@@ -6,77 +6,174 @@ import type { WebContents } from 'electron'
import type { AuroraModuleLifecycleHook } from '../shared/types'
import { getModuleManifest, moduleDirectory } from './moduleRegistry'
import { runCommandStreamed } from './commandRunner'
import { AURORA_ENV, ensureRouter, getProjectConfig, projectUrls, setProjectModuleMetadata } from './auroraEngine'
import {
AURORA_ENV,
ensureRouter,
getNativeProjectDefinition,
getProjectConfig,
projectUrls,
setProjectModuleMetadata
} from './auroraEngine'
import { startNativeProject } from './native/nativeProject'
import { runtimeRoot } from './nativeRuntime'
import { saveSiteCredentials } from './ipc/secrets'
type Settings = Record<string, string | number | boolean>
type ExternalHook = (context: Record<string, unknown>) => Promise<void> | void
type LoadedModule = Partial<Record<AuroraModuleLifecycleHook, ExternalHook>>
type HookOptions = { directory?: string; projectName?: string; settings?: Settings; toolId?: string; operationId?: string; sender?: WebContents }
type HookOptions = {
directory?: string
projectName?: string
settings?: Settings
toolId?: string
operationId?: string
sender?: WebContents
}
const execFileAsync = promisify(execFile)
const CORE_MODULE_IDS = new Set(['adminer', 'redis', 'mailpit'])
function sendExit(sender: WebContents, operationId: string, exitCode: number): void {
if (!sender.isDestroyed()) sender.send('terminal:exit', { operationId, exitCode, cancelled: false })
if (!sender.isDestroyed())
sender.send('terminal:exit', { operationId, exitCode, cancelled: false })
}
function loadModule(moduleId: string, main: string): LoadedModule {
const root = resolve(moduleDirectory(), moduleId)
const entry = resolve(root, main)
if (entry !== root && !entry.startsWith(`${root}${sep}`)) throw new Error('Unsafe module main entry')
if (entry !== root && !entry.startsWith(`${root}${sep}`))
throw new Error('Unsafe module main entry')
return createRequire(join(dirname(entry), 'loader.cjs'))(entry) as LoadedModule
}
export async function runModuleLifecycleHook(moduleId: string, hook: AuroraModuleLifecycleHook, options: HookOptions = {}): Promise<boolean> {
export async function runModuleLifecycleHook(
moduleId: string,
hook: AuroraModuleLifecycleHook,
options: HookOptions = {}
): Promise<boolean> {
const manifest = await getModuleManifest(moduleId)
if (!manifest.main) return false
const handler = loadModule(moduleId, manifest.main)[hook]
if (typeof handler !== 'function') return false
const directory = options.directory
const urls = options.projectName ? projectUrls(options.projectName) : undefined
const projectConfig = directory ? await getProjectConfig(directory) : undefined
const nativeDefinition = options.projectName
? await getNativeProjectDefinition(options.projectName)
: null
const urls =
projectConfig?.runtimeEngine === 'native' && projectConfig.nativePorts
? {
http: `http://127.0.0.1:${projectConfig.nativePorts.http}`,
https: `http://127.0.0.1:${projectConfig.nativePorts.http}`
}
: options.projectName
? projectUrls(options.projectName)
: undefined
const run = async (label: string, command: string, args: string[]): Promise<void> => {
if (options.sender && options.operationId) {
if (!options.sender.isDestroyed()) options.sender.send('terminal:data', { operationId: options.operationId, stream: 'stdout', chunk: `\n[${manifest.name}] ${label}\n` })
return runCommandStreamed(options.operationId, command, args, options.sender, { cwd: directory ?? resolve(moduleDirectory(), moduleId), emitExit: false })
if (!options.sender.isDestroyed())
options.sender.send('terminal:data', {
operationId: options.operationId,
stream: 'stdout',
chunk: `\n[${manifest.name}] ${label}\n`
})
return runCommandStreamed(options.operationId, command, args, options.sender, {
cwd: directory ?? resolve(moduleDirectory(), moduleId),
emitExit: false
})
}
await execFileAsync(command, args, { cwd: directory ?? resolve(moduleDirectory(), moduleId), env: AURORA_ENV })
await execFileAsync(command, args, {
cwd: directory ?? resolve(moduleDirectory(), moduleId),
env: AURORA_ENV
})
}
await handler(Object.freeze({
moduleId,
hook,
directory,
projectName: options.projectName,
toolId: options.toolId,
settings: Object.freeze({ ...(options.settings ?? {}) }),
environment: projectConfig ? Object.freeze({ php: projectConfig.php, node: projectConfig.node, webserver: projectConfig.webserver, database: projectConfig.database, databaseVersion: projectConfig.databaseVersion, docroot: projectConfig.docroot }) : undefined,
urls: urls ? Object.freeze(urls) : undefined,
run,
ensureRouter,
setProjectMetadata: async (metadata: Settings) => {
if (!directory) throw new Error('Project metadata is unavailable outside a project lifecycle hook')
await setProjectModuleMetadata(directory, metadata)
},
saveCredentials: async (credentials: { platform: string; adminUrl: string; username: string; password: string; email: string }) => {
if (!directory) throw new Error('Project credentials are unavailable outside a project lifecycle hook')
await saveSiteCredentials(directory, credentials)
}
}))
await handler(
Object.freeze({
moduleId,
hook,
directory,
projectName: options.projectName,
toolId: options.toolId,
settings: Object.freeze({ ...(options.settings ?? {}) }),
environment: projectConfig
? Object.freeze({
php: projectConfig.php,
node: projectConfig.node,
webserver: projectConfig.webserver,
database: projectConfig.database,
databaseVersion: projectConfig.databaseVersion,
docroot: projectConfig.docroot,
runtimeEngine: projectConfig.runtimeEngine ?? 'container'
})
: undefined,
urls: urls ? Object.freeze(urls) : undefined,
native: nativeDefinition
? Object.freeze({
php: join(runtimeRoot(), 'bin', 'php'),
wp: join(runtimeRoot(), 'bin', 'wp'),
databaseHost: '127.0.0.1',
databasePort: nativeDefinition.ports.database,
start: () => startNativeProject(nativeDefinition)
})
: undefined,
run,
ensureRouter,
setProjectMetadata: async (metadata: Settings) => {
if (!directory)
throw new Error('Project metadata is unavailable outside a project lifecycle hook')
await setProjectModuleMetadata(directory, metadata)
},
saveCredentials: async (credentials: {
platform: string
adminUrl: string
username: string
password: string
email: string
}) => {
if (!directory)
throw new Error('Project credentials are unavailable outside a project lifecycle hook')
await saveSiteCredentials(directory, credentials)
}
})
)
return true
}
export async function runProjectLifecycleHooks(directory: string, hook: 'projectStart' | 'projectRemove', operationId: string, sender: WebContents): Promise<void> {
export async function runProjectLifecycleHooks(
directory: string,
hook: 'projectStart' | 'projectRemove',
operationId: string,
sender: WebContents
): Promise<void> {
const config = await getProjectConfig(directory)
for (const moduleId of config.modules) {
if (CORE_MODULE_IDS.has(moduleId)) continue
await runModuleLifecycleHook(moduleId, hook, { directory, projectName: config.name, settings: config.moduleSettings?.[moduleId], operationId, sender })
await runModuleLifecycleHook(moduleId, hook, {
directory,
projectName: config.name,
settings: config.moduleSettings?.[moduleId],
operationId,
sender
})
}
}
export async function runModuleProjectCreate(moduleId: string, operationId: string, directory: string, projectName: string, settings: Settings, sender: WebContents): Promise<void> {
export async function runModuleProjectCreate(
moduleId: string,
operationId: string,
directory: string,
projectName: string,
settings: Settings,
sender: WebContents
): Promise<void> {
try {
await runModuleLifecycleHook(moduleId, 'projectCreate', { directory, projectName, settings, operationId, sender })
await runModuleLifecycleHook(moduleId, 'projectCreate', {
directory,
projectName,
settings,
operationId,
sender
})
sendExit(sender, operationId, 0)
} catch (error) {
sendExit(sender, operationId, 1)
@@ -84,13 +181,28 @@ export async function runModuleProjectCreate(moduleId: string, operationId: stri
}
}
export async function runModuleProjectTool(moduleId: string, toolId: string, operationId: string, directory: string, sender: WebContents): Promise<void> {
export async function runModuleProjectTool(
moduleId: string,
toolId: string,
operationId: string,
directory: string,
sender: WebContents
): Promise<void> {
const manifest = await getModuleManifest(moduleId)
if (!manifest.project?.tools?.some((tool) => tool.id === toolId)) throw new Error(`Unknown module tool '${toolId}'`)
if (!manifest.project?.tools?.some((tool) => tool.id === toolId))
throw new Error(`Unknown module tool '${toolId}'`)
const config = await getProjectConfig(directory)
if (!config.modules.includes(moduleId)) throw new Error(`Module '${moduleId}' is not installed in this project`)
if (!config.modules.includes(moduleId))
throw new Error(`Module '${moduleId}' is not installed in this project`)
try {
const handled = await runModuleLifecycleHook(moduleId, 'projectTool', { directory, projectName: config.name, settings: config.moduleSettings?.[moduleId], toolId, operationId, sender })
const handled = await runModuleLifecycleHook(moduleId, 'projectTool', {
directory,
projectName: config.name,
settings: config.moduleSettings?.[moduleId],
toolId,
operationId,
sender
})
if (!handled) throw new Error(`Module '${moduleId}' does not implement projectTool`)
sendExit(sender, operationId, 0)
} catch (error) {
+101 -1
View File
@@ -1,5 +1,18 @@
import { execFile } from 'child_process'
import { createRequire } from 'module'
import { mkdtemp, readFile, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { promisify } from 'util'
import { describe, expect, it } from 'vitest'
import { renderMariaDbConfig, renderNginxConfig, renderPhpFpmConfig } from './nativeProject'
import {
renderMariaDbConfig,
renderNginxConfig,
renderPhpFpmConfig,
startNativeProject,
stopNativeProject
} from './nativeProject'
import { allocateNativePorts } from './portAllocator'
const project = {
name: 'demo',
@@ -7,6 +20,7 @@ const project = {
docroot: 'public',
ports: { http: 41001, php: 41002, database: 41003, node: 41004 }
}
const execFileAsync = promisify(execFile)
describe('native project configuration', () => {
it('isolates PHP-FPM on its allocated loopback port', () =>
@@ -25,3 +39,89 @@ describe('native project configuration', () => {
expect(config).toContain('/.aurora/native/data/mariadb')
})
})
it.runIf(Boolean(process.env.AURORA_NATIVE_SMOKE_ROOT))(
'serves PHP through the complete native stack',
async () => {
const root = await mkdtemp(join(tmpdir(), 'aurora-native-project-'))
await writeFile(join(root, 'index.php'), '<?php echo "aurora-native-ok";')
const definition = {
name: `smoke-${Date.now()}`,
root,
docroot: '',
ports: await allocateNativePorts(),
installedRuntimeRoot: process.env.AURORA_NATIVE_SMOKE_ROOT
}
try {
await startNativeProject(definition)
const response = await fetch(`http://127.0.0.1:${definition.ports.http}`)
expect(await response.text()).toBe('aurora-native-ok')
} finally {
await stopNativeProject(definition)
}
},
45000
)
it.runIf(Boolean(process.env.AURORA_NATIVE_WORDPRESS_SMOKE_ROOT))(
'provisions WordPress with the bundled native runtime',
async () => {
const runtime = process.env.AURORA_NATIVE_WORDPRESS_SMOKE_ROOT!
const root = await mkdtemp(join(tmpdir(), 'aurora-native-wordpress-'))
const definition = {
name: `wordpress-${Date.now()}`,
root,
docroot: '',
ports: await allocateNativePorts(),
installedRuntimeRoot: runtime
}
const wordpress = createRequire(import.meta.url)(
'../../../packages/aurora-module-wordpress/main/index.cjs'
) as { projectCreate: (context: Record<string, unknown>) => Promise<void> }
try {
await wordpress.projectCreate({
moduleId: 'wordpress',
directory: root,
projectName: definition.name,
settings: {
title: 'Aurora native smoke',
admin_user: 'aurora-admin',
admin_password: 'aurora-native-test-password',
admin_email: '[email protected]',
locale: 'en_US',
multisite: 'none',
wp_debug: false
},
environment: { runtimeEngine: 'native' },
urls: {
http: `http://127.0.0.1:${definition.ports.http}`,
https: `http://127.0.0.1:${definition.ports.http}`
},
native: {
php: join(runtime, 'bin', 'php'),
wp: join(runtime, 'bin', 'wp'),
databasePort: definition.ports.database,
start: () => startNativeProject(definition)
},
run: async (_label: string, command: string, args: string[]) => {
await execFileAsync(command, args, {
cwd: root,
env: process.env,
maxBuffer: 32 * 1024 * 1024
})
},
setProjectMetadata: async () => undefined,
saveCredentials: async () => undefined
})
expect(await readFile(join(root, 'wp-config.php'), 'utf8')).toContain(
`127.0.0.1:${definition.ports.database}`
)
const response = await fetch(`http://127.0.0.1:${definition.ports.http}`)
expect(response.ok).toBe(true)
expect(await response.text()).toContain('Aurora native smoke')
} finally {
await stopNativeProject(definition)
}
},
120000
)
+38 -6
View File
@@ -14,6 +14,11 @@ export interface NativeProjectDefinition {
root: string
docroot: string
ports: AuroraNativePorts
installedRuntimeRoot?: string
}
function installedRoot(project: NativeProjectDefinition): string {
return project.installedRuntimeRoot ?? runtimeRoot()
}
function nativeDirectory(root: string): string {
@@ -53,6 +58,11 @@ error_log "${quoteNginx(join(directory, 'logs', 'nginx.log'))}" info;
events { worker_connections 256; }
http {
access_log "${quoteNginx(join(directory, 'logs', 'nginx-access.log'))}";
client_body_temp_path "${quoteNginx(join(directory, 'tmp', 'nginx-client'))}";
proxy_temp_path "${quoteNginx(join(directory, 'tmp', 'nginx-proxy'))}";
fastcgi_temp_path "${quoteNginx(join(directory, 'tmp', 'nginx-fastcgi'))}";
uwsgi_temp_path "${quoteNginx(join(directory, 'tmp', 'nginx-uwsgi'))}";
scgi_temp_path "${quoteNginx(join(directory, 'tmp', 'nginx-scgi'))}";
server {
listen 127.0.0.1:${project.ports.http};
server_name localhost;
@@ -65,9 +75,17 @@ http {
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param SERVER_NAME $server_name;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param HTTP_HOST $http_host;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
}
}
}
@@ -94,18 +112,32 @@ skip-name-resolve
export async function provisionNativeProject(project: NativeProjectDefinition): Promise<void> {
const directory = nativeDirectory(project.root)
for (const child of ['config', 'data/mariadb', 'logs', 'pids', 'tmp'])
for (const child of [
'config',
'data/mariadb',
'logs',
'pids',
'tmp',
'tmp/nginx-client',
'tmp/nginx-proxy',
'tmp/nginx-fastcgi',
'tmp/nginx-uwsgi',
'tmp/nginx-scgi'
])
await mkdir(join(directory, child), { recursive: true })
await Promise.all([
writeFile(join(directory, 'config', 'php-fpm.conf'), renderPhpFpmConfig(project)),
writeFile(join(directory, 'config', 'nginx.conf'), renderNginxConfig(project)),
writeFile(join(directory, 'config', 'mariadb.cnf'), renderMariaDbConfig(project))
writeFile(
join(directory, 'config', 'mariadb.cnf'),
renderMariaDbConfig(project, installedRoot(project))
)
])
try {
await access(join(directory, 'data', 'mariadb', 'mysql'))
} catch {
await execFileAsync(
join(runtimeRoot(), 'bin', 'mariadb-install-db'),
join(installedRoot(project), 'bin', 'mariadb-install-db'),
[
'--no-defaults',
`--datadir=${join(directory, 'data', 'mariadb')}`,
@@ -129,19 +161,19 @@ export function nativeServiceSpecs(project: NativeProjectDefinition): NativeServ
return [
{
...common('database'),
command: join(runtimeRoot(), 'bin', 'mariadbd'),
command: join(installedRoot(project), 'bin', 'mariadbd'),
args: [`--defaults-file=${join(directory, 'config', 'mariadb.cnf')}`],
ready: { port: project.ports.database, timeoutMs: 30000 }
},
{
...common('php'),
command: join(runtimeRoot(), 'bin', 'php-fpm'),
command: join(installedRoot(project), 'bin', 'php-fpm'),
args: ['--nodaemonize', '--fpm-config', join(directory, 'config', 'php-fpm.conf')],
ready: { port: project.ports.php }
},
{
...common('web'),
command: join(runtimeRoot(), 'bin', 'nginx'),
command: join(installedRoot(project), 'bin', 'nginx'),
args: ['-c', join(directory, 'config', 'nginx.conf'), '-p', `${directory}/`],
ready: { port: project.ports.http }
}