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
1 change: 1 addition & 0 deletions registry/coder/modules/code-server/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ resource "coder_script" "code-server" {
FOLDER : var.folder,
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
ADDITIONAL_ARGS : var.additional_args,
PARSE_JSONC_JS : file("${path.module}/parse_jsonc_extensions.js"),
})
run_on_start = true

Expand Down
55 changes: 55 additions & 0 deletions registry/coder/modules/code-server/parse_jsonc_extensions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Parses a JSONC file and prints extension recommendations, one per line.
// Handles // comments, /* */ block comments (including multi-line), and trailing commas.
// Used by code-server and vscode-web modules to parse .vscode/extensions.json
// and .code-workspace files.
//
// Environment variables:
// FILE - path to the JSONC file
// QUERY - jq-style query: "recommendations" (default) or "extensions.recommendations"
var fs = require("fs");
var text = fs.readFileSync(process.env.FILE, "utf8");
var result = "";
var inString = false;
var i = 0;

while (i < text.length) {
if (inString) {
if (text[i] === "\\" && i + 1 < text.length) {
result += text.slice(i, i + 2);
i += 2;
continue;
}
if (text[i] === '"') inString = false;
result += text[i++];
} else {
if (text[i] === '"') {
inString = true;
result += text[i++];
continue;
}
if (text[i] === "/" && text[i + 1] === "/") {
while (i < text.length && text[i] !== "\n") i++;
continue;
}
if (text[i] === "/" && text[i + 1] === "*") {
i += 2;
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
i += 2;
continue;
}
result += text[i++];
}
}

result = result.replace(/,(\s*[\]}])/g, "$1");

Choose a reason for hiding this comment

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

P1 Badge Preserve string contents when removing trailing commas

The trailing-comma pass result.replace(/,(\s*[\]}])/g, "$1") is not string-aware, so it also rewrites valid string data that happens to contain ,] or ,}. In extensions.json, a recommendation like https://example.com/a,].vsix is mutated to https://example.com/a].vsix before JSON.parse, which leads to installing the wrong extension ID/URL (or a failed install); the same parser logic is duplicated for vscode-web, so both modules are affected.

Useful? React with 👍 / 👎.

var data = JSON.parse(result);
var query = process.env.QUERY || "recommendations";
var recommendations;
if (query === "extensions.recommendations") {
recommendations = (data.extensions && data.extensions.recommendations) || [];
} else {
recommendations = data.recommendations || [];
}
recommendations.forEach(function (e) {
console.log(e);
});
70 changes: 70 additions & 0 deletions registry/coder/modules/code-server/parse_jsonc_extensions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, expect, it } from "bun:test";
import { spawn, readableStreamToText } from "bun";
import { unlink } from "node:fs/promises";
import { join } from "node:path";

const PARSER = join(import.meta.dir, "parse_jsonc_extensions.js");
const TMP = join(import.meta.dir, "tmp_test.json");

async function parseExtensions(
json: string,
query?: string,
): Promise<string[]> {
await Bun.write(TMP, json);
try {
const proc = spawn([process.execPath, PARSER], {
env: { FILE: TMP, QUERY: query ?? "recommendations" },
stdout: "pipe",
stderr: "pipe",
});
const out = await readableStreamToText(proc.stdout);
const exitCode = await proc.exited;
if (exitCode !== 0) {
throw new Error(await readableStreamToText(proc.stderr));
}
return out.trim().split("\n").filter(Boolean);
} finally {
await unlink(TMP).catch(() => {});
}
}

