Skip to content
Merged

Fixes #890

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 .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v18.20.4
v24.4.1
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:24.1.0-alpine3.21 AS builder
FROM node:24.4.1-alpine3.22 AS builder

RUN apk update --no-cache && apk add --no-cache git

Expand All @@ -21,13 +21,14 @@ ARG SANDBOX_HOST_NAME
ARG SANDBOX_PORT
ARG FIREBASE_CONFIG
ARG DOCS_BASE_URL
ARG NODE_OPTIONS

Comment on lines +24 to 25
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

NODE_OPTIONS build arg is never applied

Declaring ARG NODE_OPTIONS alone doesn’t expose it to the build environment, so npm ci/npm run build still run with the default heap limit. If the goal was to avoid Node OOMs during the build, this change has no effect. Please export it (or drop the arg). Example fix:

 ARG NODE_OPTIONS
+ENV NODE_OPTIONS=${NODE_OPTIONS}
🤖 Prompt for AI Agents
In Dockerfile around lines 24-25, ARG NODE_OPTIONS is declared but never applied
to the image environment so build steps still use default Node heap; change to
export the value by adding ENV NODE_OPTIONS=${NODE_OPTIONS} (or set a default
ARG and then ENV) so subsequent RUN npm ci / npm run build inherit the intended
NODE_OPTIONS, or remove the ARG if not needed.

RUN if [ "$DOCS_BASE_URL" == "null" ]; \
then npm run build:app; \
else npm run build; \
fi

FROM node:24.1.0-alpine3.21 AS server
FROM node:24.4.1-alpine3.22 AS server

RUN addgroup -S appgroup
RUN adduser -S appuser -G appgroup
Expand Down
27 changes: 19 additions & 8 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ services:
- SANDBOX_PORT=${SANDBOX_PORT:-8090}
- FIREBASE_CONFIG=${FIREBASE_CONFIG:-}
- DOCS_BASE_URL=${DOCS_BASE_URL:-null}
- LOCAL_MODULES=${LOCAL_MODULES:-false}
- NODE_OPTIONS=--max-old-space-size=4096
restart: unless-stopped
environment:
- SELF_HOSTED=true
Expand All @@ -26,6 +28,7 @@ services:
- LOG_URL=${LOG_URL:-null}
- VALKEY_HOST=valkey
- VALKEY_PORT=6379
- NODE_OPTIONS=--max-old-space-size=4096
volumes:
- ./assets:/srv/build/assets
depends_on:
Expand All @@ -38,21 +41,29 @@ services:
- valkey-data:/data
command:
[
'sh',
'-c',
"sh",
"-c",
'if [ "$SELF_HOSTED_SHARE" != "false" ]; then valkey-server --save 60 1 --loglevel warning; fi',
]

server:
image: caddy:2.10.0-alpine
entrypoint: ['/bin/sh', './entrypoint.sh']
command: ['caddy', 'run', '--config', '/etc/caddy/Caddyfile', '--adapter', 'caddyfile']
entrypoint: ["/bin/sh", "./entrypoint.sh"]
command:
[
"caddy",
"run",
"--config",
"/etc/caddy/Caddyfile",
"--adapter",
"caddyfile",
]
restart: unless-stopped
ports:
- '80:80'
- '${PORT:-443}:${PORT:-443}'
- '${SANDBOX_PORT:-8090}:${SANDBOX_PORT:-8090}'
- '${BROADCAST_PORT:-3030}:${BROADCAST_PORT:-3030}'
- "80:80"
- "${PORT:-443}:${PORT:-443}"
- "${SANDBOX_PORT:-8090}:${SANDBOX_PORT:-8090}"
- "${BROADCAST_PORT:-3030}:${BROADCAST_PORT:-3030}"
environment:
- HOST_NAME=${HOST_NAME:-livecodes.localhost}
- PORT=${PORT:-443}
Expand Down
8 changes: 4 additions & 4 deletions server/src/broadcast/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,17 @@ export const broadcast = ({
});
});

