diff --git a/ci/build/build-code-server.sh b/ci/build/build-code-server.sh index 0aff035af254..16bada4d91d8 100755 --- a/ci/build/build-code-server.sh +++ b/ci/build/build-code-server.sh @@ -25,13 +25,10 @@ main() { fi parcel build \ - --public-url "." \ + --public-url "/static/" \ --out-dir dist \ - $([[ $MINIFY ]] || echo --no-minify) \ - src/browser/register.ts \ - src/browser/serviceWorker.ts \ - src/browser/pages/login.ts \ - src/browser/pages/vscode.ts + "$([[ $MINIFY ]] || echo --no-minify)" \ + src/browser/**/*.ts } main "$@" diff --git a/ci/build/build-release.sh b/ci/build/build-release.sh index 95579eb82240..532d87bce7c9 100755 --- a/ci/build/build-release.sh +++ b/ci/build/build-release.sh @@ -38,10 +38,9 @@ bundle_code_server() { # For source maps and images. mkdir -p "$RELEASE_PATH/src/browser" - rsync src/browser/media/ "$RELEASE_PATH/src/browser/media" - mkdir -p "$RELEASE_PATH/src/browser/pages" - rsync src/browser/pages/*.html "$RELEASE_PATH/src/browser/pages" - rsync src/browser/robots.txt "$RELEASE_PATH/src/browser" + rsync -r src/browser/public "$RELEASE_PATH/src/browser" + mkdir -p "$RELEASE_PATH/src/browser/views" + rsync -r src/browser/views "$RELEASE_PATH/src/browser" # Adds the commit to package.json jq --slurp '.[0] * .[1]' package.json <( diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 9d759649efc8..9f1f88fb539b 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -986,17 +986,17 @@ index 0000000000000000000000000000000000000000..5dd5406befcb593ad6366d9e98f46485 +export const IExtHostNodeProxy = createDecorator('IExtHostNodeProxy'); diff --git a/src/vs/server/browser/mainThreadNodeProxy.ts b/src/vs/server/browser/mainThreadNodeProxy.ts new file mode 100644 -index 0000000000000000000000000000000000000000..21a139288e5b8f56016491879d69d01da929decb +index 0000000000000000000000000000000000000000..e11988d1b3263c4a2ede064c653653b038c8238c --- /dev/null +++ b/src/vs/server/browser/mainThreadNodeProxy.ts -@@ -0,0 +1,55 @@ +@@ -0,0 +1,63 @@ +import { VSBuffer } from 'vs/base/common/buffer'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { FileAccess } from 'vs/base/common/network'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { INodeProxyService } from 'vs/server/common/nodeProxy'; +import { ExtHostContext, IExtHostContext, MainContext, MainThreadNodeProxyShape } from 'vs/workbench/api/common/extHost.protocol'; -+import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; ++import {extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; + +@extHostNamedCustomer(MainContext.MainThreadNodeProxy) +export class MainThreadNodeProxy implements MainThreadNodeProxyShape { @@ -1025,14 +1025,22 @@ index 0000000000000000000000000000000000000000..21a139288e5b8f56016491879d69d01d + } + + async $fetchExtension(extensionUri: UriComponents): Promise { ++ // Use FileAccess to get the static base path. ++ const basePath = FileAccess.asBrowserUri("", require).path; ++ + const fetchUri = URI.from({ + scheme: window.location.protocol.replace(':', ''), + authority: window.location.host, -+ // Use FileAccess to get the static base path. -+ path: FileAccess.asBrowserUri("", require).path, -+ query: `tar=${encodeURIComponent(extensionUri.path)}`, ++ path: `${basePath}../../../extension/tar`, ++ query: `filePath=${encodeURIComponent(extensionUri.path)}`, + }); -+ const response = await fetch(fetchUri.toString(true)); ++ ++ const response = await fetch(fetchUri.toString(true), { ++ headers: { ++ Accept: "application/x-tar, application/json;q=0.9" ++ } ++ }); ++ + if (response.status !== 200) { + throw new Error(`Failed to download extension "${module}"`); + } diff --git a/ci/dev/watch.ts b/ci/dev/watch.ts index 646da328b3f8..e0bf3ab11564 100644 --- a/ci/dev/watch.ts +++ b/ci/dev/watch.ts @@ -173,21 +173,13 @@ class Watcher { } private createBundler(out = "dist"): Bundler { - return new Bundler( - [ - path.join(this.rootPath, "src/browser/register.ts"), - path.join(this.rootPath, "src/browser/serviceWorker.ts"), - path.join(this.rootPath, "src/browser/pages/login.ts"), - path.join(this.rootPath, "src/browser/pages/vscode.ts"), - ], - { - outDir: path.join(this.rootPath, out), - cacheDir: path.join(this.rootPath, ".cache"), - minify: !!process.env.MINIFY, - logLevel: 1, - publicUrl: ".", - }, - ) + return new Bundler([path.join(this.rootPath, "src/browser/**/*.ts")], { + outDir: path.join(this.rootPath, out), + cacheDir: path.join(this.rootPath, ".cache"), + minify: !!process.env.MINIFY, + logLevel: 1, + publicUrl: "/static/", + }) } } diff --git a/package.json b/package.json index c41b8b41c045..91cefeb72c6b 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,9 @@ "devDependencies": { "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", + "@types/entities": "^1.1.1", "@types/express": "^4.17.8", + "@types/express-handlebars": "^3.1.0", "@types/fs-extra": "^8.0.1", "@types/http-proxy": "^1.17.4", "@types/js-yaml": "^3.12.3", @@ -73,8 +75,10 @@ "@coder/logger": "1.1.16", "body-parser": "^1.19.0", "cookie-parser": "^1.4.5", + "entities": "^2.1.0", "env-paths": "^2.2.0", "express": "^5.0.0-alpha.8", + "express-handlebars": "^5.2.0", "fs-extra": "^9.0.1", "http-proxy": "^1.18.0", "httpolyglot": "^0.1.2", diff --git a/src/browser/main.ts b/src/browser/main.ts new file mode 100644 index 000000000000..e42abbf554a5 --- /dev/null +++ b/src/browser/main.ts @@ -0,0 +1,16 @@ +import "./views/error/index.css" +import "./views/global.css" +import "./views/login/index.css" + +import { getOptions, normalize } from "../common/util" + +const options = getOptions() + +if ("serviceWorker" in navigator) { + const path = normalize(`${options.base}/serviceWorker.js`) + navigator.serviceWorker + .register(path, { + scope: (options.base ?? "") + "/", + }) + .then(() => console.debug("[Code Server Service Worker] registered")) +} diff --git a/src/browser/media/manifest.json b/src/browser/media/manifest.json deleted file mode 100644 index b33be207b052..000000000000 --- a/src/browser/media/manifest.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "code-server", - "short_name": "code-server", - "start_url": "{{BASE}}", - "display": "fullscreen", - "background-color": "#fff", - "description": "Run editors on a remote server.", - "icons": [ - { - "src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-96.png", - "type": "image/png", - "sizes": "96x96" - }, - { - "src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-128.png", - "type": "image/png", - "sizes": "128x128" - }, - { - "src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-256.png", - "type": "image/png", - "sizes": "256x256" - }, - { - "src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png", - "type": "image/png", - "sizes": "384x384" - }, - { - "src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png", - "type": "image/png", - "sizes": "512x512" - } - ] -} diff --git a/src/browser/pages/login.ts b/src/browser/pages/login.ts deleted file mode 100644 index c7fc92d4a0e7..000000000000 --- a/src/browser/pages/login.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { getOptions } from "../../common/util" - -const options = getOptions() -const el = document.getElementById("base") as HTMLInputElement -if (el) { - el.value = options.base -} diff --git a/src/browser/pages/vscode.html b/src/browser/pages/vscode.html deleted file mode 100644 index d9305fe9db0f..000000000000 --- a/src/browser/pages/vscode.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/browser/pages/vscode.ts b/src/browser/pages/vscode.ts deleted file mode 100644 index 2cb7973f1061..000000000000 --- a/src/browser/pages/vscode.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { getOptions } from "../../common/util" - -const options = getOptions() - -// TODO: Add proper types. -/* eslint-disable @typescript-eslint/no-explicit-any */ - -let nlsConfig: any -try { - nlsConfig = JSON.parse(document.getElementById("vscode-remote-nls-configuration")!.getAttribute("data-settings")!) - if (nlsConfig._resolvedLanguagePackCoreLocation) { - const bundles = Object.create(null) - nlsConfig.loadBundle = (bundle: any, _language: any, cb: any): void => { - const result = bundles[bundle] - if (result) { - return cb(undefined, result) - } - // FIXME: Only works if path separators are /. - const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json" - fetch(`${options.base}/vscode/resource/?path=${encodeURIComponent(path)}`) - .then((response) => response.json()) - .then((json) => { - bundles[bundle] = json - cb(undefined, json) - }) - .catch(cb) - } - } -} catch (error) { - /* Probably fine. */ -} - -;(self.require as any) = { - // Without the full URL VS Code will try to load file://. - baseUrl: `${window.location.origin}${options.csStaticBase}/lib/vscode/out`, - recordStats: true, - paths: { - "vscode-textmate": `../node_modules/vscode-textmate/release/main`, - "vscode-oniguruma": `../node_modules/vscode-oniguruma/release/main`, - xterm: `../node_modules/xterm/lib/xterm.js`, - "xterm-addon-search": `../node_modules/xterm-addon-search/lib/xterm-addon-search.js`, - "xterm-addon-unicode11": `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`, - "xterm-addon-webgl": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`, - "semver-umd": `../node_modules/semver-umd/lib/semver-umd.js`, - "tas-client-umd": `../node_modules/tas-client-umd/lib/tas-client-umd.js`, - "iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`, - jschardet: `../node_modules/jschardet/dist/jschardet.min.js`, - }, - "vs/nls": nlsConfig, -} - -try { - document.body.style.background = JSON.parse(localStorage.getItem("colorThemeData")!).colorMap["editor.background"] -} catch (error) { - // Oh well. -} diff --git a/src/browser/media/favicon.ico b/src/browser/public/media/favicon.ico similarity index 100% rename from src/browser/media/favicon.ico rename to src/browser/public/media/favicon.ico diff --git a/src/browser/media/pwa-icon-128.png b/src/browser/public/media/pwa-icon-128.png similarity index 100% rename from src/browser/media/pwa-icon-128.png rename to src/browser/public/media/pwa-icon-128.png diff --git a/src/browser/media/pwa-icon-192.png b/src/browser/public/media/pwa-icon-192.png similarity index 100% rename from src/browser/media/pwa-icon-192.png rename to src/browser/public/media/pwa-icon-192.png diff --git a/src/browser/media/pwa-icon-256.png b/src/browser/public/media/pwa-icon-256.png similarity index 100% rename from src/browser/media/pwa-icon-256.png rename to src/browser/public/media/pwa-icon-256.png diff --git a/src/browser/media/pwa-icon-384.png b/src/browser/public/media/pwa-icon-384.png similarity index 100% rename from src/browser/media/pwa-icon-384.png rename to src/browser/public/media/pwa-icon-384.png diff --git a/src/browser/media/pwa-icon-512.png b/src/browser/public/media/pwa-icon-512.png similarity index 100% rename from src/browser/media/pwa-icon-512.png rename to src/browser/public/media/pwa-icon-512.png diff --git a/src/browser/media/pwa-icon-96.png b/src/browser/public/media/pwa-icon-96.png similarity index 100% rename from src/browser/media/pwa-icon-96.png rename to src/browser/public/media/pwa-icon-96.png diff --git a/src/browser/robots.txt b/src/browser/public/robots.txt similarity index 100% rename from src/browser/robots.txt rename to src/browser/public/robots.txt diff --git a/src/browser/register.ts b/src/browser/register.ts deleted file mode 100644 index 4f8345808915..000000000000 --- a/src/browser/register.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getOptions, normalize } from "../common/util" - -const options = getOptions() - -import "./pages/error.css" -import "./pages/global.css" -import "./pages/login.css" - -if ("serviceWorker" in navigator) { - const path = normalize(`${options.csStaticBase}/dist/serviceWorker.js`) - navigator.serviceWorker - .register(path, { - scope: (options.base ?? "") + "/", - }) - .then(() => { - console.log("[Service Worker] registered") - }) -} diff --git a/src/browser/serviceWorker.ts b/src/browser/serviceWorker.ts index 1bee59bfb3c6..c1da90d1b5ef 100644 --- a/src/browser/serviceWorker.ts +++ b/src/browser/serviceWorker.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ self.addEventListener("install", () => { - console.log("[Service Worker] install") + console.debug("[Code Server Service Worker] install") }) self.addEventListener("activate", (event: any) => { diff --git a/src/browser/pages/error.css b/src/browser/views/error/index.css similarity index 100% rename from src/browser/pages/error.css rename to src/browser/views/error/index.css diff --git a/src/browser/pages/error.html b/src/browser/views/error/index.handlebars similarity index 50% rename from src/browser/pages/error.html rename to src/browser/views/error/index.handlebars index 302b202a422e..00760b9c11a7 100644 --- a/src/browser/pages/error.html +++ b/src/browser/views/error/index.handlebars @@ -10,12 +10,12 @@ http-equiv="Content-Security-Policy" content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;" /> - {{ERROR_TITLE}} - code-server - - - - - + {{ERROR_TITLE}} — Code Server + + + + +
@@ -23,10 +23,10 @@

{{ERROR_HEADER}}

{{ERROR_BODY}}
- + diff --git a/src/browser/pages/global.css b/src/browser/views/global.css similarity index 100% rename from src/browser/pages/global.css rename to src/browser/views/global.css diff --git a/src/browser/pages/login.css b/src/browser/views/login/index.css similarity index 100% rename from src/browser/pages/login.css rename to src/browser/views/login/index.css diff --git a/src/browser/pages/login.html b/src/browser/views/login/index.handlebars similarity index 61% rename from src/browser/pages/login.html rename to src/browser/views/login/index.handlebars index fc772f392b38..8b2cba8cb286 100644 --- a/src/browser/pages/login.html +++ b/src/browser/views/login/index.handlebars @@ -10,19 +10,21 @@ http-equiv="Content-Security-Policy" content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;" /> - code-server login - - - - - + Login — Code Server + + + + + +

Welcome to code-server

-
Please log in below. {{PASSWORD_MSG}}
+
Please log in below.
+
{{PASSWORD_MSG}}
- {{ERROR}} + + {{#if ERROR}} +
{{ERROR}}
+ {{/if}}
- - - + + diff --git a/src/browser/pages/update.css b/src/browser/views/update.css similarity index 100% rename from src/browser/pages/update.css rename to src/browser/views/update.css diff --git a/src/browser/views/vscode/index.handlebars b/src/browser/views/vscode/index.handlebars new file mode 100644 index 000000000000..777f21239734 --- /dev/null +++ b/src/browser/views/vscode/index.handlebars @@ -0,0 +1,66 @@ + + + + + + + + Code Server + + + + + + + + + + + + + + + + + {{#if (prod)}} + + {{/if}} + + + + + + {{#if (prod)}} + + {{/if}} + + + + + + + + + + + + + + {{#if (prod)}} + + + {{/if}} + + + diff --git a/src/browser/views/vscode/index.ts b/src/browser/views/vscode/index.ts new file mode 100644 index 000000000000..91af44e4980c --- /dev/null +++ b/src/browser/views/vscode/index.ts @@ -0,0 +1,100 @@ +import { InternalNLSConfiguration } from "../../../../lib/vscode/src/vs/base/node/languagePacks" +import { getOptions } from "../../../common/util" + +export type Bundles = { + // TODO: Clarify this type. + [name: string]: unknown[] +} + +export type BundleCallback = (err: unknown | undefined, messages?: unknown[]) => void | PromiseLike + +export interface CodeServerNlsConfiguration extends InternalNLSConfiguration { + loadBundle: (name: string, language: string, cb: BundleCallback) => void +} + +export interface CodeServerAmdLoaderConfigurationOptions extends AMDLoader.IConfigurationOptions { + "vs/nls": CodeServerNlsConfiguration +} + +const options = getOptions() + +const parseNLSConfig = (): CodeServerNlsConfiguration => { + return JSON.parse(document.getElementById("vscode-remote-nls-configuration")!.getAttribute("data-settings")!) +} + +const syncTheme = () => { + // First attempt to parse localStorage. + try { + document.body.style.background = JSON.parse(localStorage.getItem("colorThemeData")!).colorMap["editor.background"] + } catch (error) { + // Oh well. + } + + // Then, observe the meta theme element for changes. + const themeElement = document.getElementById("monaco-workbench-meta-theme-color") as HTMLMetaElement + + const synchronizeTheme = () => { + document.body.style.background = themeElement.content + } + + const themeElementObserver = new MutationObserver(synchronizeTheme) + themeElementObserver.observe(themeElement, { attributes: true }) + + synchronizeTheme() +} + +const initializeCodeServerEditor = () => { + syncTheme() + + const nlsConfig = parseNLSConfig() + + if (nlsConfig._resolvedLanguagePackCoreLocation) { + const bundles: Bundles = Object.create(null) + + nlsConfig.loadBundle = async (bundle, _, cb) => { + const result = bundles[bundle] + + if (result) { + return cb(undefined, result) + } + + // FIXME: Only works if path separators are /. + const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json" + + let body: unknown[] + try { + const response = await fetch(`${options.base}/vscode/resource/?path=${encodeURIComponent(path)}`) + body = await response.json() + } catch (error) { + cb(error) + return + } + + bundles[bundle] = body + cb(undefined, body) + } + } + + const amdLoaderConfig: CodeServerAmdLoaderConfigurationOptions = { + // Without the full URL VS Code will try to load file://. + baseUrl: `${window.location.origin}${options.base}/vscode/lib/vscode/out`, + recordStats: true, + paths: { + "vscode-textmate": `../node_modules/vscode-textmate/release/main`, + "vscode-oniguruma": `../node_modules/vscode-oniguruma/release/main`, + xterm: `../node_modules/xterm/lib/xterm.js`, + "xterm-addon-search": `../node_modules/xterm-addon-search/lib/xterm-addon-search.js`, + "xterm-addon-unicode11": `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`, + "xterm-addon-webgl": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`, + "semver-umd": `../node_modules/semver-umd/lib/semver-umd.js`, + "tas-client-umd": `../node_modules/tas-client-umd/lib/tas-client-umd.js`, + "iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`, + jschardet: `../node_modules/jschardet/dist/jschardet.min.js`, + }, + "vs/nls": nlsConfig, + } + + ;(self.require as any) = amdLoaderConfig +} + +initializeCodeServerEditor() diff --git a/src/common/util.ts b/src/common/util.ts index 7baa355adda0..4790bf131ba4 100644 --- a/src/common/util.ts +++ b/src/common/util.ts @@ -2,7 +2,6 @@ import { logger, field } from "@coder/logger" export interface Options { base: string - csStaticBase: string logLevel: number } @@ -31,6 +30,7 @@ export const generateUuid = (length = 24): string => { /** * Remove extra slashes in a URL. + * @TODO replace with node `path` module. */ export const normalize = (url: string, keepTrailing = false): string => { return url.replace(/\/\/+/g, "/").replace(/\/+$/, keepTrailing ? "/" : "") @@ -81,7 +81,6 @@ export const getOptions = (): T => { logger.level = options.logLevel options.base = resolveBase(options.base) - options.csStaticBase = resolveBase(options.csStaticBase) logger.debug("got options", field("options", options)) diff --git a/src/node/app.ts b/src/node/app.ts index 448ec9660a56..78b292ecc4aa 100644 --- a/src/node/app.ts +++ b/src/node/app.ts @@ -1,16 +1,43 @@ import { logger } from "@coder/logger" +import { encodeXML } from "entities" import express, { Express } from "express" +import exphbs from "express-handlebars" import { promises as fs } from "fs" import http from "http" import * as httpolyglot from "httpolyglot" +import { resolve } from "path" +import { normalize } from "../common/util" import { DefaultedArgs } from "./cli" +import { commit, rootPath } from "./constants" import { handleUpgrade } from "./wsRouter" /** * Create an Express app and an HTTP/S server to serve it. */ export const createApp = async (args: DefaultedArgs): Promise<[Express, Express, http.Server]> => { + const prod = process.env.NODE_ENV === "production" const app = express() + app.set("json spaces", prod ? 0 : 2) + + app.engine( + "handlebars", + exphbs({ + defaultLayout: "", + helpers: { + prod: () => commit !== "development", + assetPath: (base: string, path: string) => normalize(base + path), + /** + * Converts to JSON string and encodes entities for use in HTML. + * @TODO we can likely move JSON attributes to