describe("parse_jsonc_extensions", () => {
it("handles comments and trailing commas", async () => {
const result = await parseExtensions(`{
// line comment
"recommendations": [
"ms-python.python",
/* block comment */
"dbaeumer.vscode-eslint", // inline
],
}`);
expect(result).toEqual(["ms-python.python", "dbaeumer.vscode-eslint"]);
});

it("does not mangle URLs in strings", async () => {
const result = await parseExtensions(`{
"recommendations": [
"ms-python.python",
"https://example.com/custom.vsix"
]
}`);
expect(result).toEqual([
"ms-python.python",
"https://example.com/custom.vsix",
]);
});

it("handles .code-workspace format", async () => {
const result = await parseExtensions(
`{
"folders": [{"path": "."}],
"extensions": {
// Recommended
"recommendations": ["ms-python.python"],
},
}`,
"extensions.recommendations",
);
expect(result).toEqual(["ms-python.python"]);
});
});
28 changes: 16 additions & 12 deletions registry/coder/modules/code-server/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ function extension_installed() {
return 1
}

# Install each extension...
# Install extensions...
INSTALL_EXT_ARGS=()
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
# shellcheck disable=SC2066
for extension in "$${EXTENSIONLIST[@]}"; do
Expand All @@ -109,34 +110,37 @@ for extension in "$${EXTENSIONLIST[@]}"; do
continue
fi
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
output=$($CODE_SERVER "$EXTENSION_ARG" --force --install-extension "$extension")
INSTALL_EXT_ARGS+=(--install-extension "$extension")
done
# shellcheck disable=SC2170,SC2255
if [ $${#INSTALL_EXT_ARGS[@]} -gt 0 ]; then
output=$($CODE_SERVER "$EXTENSION_ARG" --force "$${INSTALL_EXT_ARGS[@]}")
if [ $? -ne 0 ]; then
echo "Failed to install extension: $extension: $output"
echo "Failed to install extensions: $output"
exit 1
fi
done
fi

if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
if ! command -v jq > /dev/null; then
echo "jq is required to install extensions from a workspace file."
exit 0
fi

WORKSPACE_DIR="$HOME"
if [ -n "${FOLDER}" ]; then
WORKSPACE_DIR="${FOLDER}"
fi

if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
# Use sed to remove single-line comments before parsing with jq
extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR"/.vscode/extensions.json | jq -r '.recommendations[]')
extensions=$(FILE="$WORKSPACE_DIR/.vscode/extensions.json" "${INSTALL_PREFIX}/lib/node" -e '${PARSE_JSONC_JS}')
INSTALL_EXT_ARGS=()
for extension in $extensions; do
if extension_installed "$extension"; then
continue
fi
$CODE_SERVER "$EXTENSION_ARG" --force --install-extension "$extension"
INSTALL_EXT_ARGS+=(--install-extension "$extension")
done
# shellcheck disable=SC2170,SC2255
if [ $${#INSTALL_EXT_ARGS[@]} -gt 0 ]; then
$CODE_SERVER "$EXTENSION_ARG" --force "$${INSTALL_EXT_ARGS[@]}"
fi
fi
fi

Expand Down
1 change: 1 addition & 0 deletions registry/coder/modules/vscode-web/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ resource "coder_script" "vscode-web" {
FOLDER : var.folder,
WORKSPACE : var.workspace,
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
PARSE_JSONC_JS : file("${path.module}/parse_jsonc_extensions.js"),
SERVER_BASE_PATH : local.server_base_path,
COMMIT_ID : var.commit_id,
PLATFORM : var.platform,
Expand Down
55 changes: 55 additions & 0 deletions registry/coder/modules/vscode-web/parse_jsonc_extensions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Parses a JSONC file and prints extension recommendations, one per line.
// Handles // comments, /* */ block comments (including multi-line), and trailing commas.
// Used by code-server and vscode-web modules to parse .vscode/extensions.json
// and .code-workspace files.
//
// Environment variables:
// FILE - path to the JSONC file
// QUERY - jq-style query: "recommendations" (default) or "extensions.recommendations"
var fs = require("fs");
var text = fs.readFileSync(process.env.FILE, "utf8");
var result = "";
var inString = false;
var i = 0;

while (i < text.length) {
if (inString) {
if (text[i] === "\\" && i + 1 < text.length) {
result += text.slice(i, i + 2);
i += 2;
continue;
}
if (text[i] === '"') inString = false;
result += text[i++];
} else {
if (text[i] === '"') {
inString = true;
result += text[i++];
continue;
}
if (text[i] === "/" && text[i + 1] === "/") {
while (i < text.length && text[i] !== "\n") i++;
continue;
}
if (text[i] === "/" && text[i + 1] === "*") {
i += 2;
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
i += 2;
continue;
}
result += text[i++];
}
}

result = result.replace(/,(\s*[\]}])/g, "$1");
var data = JSON.parse(result);
var query = process.env.QUERY || "recommendations";
var recommendations;
if (query === "extensions.recommendations") {
recommendations = (data.extensions && data.extensions.recommendations) || [];
} else {
recommendations = data.recommendations || [];
}
recommendations.forEach(function (e) {
console.log(e);
});
57 changes: 32 additions & 25 deletions registry/coder/modules/vscode-web/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,47 +92,54 @@ if [ $? -ne 0 ]; then
fi
printf "$${BOLD}VS Code Web has been installed.\n"

# Install each extension...
# Install extensions...
INSTALL_EXT_ARGS=()
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
# shellcheck disable=SC2066
for extension in "$${EXTENSIONLIST[@]}"; do
if [ -z "$extension" ]; then
continue
fi
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
output=$($VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force)
INSTALL_EXT_ARGS+=(--install-extension "$extension")
done
# shellcheck disable=SC2170,SC2255
if [ $${#INSTALL_EXT_ARGS[@]} -gt 0 ]; then
output=$($VSCODE_WEB "$EXTENSION_ARG" --force "$${INSTALL_EXT_ARGS[@]}")
if [ $? -ne 0 ]; then
echo "Failed to install extension: $extension: $output"
echo "Failed to install extensions: $output"
fi
done
fi

if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
if ! command -v jq > /dev/null; then
echo "jq is required to install extensions from a workspace file."
INSTALL_EXT_ARGS=()

# Prefer WORKSPACE if set and points to a .code-workspace file
if [ -n "${WORKSPACE}" ] && [ -f "${WORKSPACE}" ]; then
printf "🧩 Installing extensions from %s...\n" "${WORKSPACE}"
extensions=$(FILE="${WORKSPACE}" QUERY="extensions.recommendations" "${INSTALL_PREFIX}/node" -e '${PARSE_JSONC_JS}')
for extension in $extensions; do
INSTALL_EXT_ARGS+=(--install-extension "$extension")
done
else
# Prefer WORKSPACE if set and points to a file
if [ -n "${WORKSPACE}" ] && [ -f "${WORKSPACE}" ]; then
printf "🧩 Installing extensions from %s...\n" "${WORKSPACE}"
# Strip single-line comments then parse .extensions.recommendations[]
extensions=$(sed 's|//.*||g' "${WORKSPACE}" | jq -r '(.extensions.recommendations // [])[]')
# Fallback to folder-based .vscode/extensions.json (existing behavior)
WORKSPACE_DIR="$HOME"
if [ -n "${FOLDER}" ]; then
WORKSPACE_DIR="${FOLDER}"
fi
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
extensions=$(FILE="$WORKSPACE_DIR/.vscode/extensions.json" "${INSTALL_PREFIX}/node" -e '${PARSE_JSONC_JS}')
for extension in $extensions; do
$VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
INSTALL_EXT_ARGS+=(--install-extension "$extension")
done
else
# Fallback to folder-based .vscode/extensions.json (existing behavior)
WORKSPACE_DIR="$HOME"
if [ -n "${FOLDER}" ]; then
WORKSPACE_DIR="${FOLDER}"
fi
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR/.vscode/extensions.json" | jq -r '.recommendations[]')
for extension in $extensions; do
$VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
done
fi
fi
fi

# shellcheck disable=SC2170,SC2255
if [ $${#INSTALL_EXT_ARGS[@]} -gt 0 ]; then
$VSCODE_WEB "$EXTENSION_ARG" --force "$${INSTALL_EXT_ARGS[@]}"
fi
fi

run_vscode_web
Loading