app.get('/channels/:id', (req, res) => {
app.get('/channels/:id', async (req, res) => {
const channel = req.params.id;
if (channels[channel]) {
channels[channel].lastAccessed = Date.now();
const hasData = Object.keys(channels[channel].data || {}).length > 0;
const views = ['index', 'code', 'result'] as const;
const view = req.query.view;
const file = views.find((v) => v === view) || (hasData ? 'index' : 'result');
const fileContent = fs
.readFileSync(path.join(broadcastDir, `/${file}.html`), 'utf-8')
.replaceAll('{{AppUrl}}', appUrl);
const fileContent = (
await fs.promises.readFile(path.join(broadcastDir, `/${file}.html`), 'utf-8')
).replaceAll('{{AppUrl}}', appUrl);
res.status(200).send(fileContent);
} else {
res.status(404).send('Channel not found!');
Expand Down
36 changes: 21 additions & 15 deletions server/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,47 @@ import cors from 'cors';
import express from 'express';
import fs from 'node:fs';
import path from 'node:path';
import { sandboxVersion } from '../../src/livecodes/html/sandbox/index.ts';
import { dirname } from './utils.ts';

export const sandbox = ({ hostname, port }: { hostname: string; port: number }) => {
export const sandbox = async ({ hostname, port }: { hostname: string; port: number }) => {
const app = express();

app.use(cors());
app.disable('x-powered-by');

const sandboxDir = path.resolve(dirname, 'sandbox');
let sandboxVersionDir = path.resolve(sandboxDir, sandboxVersion);
fs.readdirSync(sandboxDir).forEach((v) => {
if (fs.statSync(path.resolve(sandboxDir, v)).isDirectory()) {
sandboxVersionDir = path.resolve(sandboxDir, v);
}
});
const dirs = await fs.promises.readdir(sandboxDir);
const version =
dirs
.filter((v) => v.startsWith('v'))
.map((v) => Number(v.slice(1)))
.filter((v) => !Number.isNaN(v))
.sort((a, b) => b - a)
.map((v) => 'v' + v)
.pop() || '';
const sandboxVersionDir = path.resolve(sandboxDir, version);
Comment on lines +16 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Latest sandbox version detection is inverted

After sorting versions in descending order, calling .pop() returns the smallest entry, so the sandbox now serves the oldest bundle whenever multiple v* folders exist. Use .shift() (or grab index 0) to keep the newest directory.

-  const version =
-    dirs
-      .filter((v) => v.startsWith('v'))
-      .map((v) => Number(v.slice(1)))
-      .filter((v) => !Number.isNaN(v))
-      .sort((a, b) => b - a)
-      .map((v) => 'v' + v)
-      .pop() || '';
+  const version =
+    dirs
+      .filter((v) => v.startsWith('v'))
+      .map((v) => Number(v.slice(1)))
+      .filter((v) => !Number.isNaN(v))
+      .sort((a, b) => b - a)
+      .map((v) => 'v' + v)[0] || '';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const version =
dirs
.filter((v) => v.startsWith('v'))
.map((v) => Number(v.slice(1)))
.filter((v) => !Number.isNaN(v))
.sort((a, b) => b - a)
.map((v) => 'v' + v)
.pop() || '';
const sandboxVersionDir = path.resolve(sandboxDir, version);
const version =
dirs
.filter((v) => v.startsWith('v'))
.map((v) => Number(v.slice(1)))
.filter((v) => !Number.isNaN(v))
.sort((a, b) => b - a)
.map((v) => 'v' + v)[0] || '';
const sandboxVersionDir = path.resolve(sandboxDir, version);
🤖 Prompt for AI Agents
In server/src/sandbox.ts around lines 16 to 24, the code sorts version numbers
in descending order but then calls .pop(), which returns the smallest (oldest)
version; change that to .shift() (or use [0]) so the newest version is selected,
and keep the existing fallback to '' for the sandboxVersionDir resolution.


app.use('/', (req, res) => {
if (req.path === '/') {
res.set('Content-Type', 'text/html');
res.status(200).sendFile(path.resolve(sandboxVersionDir, 'index.html'));
return;
}
const reqPath = req.path.endsWith('/')
let reqPath = req.path.endsWith('/')
? req.path + 'index.html'
: !req.path.split('/').pop()?.includes('.')
? req.path + '.html'
: req.path;
res.set('Content-Type', 'text/html');
const filePath = path.resolve(dirname, 'sandbox' + reqPath);
if (fs.existsSync(filePath)) {
res.status(200).sendFile(filePath);
return;
if (reqPath.startsWith('/')) {
reqPath = reqPath.slice(1);
}
res.status(404).sendFile(path.resolve(sandboxVersionDir, 'index.html'));
const filePath = path.resolve(sandboxDir, reqPath);
const onError = (_err: unknown) => {
if (res.headersSent) return;
res.status(404).sendFile(path.resolve(sandboxVersionDir, 'index.html'));
};
res.set('Content-Type', 'text/html');
res.status(200).sendFile(filePath, onError);
});

app.listen(port, () => {
Expand Down
11 changes: 5 additions & 6 deletions server/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const getDirname = (metaUrl: string) => path.dirname(fileURLToPath(metaUrl));
export const dirname = getDirname(import.meta.url);
export const appDir = path.resolve(dirname, '../../build/');

const getFileContent = async (fullUrl: string) => {
const getFileContent = (fullUrl: string): Promise<string> => {
let pathname: string;
try {
const url = new URL(fullUrl);
Expand All @@ -21,11 +21,10 @@ const getFileContent = async (fullUrl: string) => {
if (!pathname.trim()) {
pathname = 'index.html';
}
let filePath = path.resolve(appDir, pathname);
if (!fs.existsSync(filePath)) {
filePath = path.resolve(appDir, '404.html');
}
return fs.promises.readFile(filePath, 'utf8');
const filePath = path.resolve(appDir, pathname);
return fs.promises
.readFile(filePath, 'utf8')
.catch(() => fs.promises.readFile(path.resolve(appDir, '404.html'), 'utf8'));
};

const convertToWebRequest = (req: express.Request) => {
Expand Down
2 changes: 1 addition & 1 deletion src/livecodes/styles/inc-menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ i.arrow {
width: var(--s16);

&[for='theme-color-custom'] {
background: conic-gradient(in hsl longer hue, red 0 0);
background: conic-gradient(in hsl longer hue, red 0 100%);
filter: contrast(0.5);
}

Expand Down