Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to Express 5 #7251

Merged
merged 4 commits into from
Mar 7, 2025
Merged
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -22,6 +22,19 @@ Code v99.99.999

## Unreleased

## [4.97.2](https://github.com/coder/code-server/releases/tag/v4.96.4) - 2025-02-18

Code v1.97.2

### Added

- Added back macOS amd64 builds.

### Changed

- Update to Code 1.97.2.
- Softened dark mode login page colors.

## [4.96.4](https://github.com/coder/code-server/releases/tag/v4.96.4) - 2025-01-20

Code v1.96.4
4 changes: 0 additions & 4 deletions ci/build/build-release.sh
Original file line number Diff line number Diff line change
@@ -44,10 +44,6 @@ bundle_code_server() {
rsync src/browser/pages/*.css "$RELEASE_PATH/src/browser/pages"
rsync src/browser/robots.txt "$RELEASE_PATH/src/browser"

# Add typings for plugins
mkdir -p "$RELEASE_PATH/typings"
rsync typings/pluginapi.d.ts "$RELEASE_PATH/typings"

# Adds the commit to package.json
jq --slurp '(.[0] | del(.scripts,.jest,.devDependencies)) * .[1]' package.json <(
cat << EOF
2 changes: 1 addition & 1 deletion ci/dev/test-integration.sh
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ main() {
exit 1
fi

CODE_SERVER_PATH="$path" CS_DISABLE_PLUGINS=true ./test/node_modules/.bin/jest "$@" --coverage=false --testRegex "./test/integration" --testPathIgnorePatterns "./test/integration/fixtures"
CODE_SERVER_PATH="$path" ./test/node_modules/.bin/jest "$@" --coverage=false --testRegex "./test/integration" --testPathIgnorePatterns "./test/integration/fixtures"
}

main "$@"
7 changes: 1 addition & 6 deletions ci/dev/test-unit.sh
Original file line number Diff line number Diff line change
@@ -6,15 +6,10 @@ main() {

source ./ci/lib.sh

echo "Building test plugin"
pushd test/unit/node/test-plugin
make -s out/index.js
popd

# We must keep jest in a sub-directory. See ../../test/package.json for more
# information. We must also run it from the root otherwise coverage will not
# include our source files.
CS_DISABLE_PLUGINS=true ./test/node_modules/.bin/jest "$@" --testRegex "./test/unit/.*ts" --testPathIgnorePatterns "./test/unit/node/test-plugin"
./test/node_modules/.bin/jest "$@" --testRegex "./test/unit/.*ts"
}

main "$@"
723 changes: 456 additions & 267 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@
"publish:docker": "./ci/steps/docker-buildx-push.sh",
"fmt": "npm run prettier && ./ci/dev/doctoc.sh",
"lint:scripts": "./ci/dev/lint-scripts.sh",
"lint:ts": "eslint --max-warnings=0 --fix $(git ls-files '*.ts' '*.js' | grep -v 'lib/vscode' | grep -v test-plugin)",
"lint:ts": "eslint --max-warnings=0 --fix $(git ls-files '*.ts' '*.js' | grep -v 'lib/vscode')",
"test": "echo 'Run npm run test:unit or npm run test:e2e' && exit 1",
"watch": "VSCODE_DEV=1 VSCODE_IPC_HOOK_CLI= NODE_OPTIONS='--max_old_space_size=32384 --trace-warnings' ts-node ./ci/dev/watch.ts",
"icons": "./ci/dev/gen_icons.sh"
@@ -44,7 +44,7 @@
"@types/compression": "^1.7.3",
"@types/cookie-parser": "^1.4.4",
"@types/eslint__js": "^8.42.3",
"@types/express": "^4.17.17",
"@types/express": "^5.0.0",
"@types/http-proxy": "1.17.7",
"@types/js-yaml": "^4.0.6",
"@types/node": "20.x",
@@ -73,7 +73,7 @@
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"env-paths": "^2.2.1",
"express": "5.0.0-beta.3",
"express": "^5.0.1",
"http-proxy": "^1.18.1",
"httpolyglot": "^0.1.2",
"i18next": "^23.5.1",
302 changes: 0 additions & 302 deletions src/node/plugin.ts

This file was deleted.

17 changes: 0 additions & 17 deletions src/node/routes/apps.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/node/routes/errors.ts
Original file line number Diff line number Diff line change
@@ -2,8 +2,8 @@ import { logger } from "@coder/logger"
import express from "express"
import { promises as fs } from "fs"
import path from "path"
import { WebsocketRequest } from "../../../typings/pluginapi"
import { HttpCode } from "../../common/http"
import type { WebsocketRequest } from "../wsRouter"
import { rootPath } from "../constants"
import { replaceTemplates } from "../http"
import { escapeHtml, getMediaMime } from "../util"
59 changes: 23 additions & 36 deletions src/node/routes/index.ts
Original file line number Diff line number Diff line change
@@ -4,20 +4,18 @@ import * as express from "express"
import { promises as fs } from "fs"
import * as path from "path"
import * as tls from "tls"
import * as pluginapi from "../../../typings/pluginapi"
import { Disposable } from "../../common/emitter"
import { HttpCode, HttpError } from "../../common/http"
import { plural } from "../../common/util"
import { App } from "../app"
import { AuthType, DefaultedArgs } from "../cli"
import { commit, rootPath } from "../constants"
import { Heart } from "../heart"
import { ensureAuthenticated, redirect } from "../http"
import { PluginAPI } from "../plugin"
import { redirect } from "../http"
import { CoderSettings, SettingsProvider } from "../settings"
import { UpdateProvider } from "../update"
import type { WebsocketRequest } from "../wsRouter"
import { getMediaMime, paths } from "../util"
import * as apps from "./apps"
import * as domainProxy from "./domainProxy"
import { errorHandler, wsErrorHandler } from "./errors"
import * as health from "./health"
@@ -81,65 +79,53 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
app.router.use(common)
app.wsRouter.use(common)

app.router.use(async (req, res, next) => {
app.router.use(/.*/, async (req, res, next) => {
// If we're handling TLS ensure all requests are redirected to HTTPS.
// TODO: This does *NOT* work if you have a base path since to specify the
// protocol we need to specify the whole path.
if (args.cert && !(req.connection as tls.TLSSocket).encrypted) {
return res.redirect(`https://${req.headers.host}${req.originalUrl}`)
}
next()
})

