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
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
{
"caches": {
"apt": {
"directory": "/var/cache/apt",
"type": "locked"
},
"apt-lists": {
"directory": "/var/lib/apt/lists",
"type": "locked"
},
"next": {
"directory": "/app/.next/cache",
"type": "shared"
},
"node-modules": {
"directory": "/app/node_modules/.cache",
"type": "shared"
},
"npm-install": {
"directory": "/root/.npm",
"type": "shared"
}
},
"deploy": {
"base": {
"step": "packages:apt:runtime"
},
"inputs": [
{
"include": [
"/railpack/caddy"
],
"step": "packages:caddy"
},
{
"include": [
"/Caddyfile"
],
"step": "caddy"
},
{
"include": [
"out"
],
"step": "build"
}
],
"startCommand": "caddy run --config /Caddyfile --adapter caddyfile 2\u003e\u00261",
"variables": {
"CI": "true",
"NODE_ENV": "production",
"NPM_CONFIG_FUND": "false",
"NPM_CONFIG_PRODUCTION": "false",
"NPM_CONFIG_UPDATE_NOTIFIER": "false"
}
},
"steps": [
{
"assets": {
"generated-mise-toml": "[generated-mise-toml]"
},
"commands": [
{
"path": "/mise/shims"
},
{
"customName": "create mise config",
"name": "generated-mise-toml",
"path": "/etc/mise/config.toml"
},
{
"cmd": "mise install",
"customName": "install mise packages: node"
}
],
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder:mise-2026.3.16"
}
],
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
"MISE_DATA_DIR": "/mise",
"MISE_INSTALLS_DIR": "/mise/installs",
"MISE_SHIMS_DIR": "/mise/shims"
}
},
{
"caches": [
"npm-install"
],
"commands": [
{
"path": "/app/node_modules/.bin"
},
{
"cmd": "mkdir -p /app/node_modules/.cache"
},
{
"dest": "package-lock.json",
"src": "package-lock.json"
},
{
"dest": "package.json",
"src": "package.json"
},
{
"cmd": "npm ci"
}
],
"inputs": [
{
"step": "packages:mise"
}
],
"name": "install",
"variables": {
"CI": "true",
"NODE_ENV": "production",
"NPM_CONFIG_FUND": "false",
"NPM_CONFIG_PRODUCTION": "false",
"NPM_CONFIG_UPDATE_NOTIFIER": "false"
}
},
{
"caches": [
"node-modules",
"next"
],
"commands": [
{
"cmd": "npm run build"
}
],
"inputs": [
{
"step": "install"
},
{
"include": [
"."
],
"local": true
}
],
"name": "build",
"secrets": [
"*"
],
"variables": {
"NEXT_TELEMETRY_DISABLED": "1"
}
},
{
"commands": [
{
"cmd": "mise install-into caddy@2.11.2 /railpack/caddy"
},
{
"path": "/railpack/caddy"
},
{
"path": "/railpack/caddy/bin"
}
],
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder:mise-2026.3.16"
}
],
"name": "packages:caddy",
"variables": {
"MISE_PARANOID": "1"
}
},
{
"assets": {
"Caddyfile": "# global options\n{\n\tadmin off\n\tpersist_config off\n\tauto_https off\n\n\tlog {\n\t\tformat json\n\t}\n\n\tservers {\n\t\ttrusted_proxies static private_ranges 100.0.0.0/8 # trust railway's proxy\n\t}\n}\n\n# site block, listens on the $PORT environment variable, automatically assigned by railway\n:{$PORT:80} {\n\tlog {\n\t\tformat json\n\t}\n\n\trespond /health 200\n\n\t# Security headers\n\theader {\n\t\t# Prevent some browsers from MIME-sniffing a response away from the declared Content-Type\n\t\tX-Content-Type-Options \"nosniff\"\n\t\t# Remove Server header\n\t\t-Server\n\t}\n\n\t# serve from the 'dist' folder (Vite builds into the 'dist' folder)\n\troot * /app/out\n\n\t# Handle static files\n\tfile_server {\n\t\thide .git\n\t\thide .env*\n\t}\n\n\t# Compression with more formats\n\tencode {\n\t\tgzip\n\t\tzstd\n\t}\n\n\t# Try files with HTML extension and handle SPA routing\n\ttry_files {path} {path}.html {path}/index.html /index.html\n}\n"
},
"commands": [
{
"name": "Caddyfile",
"path": "/Caddyfile"
},
{
"cmd": "caddy fmt --overwrite /Caddyfile"
}
],
"inputs": [
{
"step": "packages:caddy"
}
],
"name": "caddy",
"secrets": [
"*"
]
},
{
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y libatomic1'",
"customName": "install apt packages: libatomic1"
}
],
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime:mise-2026.3.16"
}
],
"name": "packages:apt:runtime"
}
]
}
20 changes: 20 additions & 0 deletions core/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"gopkg.in/yaml.v2"
)

