From 71fb9f8b345cc88eddd7bc334f9644bebf1313a6 Mon Sep 17 00:00:00 2001
From: Chris Raible <chris@ghost.org>
Date: Tue, 29 Oct 2024 13:03:49 -0700
Subject: [PATCH] Converted dev container's onCreateCommand to JavaScript
 (#21457)

no issue

- The onCreateCommand was previously a bash script, which made it a bit
more challenging to read and make changes to it. This commit converts it
to JavaScript so it will be easier to make updates to it.
---
 .devcontainer/createLocalConfig.js |  93 ---------------
 .devcontainer/devcontainer.json    |   2 +-
 .devcontainer/onCreateCommand.js   | 179 +++++++++++++++++++++++++++++
 .devcontainer/onCreateCommand.sh   |  18 ---
 4 files changed, 180 insertions(+), 112 deletions(-)
 delete mode 100644 .devcontainer/createLocalConfig.js
 create mode 100644 .devcontainer/onCreateCommand.js
 delete mode 100755 .devcontainer/onCreateCommand.sh

diff --git a/.devcontainer/createLocalConfig.js b/.devcontainer/createLocalConfig.js
deleted file mode 100644
index 5447868ddb..0000000000
--- a/.devcontainer/createLocalConfig.js
+++ /dev/null
@@ -1,93 +0,0 @@
-const fs = require('fs');
-const path = require('path');
-const assert = require('node:assert/strict');
-
-// Reads the config.local.json file and updates it with environments variables for devcontainer setup
-const configBasePath = path.join(__dirname, '..', 'ghost', 'core');
-const configFile = path.join(configBasePath, 'config.local.json');
-let originalConfig = {};
-if (fs.existsSync(configFile)) {
-    try {
-        // Backup the user's config.local.json file just in case
-        // This won't be used by Ghost but can be useful to switch back to local development
-        const backupFile = path.join(configBasePath, 'config.local-backup.json');
-        fs.copyFileSync(configFile, backupFile);
-
-        // Read the current config.local.json file into memory
-        const fileContent = fs.readFileSync(configFile, 'utf8');
-        originalConfig = JSON.parse(fileContent);
-    } catch (error) {
-        console.error('Error reading or parsing config file:', error);
-        process.exit(1);
-    }
-} else {
-    console.log('Config file does not exist. Creating a new one.');
-}
-
-let newConfig = {};
-// Change the url if we're in a codespace
-if (process.env.CODESPACES === 'true') {
-    assert.ok(process.env.CODESPACE_NAME, 'CODESPACE_NAME is not defined');
-    assert.ok(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN, 'GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN is not defined');
-    const url = `https://${process.env.CODESPACE_NAME}-2368.${process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}`;
-    newConfig.url = url;
-}
-
-newConfig.database = {
-    client: 'mysql2',
-    connection: {
-        host: 'mysql',
-        user: 'root',
-        password: 'root',
-        database: 'ghost'
-    }
-}
-newConfig.adapters = {
-    Redis: {
-        host: 'redis',
-        port: 6379
-    }
-}
-
-
-// Only update the mail settings if they aren't already set
-if (!originalConfig.mail && process.env.MAILGUN_SMTP_PASS && process.env.MAILGUN_SMTP_USER && process.env.MAILGUN_FROM_ADDRESS) {
-    newConfig.mail = {
-        transport: 'SMTP',
-        from: process.env.MAILGUN_FROM_ADDRESS,
-        options: {
-            service: 'Mailgun',
-            host: 'smtp.mailgun.org',
-            secure: true,
-            port: 465,
-            auth: {
-                user: process.env.MAILGUN_SMTP_USER,
-                pass: process.env.MAILGUN_SMTP_PASS
-            }
-        }
-    }
-}
-
-// Only update the bulk email settings if they aren't already set
-if (!originalConfig.bulkEmail && process.env.MAILGUN_API_KEY && process.env.MAILGUN_DOMAIN) {
-    newConfig.bulkEmail = {
-        mailgun: {
-            baseUrl: 'https://api.mailgun.net/v3',
-            apiKey: process.env.MAILGUN_API_KEY,
-            domain: process.env.MAILGUN_DOMAIN,
-            tag: 'bulk-email'
-        }
-    }
-}
-
-// Merge the original config with the new config
-const config = {...originalConfig, ...newConfig};
-
-// Write the updated config.local.json file
-try {
-    fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
-    console.log('Config file updated successfully.');
-} catch (error) {
-    console.error('Error writing config file:', error);
-    process.exit(1);
-}
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index bb4396cc18..ac0efdfc08 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -4,7 +4,7 @@
     "service": "ghost",
     "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
     "shutdownAction": "stopCompose",
-    "onCreateCommand": ["./.devcontainer/onCreateCommand.sh"],
+    "onCreateCommand": ["node", "./.devcontainer/onCreateCommand.js"],
     "updateContentCommand": ["git", "submodule", "update", "--init", "--recursive"],
     "postCreateCommand": ["yarn", "knex-migrator", "init"],
     "remoteEnv": {
diff --git a/.devcontainer/onCreateCommand.js b/.devcontainer/onCreateCommand.js
new file mode 100644
index 0000000000..72e7706131
--- /dev/null
+++ b/.devcontainer/onCreateCommand.js
@@ -0,0 +1,179 @@
+// This script is run in the Dev Container right after it is created
+// No dependencies are installed at this point so we can't use any npm packages
+const fs = require('fs');
+const path = require('path');
+const assert = require('node:assert/strict');
+const { execSync } = require('child_process');
+
+// Main function that runs all the setup steps
+function main() {
+    setupLocalConfig();
+    runCleanHard();
+    runInstall();
+    runSubmoduleUpdate();
+    runTypescriptBuild();
+}
+
+// Basic color constants for console output
+const colors = {
+    reset: '\x1b[0m',
+    bright: '\x1b[1m',
+    dim: '\x1b[2m',
+    red: '\x1b[31m',
+    green: '\x1b[32m',
+    yellow: '\x1b[33m',
+    blue: '\x1b[34m',
+    magenta: '\x1b[35m',
+    cyan: '\x1b[36m',
+};
+
+function log(message, color = colors.reset) {
+    console.log(`${color}${message}${colors.reset}`);
+}
+
+function logError(message, error) {
+    console.error(`${colors.red}${message}${colors.reset}`, error);
+}
+
+// Creates config.local.json file with the correct values for the devcontainer
+function setupLocalConfig() {
+    log('Setting up local config file...', colors.blue);
+    // Reads the config.local.json file and updates it with environments variables for devcontainer setup
+    const configBasePath = path.join(__dirname, '..', 'ghost', 'core');
+    const configFile = path.join(configBasePath, 'config.local.json');
+    let originalConfig = {};
+    if (fs.existsSync(configFile)) {
+        try {
+            // Backup the user's config.local.json file just in case
+            // This won't be used by Ghost but can be useful to switch back to local development
+            const backupFile = path.join(configBasePath, 'config.local-backup.json');
+            fs.copyFileSync(configFile, backupFile);
+
+            // Read the current config.local.json file into memory
+            const fileContent = fs.readFileSync(configFile, 'utf8');
+            originalConfig = JSON.parse(fileContent);
+        } catch (error) {
+            logError('Error reading or parsing config file:', error);
+            process.exit(1);
+        }
+    } else {
+        log('Config file does not exist. Creating a new one.', colors.dim);
+    }
+
+    let newConfig = {};
+    // Change the url if we're in a codespace
+    if (process.env.CODESPACES === 'true') {
+        assert.ok(process.env.CODESPACE_NAME, 'CODESPACE_NAME is not defined');
+        assert.ok(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN, 'GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN is not defined');
+        const url = `https://${process.env.CODESPACE_NAME}-2368.${process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}`;
+        newConfig.url = url;
+    }
+
+    newConfig.database = {
+        client: 'mysql2',
+        connection: {
+            host: 'mysql',
+            user: 'root',
+            password: 'root',
+            database: 'ghost'
+        }
+    }
+    newConfig.adapters = {
+        Redis: {
+            host: 'redis',
+            port: 6379
+        }
+    }
+
+    // Only update the mail settings if they aren't already set
+    if (!originalConfig.mail && process.env.MAILGUN_SMTP_PASS && process.env.MAILGUN_SMTP_USER && process.env.MAILGUN_FROM_ADDRESS) {
+        newConfig.mail = {
+            transport: 'SMTP',
+            from: process.env.MAILGUN_FROM_ADDRESS,
+            options: {
+                service: 'Mailgun',
+                host: 'smtp.mailgun.org',
+                secure: true,
+                port: 465,
+                auth: {
+                    user: process.env.MAILGUN_SMTP_USER,
+                    pass: process.env.MAILGUN_SMTP_PASS
+                }
+            }
+        }
+    }
+
+    // Only update the bulk email settings if they aren't already set
+    if (!originalConfig.bulkEmail && process.env.MAILGUN_API_KEY && process.env.MAILGUN_DOMAIN) {
+        newConfig.bulkEmail = {
+            mailgun: {
+                baseUrl: 'https://api.mailgun.net/v3',
+                apiKey: process.env.MAILGUN_API_KEY,
+                domain: process.env.MAILGUN_DOMAIN,
+                tag: 'bulk-email'
+            }
+        }
+    }
+
+    // Merge the original config with the new config
+    const config = {...originalConfig, ...newConfig};
+
+    // Write the updated config.local.json file
+    try {
+        fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
+        log('Config file updated successfully.', colors.dim);
+    } catch (error) {
+        logError('Error writing config file:', error);
+        process.exit(1);
+    }
+}
+
+// Deletes node_modules and clears yarn & nx caches
+function runCleanHard() {
+    try {
+        log('Cleaning up node_modules and yarn/nx caches...', colors.blue);
+        execSync('yarn clean:hard', { stdio: 'inherit' });
+        log('Successfully ran yarn clean:hard', colors.dim);
+    } catch (error) {
+        logError('Error running yarn clean:hard:', error);
+        process.exit(1);
+    }
+}
+
+// Installs dependencies
+function runInstall() {
+    try {
+        log('Installing dependencies...', colors.blue);
+        execSync('yarn install --frozen-lockfile', { stdio: 'inherit' });
+        log('Successfully ran yarn install', colors.dim);
+    } catch (error) {
+        logError('Error running yarn install:', error);
+        process.exit(1);
+    }
+}
+
+// Initializes and updates git submodules
+function runSubmoduleUpdate() {
+    try {
+        log('Updating git submodules...', colors.blue);
+        execSync('git submodule update --init --recursive', { stdio: 'inherit' });
+        log('Successfully ran git submodule update', colors.dim);
+    } catch (error) {
+        logError('Error running git submodule update:', error);
+        process.exit(1);
+    }
+}
+
+// Builds the typescript packages
+function runTypescriptBuild() {
+    try {
+        log('Building typescript packages...', colors.blue);
+        execSync('yarn nx run-many -t build:ts', { stdio: 'inherit' });
+        log('Successfully ran yarn nx run-many -t build:ts', colors.dim);
+    } catch (error) {
+        logError('Error running yarn nx run-many -t build:ts:', error);
+        process.exit(1);
+    }
+}
+
+main();
\ No newline at end of file
diff --git a/.devcontainer/onCreateCommand.sh b/.devcontainer/onCreateCommand.sh
deleted file mode 100755
index a227848dc5..0000000000
--- a/.devcontainer/onCreateCommand.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/bash
-
-set -e
-
-echo "Setting up local config file..."
-node .devcontainer/createLocalConfig.js
-
-echo "Cleaning up any previous installs..."
-yarn clean:hard
-
-echo "Installing dependencies..."
-yarn install
-
-echo "Updating git submodules..."
-git submodule update --init --recursive
-
-echo "Building typescript packages..."
-yarn nx run-many -t build:ts
\ No newline at end of file