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
2 changes: 1 addition & 1 deletion contents/docs/installation/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Click your operating system below to access the specific installation steps and
Compatible with Windows 10, Windows 11, and modern Windows Server architectures.

### <img src="/assets/platform-icons/macos.png" width="32" height="32" className="not-prose" style={{ display: 'inline-block', margin: '0 8px 4px 0', verticalAlign: 'middle' }} /> [macOS (Apple Silicon)](./installation/mac)
Built for ARM64 architectures natively (M1, M2, M3, M4). *Note: Intel Macs are explicitly unsupported.*
Built for ARM64 architectures natively (Apple M-Series). *Note: Intel Macs are explicitly unsupported.*

### <img src="/assets/platform-icons/linux.png" width="32" height="32" className="not-prose" style={{ display: 'inline-block', margin: '0 8px 4px 0', verticalAlign: 'middle' }} /> [Linux (AppImage)](./installation/linux)
Portable AppImage binary compatible with Ubuntu 20.04+ and most modern Debian-based distributions.
6 changes: 3 additions & 3 deletions contents/docs/installation/linux/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This guide explains how to install and run the **Robonito** application on Linux

Unlike traditional `.deb` packages, an AppImage is a fully portable software format that contains all dependencies in a single file without requiring a system-level installation.

📥 **[Download Robonito for Linux (v2.5.0)](https://robonito-prod-builds.s3.ap-south-1.amazonaws.com/Robonito-2.5.0.AppImage)**
📥 **[Download Robonito for Linux (v{{version}})](https://{{linuxBucketName}}.s3.{{region}}.amazonaws.com/Robonito-{{version}}.AppImage)**

---

Expand All @@ -30,7 +30,7 @@ Before you can run the AppImage, you must mark it as an executable binary on you
```
2. Grant execution rights using `chmod`:
```bash
chmod +x Robonito-2.5.0.AppImage
chmod +x Robonito-{{version}}.AppImage
```

**Via GUI (File Manager):**
Expand All @@ -43,7 +43,7 @@ Alternatively, right-click the file, select **Properties**, go to the **Permissi
Once permissions are properly set, launch the application via your terminal:

```bash
./Robonito-2.5.0.AppImage
./Robonito-{{version}}.AppImage
```

Alternatively, simply double-click the file tightly inside your visual Desktop Environment to start the Robonito client.
2 changes: 1 addition & 1 deletion contents/docs/installation/mac/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Check your hardware via `Apple Menu > About This Mac` before downloading the sof

Download the official Robonito disk image file specifically compiled for macOS ARM64 architectures:

📥 **[Download Robonito for macOS (v2.5.0)](https://robonito-prod-builds.s3.ap-south-1.amazonaws.com/Robonito-2.5.0-arm64.dmg)**
📥 **[Download Robonito for macOS (v{{version}})](https://{{macBucketName}}.s3.{{region}}.amazonaws.com/Robonito-{{version}}-arm64.dmg)**

---

Expand Down
4 changes: 2 additions & 2 deletions contents/docs/installation/window/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ This guide explains how to install the **Robonito** desktop application on Windo

Download the latest Robonito setup executable directly from our secure build server:

📥 **[Download Robonito for Windows (v2.5.0)](https://robonito-prod-builds.s3.ap-south-1.amazonaws.com/Robonito-Setup-2.5.0.exe)**
📥 **[Download Robonito for Windows (v{{version}})](https://{{winBucketName}}.s3.{{region}}.amazonaws.com/Robonito-Setup-{{version}}.exe)**

---

## 2. Execute the Setup Wizard

1. Locate the downloaded `Robonito-Setup-2.5.0.exe` file in your Downloads folder.
1. Locate the downloaded `Robonito-Setup-{{version}}.exe` file in your Downloads folder.
2. Double-click the file to launch the installation wizard.

### Handling Windows SmartScreen Warnings
Expand Down
2 changes: 1 addition & 1 deletion contents/docs/web-testing/create-web-test/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This guide will walk you through the process of setting up and creating a new We

In Robonito, tests are systematically organized inside containers called **Test Suites**. Suites help group related functional behaviors together (e.g., "E-commerce Checkout", "User Profile Management").

When automating web applications, ensure your test case is housed inside a designated **Web** Test Suite.
When automating web applications, it is recommended to put your Test Cases inside a designated **Web** Test Suite.

---

Expand Down
7 changes: 7 additions & 0 deletions download-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version": "4.1.0",
"region": "ap-south-1",
"linuxBucketName": "robonito-downloads",
"macBucketName": "robonito-downloads",
"winBucketName": "robonito-downloads"
}
30 changes: 28 additions & 2 deletions lib/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,38 @@ type BaseMdxFrontmatter = {
description: string;
};

let cachedConfig: Record<string, string> | null = null;

async function loadDownloadConfig(): Promise<Record<string, string>> {
if (cachedConfig) return cachedConfig;
try {
const configPath = path.join(process.cwd(), "download-config.json");
const content = await fs.readFile(configPath, "utf-8");
cachedConfig = JSON.parse(content);
return cachedConfig || {};
} catch (err) {
console.error("Failed to load download-config.json", err);
return {};
}
}
Comment on lines +63 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When multiple documentation pages are requested concurrently, loadDownloadConfig can trigger multiple concurrent fs.readFile operations because cachedConfig is only assigned after the file read and JSON parsing complete. Caching the promise instead of the resolved configuration object avoids this race condition and ensures the file is read at most once.

let cachedConfigPromise: Promise<Record<string, string>> | null = null;

function loadDownloadConfig(): Promise<Record<string, string>> {
  if (!cachedConfigPromise) {
    cachedConfigPromise = (async () => {
      try {
        const configPath = path.join(process.cwd(), "download-config.json");
        const content = await fs.readFile(configPath, "utf-8");
        return JSON.parse(content) || {};
      } catch (err) {
        console.error("Failed to load download-config.json", err);
        return {};
      }
    })();
  }
  return cachedConfigPromise;
}


function replacePlaceholders(content: string, config: Record<string, string>): string {
return content.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const trimmedKey = key.trim();
return trimmedKey in config ? config[trimmedKey] : match;
});
}
Comment on lines +78 to +83

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using the in operator (trimmedKey in config) checks the entire prototype chain of the object. If a placeholder matches a built-in property name (such as toString, valueOf, or constructor), it will evaluate to true and attempt to replace the placeholder with a function or prototype property, leading to unexpected output or runtime errors. Checking typeof config[trimmedKey] === "string" is safer and ensures only explicitly defined string properties from the JSON configuration are replaced.

Suggested change
function replacePlaceholders(content: string, config: Record<string, string>): string {
return content.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const trimmedKey = key.trim();
return trimmedKey in config ? config[trimmedKey] : match;
});
}
function replacePlaceholders(content: string, config: Record<string, string>): string {
return content.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const trimmedKey = key.trim();
return typeof config[trimmedKey] === "string" ? config[trimmedKey] : match;
});
}


export async function getDocsForSlug(slug: string) {
// skip assets to prevent ENOENT errors on index.mdx
const assetExtensions = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".css", ".js"];
if (assetExtensions.some(ext => slug.endsWith(ext))) return null;

try {
const contentPath = getDocsContentPath(slug);
const rawMdx = await fs.readFile(contentPath, "utf-8");
let rawMdx = await fs.readFile(contentPath, "utf-8");
const downloadConfig = await loadDownloadConfig();
rawMdx = replacePlaceholders(rawMdx, downloadConfig);
const res = await parseMdx<BaseMdxFrontmatter>(rawMdx);

// If title/description are missing from frontmatter, extract from content
Expand Down Expand Up @@ -99,7 +123,9 @@ function stripMdx(text: string) {

export async function getDocsTocs(slug: string) {
const contentPath = getDocsContentPath(slug);
const rawMdx = await fs.readFile(contentPath, "utf-8");
let rawMdx = await fs.readFile(contentPath, "utf-8");
const downloadConfig = await loadDownloadConfig();
rawMdx = replacePlaceholders(rawMdx, downloadConfig);
// captures between ## - #### can modify accordingly
const headingsRegex = /^(#{2,4})\s(.+)$/gm;
let match;
Expand Down