var ErrNoFileFound = errors.New("unable to find a matching file")

type App struct {
Source string
globCache map[string][]string
Expand Down Expand Up @@ -142,6 +144,24 @@ func (a *App) FindFilesWithContent(pattern string, regex *regexp.Regexp) []strin
return matches
}

// ReadFirstFileOf reads the contents of the first file that exists within the application source directory
func (a *App) ReadFirstFileOf(names ...string) (string, string, error) {
for _, name := range names {
if !a.HasFile(name) {
continue
}

contents, err := a.ReadFile(name)
if err != nil {
return "", "", err
}

return name, contents, nil
}

return "", "", ErrNoFileFound
}

// ReadFile reads the contents of a file within the application source directory
func (a *App) ReadFile(name string) (string, error) {
path := filepath.Join(a.Source, name)
Expand Down
23 changes: 9 additions & 14 deletions core/generate/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package generate

import (
"bytes"
"errors"
"fmt"
"text/template"

"github.com/railwayapp/railpack/core/app"
)

type TemplateFileResult struct {
Expand All @@ -14,21 +17,13 @@ type TemplateFileResult struct {
// TemplateFiles will look the first file that exists in the list of potential files and render it with the given data
// If no file is found, it will use the default contents and render it with the given data
func (c *GenerateContext) TemplateFiles(potentialFiles []string, defaultContents string, data map[string]any) (*TemplateFileResult, error) {
contents := defaultContents
filename := ""

for _, potentialFilename := range potentialFiles {
if c.App.HasFile(potentialFilename) {
c, err := c.App.ReadFile(potentialFilename)
if err != nil {
return nil, err
}

contents = c
filename = potentialFilename

break
filename, contents, err := c.App.ReadFirstFileOf(potentialFiles...)
if err != nil {
if !errors.Is(err, app.ErrNoFileFound) {
return nil, err
}

contents = defaultContents
}

tmpl, err := template.New(filename).Parse(contents)
Expand Down
12 changes: 2 additions & 10 deletions core/providers/java/gradle.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,8 @@ func (p *JavaProvider) gradleCache(ctx *generate.GenerateContext) string {
}

func (p *JavaProvider) readBuildGradle(ctx *generate.GenerateContext) string {
filePath := "build.gradle"
if !ctx.App.HasFile(filePath) {
filePath = "build.gradle.kts"
}
result, err := ctx.App.ReadFile(filePath)
if err != nil {
return ""
} else {
return result
}
_, result, _ := ctx.App.ReadFirstFileOf("build.gradle", "build.gradle.kts")
return result
}

func isUsingSpringBoot(buildGradle string) bool {
Expand Down
17 changes: 2 additions & 15 deletions core/providers/node/astro.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,8 @@ func (p *NodeProvider) getAstroOutputDirectory(ctx *generate.GenerateContext) st
}

func (p *NodeProvider) getAstroConfigFileContents(ctx *generate.GenerateContext) string {
configFile := ""

if ctx.App.HasFile("astro.config.mjs") {
contents, err := ctx.App.ReadFile("astro.config.mjs")
if err == nil {
configFile = contents
}
} else if ctx.App.HasFile("astro.config.ts") {
contents, err := ctx.App.ReadFile("astro.config.ts")
if err == nil {
configFile = contents
}
}

return configFile
_, contents, _ := ctx.App.ReadFirstFileOf("astro.config.mjs", "astro.config.ts")
return contents
}

func (p *NodeProvider) getAstroEnvVars() map[string]string {
Expand Down
48 changes: 48 additions & 0 deletions core/providers/node/next.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package node

import (
"regexp"
"strings"

"github.com/railwayapp/railpack/core/generate"
)

const (
DefaultNextOutputDirectory = "out"
DefaultNextStartCommand = "next start"
)

var nextConfigFiles = []string{
"next.config.js",
"next.config.mjs",
"next.config.ts",
}

func (p *NodeProvider) isNextSPA(ctx *generate.GenerateContext) bool {
if !p.isNext() {
return false
}

configFileContents := p.getNextConfigFileContents(ctx)
hasExportOutput := strings.Contains(configFileContents, "output: 'export'") || strings.Contains(configFileContents, "output: \"export\"")

return hasExportOutput
}

func (p *NodeProvider) getNextOutputDirectory(ctx *generate.GenerateContext) string {
configFileContents := p.getNextConfigFileContents(ctx)
if configFileContents != "" {
distDirRegex := regexp.MustCompile(`distDir:\s*['"](.+?)['"]`)
matches := distDirRegex.FindStringSubmatch(configFileContents)
if len(matches) > 1 {
return matches[1]
}
}

return DefaultNextOutputDirectory
}

func (p *NodeProvider) getNextConfigFileContents(ctx *generate.GenerateContext) string {
_, contents, _ := ctx.App.ReadFirstFileOf(nextConfigFiles...)
return contents
}
Loading