// Return security.txt.
if (req.originalUrl === "/security.txt" || req.originalUrl === "/.well-known/security.txt") {
const resourcePath = path.resolve(rootPath, "src/browser/security.txt")
res.set("Content-Type", getMediaMime(resourcePath))
return res.send(await fs.readFile(resourcePath))
}

// Return robots.txt.
if (req.originalUrl === "/robots.txt") {
const resourcePath = path.resolve(rootPath, "src/browser/robots.txt")
res.set("Content-Type", getMediaMime(resourcePath))
return res.send(await fs.readFile(resourcePath))
}
app.router.get(["/security.txt", "/.well-known/security.txt"], async (_, res) => {
const resourcePath = path.resolve(rootPath, "src/browser/security.txt")
res.set("Content-Type", getMediaMime(resourcePath))
res.send(await fs.readFile(resourcePath))
})

next()
app.router.get("/robots.txt", async (_, res) => {
const resourcePath = path.resolve(rootPath, "src/browser/robots.txt")
res.set("Content-Type", getMediaMime(resourcePath))
res.send(await fs.readFile(resourcePath))
})

app.router.use("/", domainProxy.router)
app.wsRouter.use("/", domainProxy.wsRouter.router)

app.router.all("/proxy/:port/:path(.*)?", async (req, res) => {
app.router.all("/proxy/:port{/*path}", async (req, res) => {
await pathProxy.proxy(req, res)
})
app.wsRouter.get("/proxy/:port/:path(.*)?", async (req) => {
await pathProxy.wsProxy(req as pluginapi.WebsocketRequest)
app.wsRouter.get("/proxy/:port{/*path}", async (req) => {
await pathProxy.wsProxy(req as unknown as WebsocketRequest)
})
// These two routes pass through the path directly.
// So the proxied app must be aware it is running
// under /absproxy/<someport>/
app.router.all("/absproxy/:port/:path(.*)?", async (req, res) => {
app.router.all("/absproxy/:port{/*path}", async (req, res) => {
await pathProxy.proxy(req, res, {
passthroughPath: true,
proxyBasePath: args["abs-proxy-base-path"],
})
})
app.wsRouter.get("/absproxy/:port/:path(.*)?", async (req) => {
await pathProxy.wsProxy(req as pluginapi.WebsocketRequest, {
app.wsRouter.get("/absproxy/:port{/*path}", async (req) => {
await pathProxy.wsProxy(req as unknown as WebsocketRequest, {
passthroughPath: true,
proxyBasePath: args["abs-proxy-base-path"],
})
})

let pluginApi: PluginAPI
if (!process.env.CS_DISABLE_PLUGINS) {
const workingDir = args._ && args._.length > 0 ? path.resolve(args._[args._.length - 1]) : undefined
pluginApi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH, workingDir)
await pluginApi.loadPlugins()
pluginApi.mount(app.router, app.wsRouter)
app.router.use("/api/applications", ensureAuthenticated, apps.router(pluginApi))
}

app.router.use(express.json())
app.router.use(express.urlencoded({ extended: true }))

@@ -172,7 +158,9 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl

app.router.use("/update", update.router)

// Note that the root route is replaced in Coder Enterprise by the plugin API.
// For historic reasons we also load at /vscode because the root was replaced
// by a plugin in v1 of Coder. The plugin system (which was for internal use
// only) has been removed, but leave the additional route for now.
for (const routePrefix of ["/vscode", "/"]) {
app.router.use(routePrefix, vscode.router)
app.wsRouter.use(routePrefix, vscode.wsRouter.router)
@@ -187,7 +175,6 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl

return () => {
heart.dispose()
pluginApi?.dispose()
vscode.dispose()
}
}
4 changes: 2 additions & 2 deletions src/node/routes/pathProxy.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Request, Response } from "express"
import * as path from "path"
import * as pluginapi from "../../../typings/pluginapi"
import { HttpCode, HttpError } from "../../common/http"
import { ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http"
import { proxy as _proxy } from "../proxy"
import type { WebsocketRequest } from "../wsRouter"

const getProxyTarget = (
req: Request,
@@ -49,7 +49,7 @@ export async function proxy(
}

export async function wsProxy(
req: pluginapi.WebsocketRequest,
req: WebsocketRequest,
opts?: {
passthroughPath?: boolean
proxyBasePath?: string
5 changes: 2 additions & 3 deletions src/node/routes/vscode.ts
Original file line number Diff line number Diff line change
@@ -6,14 +6,13 @@ import * as http from "http"
import * as net from "net"
import * as path from "path"
import * as os from "os"
import { WebsocketRequest } from "../../../typings/pluginapi"
import { logError } from "../../common/util"
import { CodeArgs, toCodeArgs } from "../cli"
import { isDevMode, vsRootPath } from "../constants"
import { authenticated, ensureAuthenticated, ensureOrigin, redirect, replaceTemplates, self } from "../http"
import { SocketProxyProvider } from "../socket"
import { isFile } from "../util"
import { Router as WsRouter } from "../wsRouter"
import { type WebsocketRequest, Router as WsRouter } from "../wsRouter"

export const router = express.Router()

@@ -176,7 +175,7 @@ router.get("/manifest.json", async (req, res) => {
const appName = req.args["app-name"] || "code-server"
res.writeHead(200, { "Content-Type": "application/manifest+json" })

return res.end(
res.end(
replaceTemplates(
req,
JSON.stringify(
23 changes: 17 additions & 6 deletions src/node/wsRouter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import * as express from "express"
import * as expressCore from "express-serve-static-core"
import * as http from "http"
import * as stream from "stream"
import Websocket from "ws"
import * as pluginapi from "../../typings/pluginapi"

export interface WebsocketRequest extends express.Request {
ws: stream.Duplex
head: Buffer
}

interface InternalWebsocketRequest extends WebsocketRequest {
_ws_handled: boolean
}

export const handleUpgrade = (app: express.Express, server: http.Server): void => {
server.on("upgrade", (req, socket, head) => {
@@ -22,9 +31,11 @@ export const handleUpgrade = (app: express.Express, server: http.Server): void =
})
}

interface InternalWebsocketRequest extends pluginapi.WebsocketRequest {
_ws_handled: boolean
}
export type WebSocketHandler = (
req: WebsocketRequest,
res: express.Response,
next: express.NextFunction,
) => void | Promise<void>

export class WebsocketRouter {
public readonly router = express.Router()
@@ -36,13 +47,13 @@ export class WebsocketRouter {
* If the origin header exists it must match the host or the connection will
* be prevented.
*/
public ws(route: expressCore.PathParams, ...handlers: pluginapi.WebSocketHandler[]): void {
public ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void {
this.router.get(
route,
...handlers.map((handler) => {
const wrapped: express.Handler = (req, res, next) => {
;(req as InternalWebsocketRequest)._ws_handled = true
return handler(req as pluginapi.WebsocketRequest, res, next)
return handler(req as WebsocketRequest, res, next)
}
return wrapped
}),
3 changes: 1 addition & 2 deletions test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["./**/*.ts"],
"exclude": ["./unit/node/test-plugin"]
"include": ["./**/*.ts"]
}
118 changes: 0 additions & 118 deletions test/unit/node/plugin.test.ts

This file was deleted.

9 changes: 0 additions & 9 deletions test/unit/node/test-plugin/.eslintrc.js

This file was deleted.

1 change: 0 additions & 1 deletion test/unit/node/test-plugin/.gitignore

This file was deleted.

6 changes: 0 additions & 6 deletions test/unit/node/test-plugin/Makefile

This file was deleted.

90 changes: 0 additions & 90 deletions test/unit/node/test-plugin/package-lock.json

This file was deleted.

16 changes: 0 additions & 16 deletions test/unit/node/test-plugin/package.json

This file was deleted.

1 change: 0 additions & 1 deletion test/unit/node/test-plugin/public/icon.svg

This file was deleted.

10 changes: 0 additions & 10 deletions test/unit/node/test-plugin/public/index.html

This file was deleted.

52 changes: 0 additions & 52 deletions test/unit/node/test-plugin/src/index.ts

This file was deleted.

71 changes: 0 additions & 71 deletions test/unit/node/test-plugin/tsconfig.json

This file was deleted.

297 changes: 0 additions & 297 deletions typings/pluginapi.d.ts

This file was deleted.