Skip to content

Instantly share code, notes, and snippets.

@kakashy
Created October 9, 2024 05:00
Show Gist options
  • Save kakashy/34a8c522f96ee0102baa3acc8f809330 to your computer and use it in GitHub Desktop.
Save kakashy/34a8c522f96ee0102baa3acc8f809330 to your computer and use it in GitHub Desktop.
This Go script streamlines the process of Dockerizing SvelteKit projects by automating the creation of necessary Docker configuration files. By detecting environment variables and incorporating them as build arguments, it ensures that the Docker build process has access to all required configurations without compromising security.
package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
// PackageJSON represents the structure of package.json
type PackageJSON struct {
Scripts map[string]string `json:"scripts"`
}
// SvelteKitProject holds information about each SvelteKit project
type SvelteKitProject struct {
Path string
PreviewPort string
EnvVars map[string]string
}
// DefaultPort is used if no port is specified in the preview script
const DefaultPort = "3000"
func main() {
currentDir, err := os.Getwd()
if err != nil {
fmt.Printf("Error getting current directory: %v\n", err)
return
}
// Find all SvelteKit projects (current dir and one level deep)
projects, err := findSvelteKitProjects(currentDir)
if err != nil {
fmt.Printf("Error finding SvelteKit projects: %v\n", err)
return
}
if len(projects) == 0 {
fmt.Println("No SvelteKit repositories found.")
return
}
for _, project := range projects {
fmt.Printf("SvelteKit repository detected: %s\n", project.Path)
// Create .dockerignore
err := createDockerignore(project.Path)
if err != nil {
fmt.Printf("Error creating .dockerignore in %s: %v\n", project.Path, err)
continue
}
fmt.Printf(".dockerignore created successfully in %s.\n", project.Path)
// Create Dockerfile
err = createDockerfile(project)
if err != nil {
fmt.Printf("Error creating Dockerfile in %s: %v\n", project.Path, err)
continue
}
fmt.Printf("Dockerfile created successfully in %s.\n", project.Path)
}
// Optionally, create a docker-compose.yml if multiple projects are found
if len(projects) > 1 {
err = createDockerCompose(projects, currentDir)
if err != nil {
fmt.Printf("Error creating docker-compose.yml: %v\n", err)
return
}
fmt.Println("docker-compose.yml created successfully.")
}
// Generate build.sh script
if len(projects) > 0 {
err = generateBuildScript(projects, currentDir)
if err != nil {
fmt.Printf("Error creating build.sh: %v\n", err)
return
}
fmt.Println("build.sh created successfully. Remember to make it executable with `chmod +x build.sh`.")
}
}
// findSvelteKitProjects searches for SvelteKit projects in the current directory and its immediate subdirectories
func findSvelteKitProjects(root string) ([]SvelteKitProject, error) {
var projects []SvelteKitProject
entries, err := os.ReadDir(root)
if err != nil {
return nil, err
}
// Include the root directory itself
candidateDirs := []string{root}
// Add immediate subdirectories, excluding node_modules and .git
for _, entry := range entries {
if entry.IsDir() && entry.Name() != "node_modules" && entry.Name() != ".git" {
candidateDirs = append(candidateDirs, filepath.Join(root, entry.Name()))
}
}
for _, dir := range candidateDirs {
isSvelteKit, previewPort, err := checkSvelteKitRepo(dir)
if err != nil {
fmt.Printf("Error checking directory %s: %v\n", dir, err)
continue
}
if isSvelteKit {
envVars, err := parseEnvFile(dir)
if err != nil {
fmt.Printf("Error parsing .env in %s: %v\n", dir, err)
// Continue without envVars
}
projects = append(projects, SvelteKitProject{
Path: dir,
PreviewPort: previewPort,
EnvVars: envVars,
})
}
}
return projects, nil
}
// checkSvelteKitRepo checks if a directory is a SvelteKit project and extracts the preview port
func checkSvelteKitRepo(dir string) (bool, string, error) {
svelteConfigPath := filepath.Join(dir, "svelte.config.js")
if _, err := os.Stat(svelteConfigPath); os.IsNotExist(err) {
return false, DefaultPort, nil
}
packageJSONPath := filepath.Join(dir, "package.json")
data, err := os.ReadFile(packageJSONPath)
if err != nil {
if os.IsNotExist(err) {
return false, DefaultPort, nil
}
return false, DefaultPort, err
}
var pkg PackageJSON
err = json.Unmarshal(data, &pkg)
if err != nil {
return false, DefaultPort, err
}
// Extract port from the "preview" script
previewPort := extractPort(pkg.Scripts["preview"])
return true, previewPort, nil
}
// extractPort parses the preview script to find the port number
func extractPort(script string) string {
// Default port if not found
port := DefaultPort
if script == "" {
return port
}
// Regex to match --port=NUMBER or --port NUMBER
re := regexp.MustCompile(`--port\s*=?\s*(\d+)`)
matches := re.FindStringSubmatch(script)
if len(matches) >= 2 {
fmt.Printf("Found custom preview port: %s\n", matches[1])
if _, err := strconv.Atoi(matches[1]); err == nil {
port = matches[1]
} else {
fmt.Printf("Invalid port number '%s' in preview script. Using default port %s.\n", matches[1], DefaultPort)
}
}
return port
}
// parseEnvFile checks for a .env file and parses its contents into a map
func parseEnvFile(dir string) (map[string]string, error) {
envPath := filepath.Join(dir, ".env")
if _, err := os.Stat(envPath); os.IsNotExist(err) {
// No .env file found
return nil, nil
}
file, err := os.Open(envPath)
if err != nil {
return nil, err
}
defer file.Close()
envVars := make(map[string]string)
scanner := bufio.NewScanner(file)
re := regexp.MustCompile(`^\s*([^#\s][A-Za-z0-9_]+)\s*=\s*(.*)\s*$`)
for scanner.Scan() {
line := scanner.Text()
matches := re.FindStringSubmatch(line)
if len(matches) == 3 {
key := matches[1]
value := matches[2]
// Remove surrounding quotes if present
value = strings.Trim(value, `"'`)
envVars[key] = value
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
if len(envVars) > 0 {
fmt.Printf("Found %d environment variables in .env\n", len(envVars))
}
return envVars, nil
}
// createDockerignore generates a .dockerignore file in the specified directory
func createDockerignore(dir string) error {
dockerignorePath := filepath.Join(dir, ".dockerignore")
file, err := os.Create(dockerignorePath)
if err != nil {
return err
}
defer file.Close()
dockerignoreContent := `node_modules
.git
*.log
*.env
.DS_Store
`
_, err = file.WriteString(dockerignoreContent)
return err
}
// createDockerfile generates a Dockerfile for a SvelteKit project using Bun's official image
func createDockerfile(project SvelteKitProject) error {
dockerfilePath := filepath.Join(project.Path, "Dockerfile")
file, err := os.Create(dockerfilePath)
if err != nil {
return err
}
defer file.Close()
var buildArgs string
var envInstructions string
// If there are environment variables, declare them as build args and set as env
if len(project.EnvVars) > 0 {
for key := range project.EnvVars {
// Declare build arg
buildArgs += fmt.Sprintf("ARG %s\n", key)
// Set env variable
envInstructions += fmt.Sprintf("ENV %s=${%s}\n", key, key)
}
}
dockerfileContent := fmt.Sprintf(`# Use Bun's official lightweight image
FROM oven/bun:latest
# Set working directory
WORKDIR /app
# Declare build arguments if any
%s
# Set environment variables from build arguments
%s
# Copy package files
COPY package*.json ./
# Install dependencies using Bun
RUN bun install
# Copy the rest of the application
COPY . .
# Build the application
RUN bun run build
# Expose the port (from package.json or default 3000)
EXPOSE %s
# Start the application
CMD ["bun", "run", "preview"]
`, buildArgs, envInstructions, project.PreviewPort)
_, err = file.WriteString(dockerfileContent)
return err
}
// createDockerCompose generates a docker-compose.yml for multiple SvelteKit projects
func createDockerCompose(projects []SvelteKitProject, root string) error {
var services []string
for i, project := range projects {
serviceName := fmt.Sprintf("service%d", i+1)
port := project.PreviewPort
relativePath, err := filepath.Rel(root, project.Path)
if err != nil {
return err
}
// Prepare environment variables section
var envSection string
if len(project.EnvVars) > 0 {
envSection = " environment:\n"
for key := range project.EnvVars {
envSection += fmt.Sprintf(" - %s=${%s}\n", key, key)
}
}
// Define the service
service := fmt.Sprintf(`
%s:
build:
context: ./%s
dockerfile: Dockerfile
ports:
- "%s:%s"
restart: unless-stopped
%s
`, serviceName, relativePath, port, port, envSection)
services = append(services, service)
}
dockerComposeContent := `version: '3'
services:` + strings.Join(services, "\n")
dockerComposePath := filepath.Join(root, "docker-compose.yml")
return os.WriteFile(dockerComposePath, []byte(dockerComposeContent), 0644)
}
// generateBuildScript creates a build.sh script that passes environment variables as build args
func generateBuildScript(projects []SvelteKitProject, root string) error {
var lines []string
lines = append(lines, "#!/bin/bash\n")
lines = append(lines, "set -e\n\n")
for _, project := range projects {
projectName := filepath.Base(project.Path)
lines = append(lines, fmt.Sprintf("echo 'Building %s...'\n", projectName))
lines = append(lines, fmt.Sprintf("docker build \\\n"))
for key, value := range project.EnvVars {
lines = append(lines, fmt.Sprintf(" --build-arg %s=\"%s\" \\\n", key, escapeShellArg(value)))
}
lines = append(lines, fmt.Sprintf(" -t %s:latest %s\n\n", projectName, filepath.Base(project.Path)))
}
// Write to build.sh
buildScriptPath := filepath.Join(root, "build.sh")
file, err := os.Create(buildScriptPath)
if err != nil {
return err
}
defer file.Close()
writer := bufio.NewWriter(file)
for _, line := range lines {
_, err := writer.WriteString(line)
if err != nil {
return err
}
}
return writer.Flush()
}
// escapeShellArg escapes double quotes in shell arguments
func escapeShellArg(arg string) string {
return strings.ReplaceAll(arg, `"`, `\"`)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment