Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ export default class Serve extends BaseCommand {
'```',
'{{ binaryName }} serve --assets-args="--debug --base=/public"',
'```',
'',
'When running inside a git worktree, the server automatically uses a deterministic',
'port based on the worktree name, so multiple worktrees can run in parallel.',
'You may disable this behavior using the --no-worktree-port flag.',
'```',
'{{ binaryName }} serve --no-worktree-port',
'```',
]

/**
Expand Down Expand Up @@ -101,6 +108,16 @@ export default class Serve extends BaseCommand {
})
declare clear?: boolean

/**
* Use a deterministic port based on the git worktree name
*/
@flags.boolean({
description: 'Use a deterministic port based on the git worktree name',
showNegatedVariantInHelp: true,
default: true,
})
declare worktreePort: boolean

/**
* Log a development dependency is missing
*
Expand Down Expand Up @@ -135,6 +152,21 @@ export default class Serve extends BaseCommand {
return
}

/**
* Use a deterministic port when running inside a git worktree, so that
* multiple worktrees of the same application can be started in parallel
* without port conflicts
*/
if (this.worktreePort) {
const worktreePort = await this.getWorktreePort()
if (worktreePort) {
process.env.PORT = String(worktreePort.port)
this.logger.info(
`Using worktree "${worktreePort.worktree.name}" on port ${worktreePort.port}`
)
}
}

this.devServer = new assembler.DevServer(this.app.appRoot, {
hmr: this.hmr === true ? true : false,
clearScreen: this.clear === false ? false : true,
Expand Down
48 changes: 48 additions & 0 deletions modules/ace/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
* file that was distributed with this source code.
*/

import { fileURLToPath } from 'node:url'
import { getGitWorktree, type GitWorktree } from '@poppinss/utils'
import { BaseCommand as AceBaseCommand, ListCommand as AceListCommand } from '@adonisjs/ace'

import { type Kernel } from './kernel.ts'
import type { ApplicationService } from '../../src/types.ts'
import type { CommandOptions, ParsedOutput, UIPrimitives } from '../../types/ace.ts'
import { getBasePort, computeWorktreePort } from '../../src/helpers/worktree.ts'

/**
* The base command class for creating custom Ace commands in AdonisJS applications.
Expand Down Expand Up @@ -75,6 +78,36 @@ export class BaseCommand extends AceBaseCommand {
return codemods
}

/**
* Returns the linked git worktree in which the application is running,
* alongside a deterministic port computed from the worktree name. Returns
* "null" when the application is not running inside a linked git worktree
* (for example the main checkout or a non-git directory).
*
* The port is stable for a given worktree name and base port, so multiple
* worktrees of the same application can run in parallel without port
* conflicts. The base port is read from the application dot-env files
* (via the "PORT" variable), with 3333 as the fallback.
*
* @example
* ```ts
* const worktreePort = await this.getWorktreePort()
* if (worktreePort) {
* console.log(worktreePort.worktree.name)
* console.log(worktreePort.port)
* }
* ```
*/
async getWorktreePort(): Promise<{ worktree: GitWorktree; port: number } | null> {
const worktree = await getGitWorktree(fileURLToPath(this.app.appRoot))
if (!worktree) {
return null
}

const basePort = await getBasePort(this.app.appRoot)
return { worktree, port: computeWorktreePort(worktree.name, basePort) }
}

/**
* The prepare template method is used to prepare the
* state for the command. This is the first method
Expand Down Expand Up @@ -205,6 +238,21 @@ export class ListCommand extends AceListCommand implements BaseCommand {
return new Codemods(this.app, this.logger)
}

/**
* Returns the linked git worktree in which the application is running,
* alongside a deterministic port computed from the worktree name. Returns
* "null" when the application is not running inside a linked git worktree.
*/
async getWorktreePort(): Promise<{ worktree: GitWorktree; port: number } | null> {
const worktree = await getGitWorktree(fileURLToPath(this.app.appRoot))
if (!worktree) {
return null
}

const basePort = await getBasePort(this.app.appRoot)
return { worktree, port: computeWorktreePort(worktree.name, basePort) }
}

/**
* Terminate the app. A command should prefer calling this method
* over the "app.terminate", because this method only triggers
Expand Down
Loading