diff --git a/src/browser/pages/vscode.ts b/src/browser/pages/vscode.ts index 57ad6167e..128df2c45 100644 --- a/src/browser/pages/vscode.ts +++ b/src/browser/pages/vscode.ts @@ -1,10 +1,8 @@ -import { getOptions } from "../../common/util" +import { getOptions, Options } from "../../common/util" import "../register" -const options = getOptions() - -// TODO: Add proper types. -/* eslint-disable @typescript-eslint/no-explicit-any */ +// TODO@jsjoeio: Add proper types. +type FixMeLater = any // NOTE@jsjoeio // This lives here ../../../lib/vscode/src/vs/base/common/platform.ts#L106 @@ -19,7 +17,20 @@ type NlsConfiguration = { _resolvedLanguagePackCoreLocation?: string _corruptedFile?: string _languagePackSupport?: boolean - loadBundle?: any + loadBundle?: FixMeLater +} + +/** + * Helper function to create the path to the bundle + * for getNlsConfiguration. + */ +export function createBundlePath(_resolvedLanguagePackCoreLocation: string, bundle: string) { + // NOTE@jsjoeio - this comment was here before me + // Refers to operating systems that use a different path separator. + // Probably just Windows but we're not sure if "/" breaks on Windows + // so we'll leave it alone for now. + // FIXME: Only works if path separators are /. + return _resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json" } /** @@ -30,14 +41,10 @@ type NlsConfiguration = { * * Make sure to wrap this in a try/catch block when you call it. **/ -export function getNlsConfiguration(document: Document) { +export function getNlsConfiguration(_document: Document, base: string) { const errorMsgPrefix = "[vscode]" - const nlsConfigElement = document?.getElementById(nlsConfigElementId) - const nlsConfig = nlsConfigElement?.getAttribute("data-settings") - - if (!document) { - throw new Error(`${errorMsgPrefix} Could not parse NLS configuration. document is undefined.`) - } + const nlsConfigElement = _document?.getElementById(nlsConfigElementId) + const dataSettings = nlsConfigElement?.getAttribute("data-settings") if (!nlsConfigElement) { throw new Error( @@ -45,27 +52,34 @@ export function getNlsConfiguration(document: Document) { ) } - if (!nlsConfig) { + if (!dataSettings) { throw new Error( `${errorMsgPrefix} Could not parse NLS configuration. Found nlsConfigElement but missing data-settings attribute.`, ) } - return JSON.parse(nlsConfig) as NlsConfiguration -} + const nlsConfig = JSON.parse(dataSettings) as NlsConfiguration -try { - const nlsConfig = getNlsConfiguration(document) if (nlsConfig._resolvedLanguagePackCoreLocation) { - const bundles = Object.create(null) - nlsConfig.loadBundle = (bundle: any, _language: any, cb: any): void => { + // NOTE@jsjoeio + // Not sure why we use Object.create(null) instead of {} + // They are not the same + // See: https://stackoverflow.com/a/15518712/3015595 + // We copied this from ../../../lib/vscode/src/bootstrap.js#L143 + const bundles: { + [key: string]: string + } = Object.create(null) + + type LoadBundleCallback = (_: undefined, result?: string) => void + + nlsConfig.loadBundle = (bundle: string, _language: string, cb: LoadBundleCallback): 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)}`) + const path = createBundlePath(nlsConfig._resolvedLanguagePackCoreLocation || "", bundle) + fetch(`${base}/vscode/resource/?path=${encodeURIComponent(path)}`) .then((response) => response.json()) .then((json) => { bundles[bundle] = json @@ -74,17 +88,61 @@ try { .catch(cb) } } - ;(self.require as any) = { + + return nlsConfig +} + +type GetLoaderParams = { + nlsConfig: NlsConfiguration + options: Options + _window: Window +} + +/** + * Link to types in the loader source repo + * https://github.com/microsoft/vscode-loader/blob/main/src/loader.d.ts#L280 + */ +type Loader = { + baseUrl: string + recordStats: boolean + // TODO@jsjoeio: There don't appear to be any types for trustedTypes yet. + trustedTypesPolicy: FixMeLater + paths: { + [key: string]: string + } + "vs/nls": NlsConfiguration +} + +/** + * A helper function which creates a script url if the value + * is valid. + * + * Extracted into a function to make it easier to test + */ +export function _createScriptURL(value: string, origin: string): string { + if (value.startsWith(origin)) { + return value + } + throw new Error(`Invalid script url: ${value}`) +} + +/** + * A helper function to get the require loader + * + * This used by VSCode/code-server + * to load files. + * + * We extracted the logic into a function so that + * it's easier to test. + **/ +export function getConfigurationForLoader({ nlsConfig, options, _window }: GetLoaderParams) { + const loader: Loader = { // Without the full URL VS Code will try to load file://. baseUrl: `${window.location.origin}${options.csStaticBase}/lib/vscode/out`, recordStats: true, - // TODO: There don't appear to be any types for trustedTypes yet. - trustedTypesPolicy: (window as any).trustedTypes?.createPolicy("amdLoader", { + trustedTypesPolicy: (_window as FixMeLater).trustedTypes?.createPolicy("amdLoader", { createScriptURL(value: string): string { - if (value.startsWith(window.location.origin)) { - return value - } - throw new Error(`Invalid script url: ${value}`) + return _createScriptURL(value, window.location.origin) }, }), paths: { @@ -100,25 +158,16 @@ try { }, "vs/nls": nlsConfig, } -} catch (error) { - console.error(error) - /* Probably fine. */ + + return loader } -export function setBodyBackgroundToThemeBackgroundColor(document: Document, localStorage: Storage) { +/** + * Sets the body background color to match the theme. + */ +export function setBodyBackgroundToThemeBackgroundColor(_document: Document, _localStorage: Storage) { const errorMsgPrefix = "[vscode]" - - if (!document) { - throw new Error(`${errorMsgPrefix} Could not set body background to theme background color. Document is undefined.`) - } - - if (!localStorage) { - throw new Error( - `${errorMsgPrefix} Could not set body background to theme background color. localStorage is undefined.`, - ) - } - - const colorThemeData = localStorage.getItem("colorThemeData") + const colorThemeData = _localStorage.getItem("colorThemeData") if (!colorThemeData) { throw new Error( @@ -155,14 +204,48 @@ export function setBodyBackgroundToThemeBackgroundColor(document: Document, loca ) } - document.body.style.background = editorBgColor + _document.body.style.background = editorBgColor return null } +/** + * A helper function to encapsulate all the + * logic used in this file. + * + * We purposely include all of this in a single function + * so that it's easier to test. + */ +export function main(_document: Document | undefined, _window: Window | undefined, _localStorage: Storage | undefined) { + if (!_document) { + throw new Error(`document is undefined.`) + } + + if (!_window) { + throw new Error(`window is undefined.`) + } + + if (!_localStorage) { + throw new Error(`localStorage is undefined.`) + } + + const options = getOptions() + const nlsConfig = getNlsConfiguration(_document, options.base) + + const loader = getConfigurationForLoader({ + nlsConfig, + options, + _window, + }) + + ;(self.require as unknown as Loader) = loader + + setBodyBackgroundToThemeBackgroundColor(_document, _localStorage) +} + try { - setBodyBackgroundToThemeBackgroundColor(document, localStorage) + main(document, window, localStorage) } catch (error) { - console.error("Something went wrong setting the body background to the theme background color.") + console.error("[vscode] failed to initialize VS Code") console.error(error) } diff --git a/test/unit/browser/pages/vscode.test.ts b/test/unit/browser/pages/vscode.test.ts index 992d3087c..99c109bbf 100644 --- a/test/unit/browser/pages/vscode.test.ts +++ b/test/unit/browser/pages/vscode.test.ts @@ -5,48 +5,47 @@ import { JSDOM } from "jsdom" import { getNlsConfiguration, nlsConfigElementId, + getConfigurationForLoader, setBodyBackgroundToThemeBackgroundColor, + _createScriptURL, + main, + createBundlePath, } from "../../../../src/browser/pages/vscode" describe("vscode", () => { describe("getNlsConfiguration", () => { + let _document: Document + beforeEach(() => { - const { window } = new JSDOM() - global.document = window.document + // We use underscores to not confuse with global values + const { window: _window } = new JSDOM() + _document = _window.document }) - it("should throw an error if Document is undefined", () => { - const errorMsgPrefix = "[vscode]" - const errorMessage = `${errorMsgPrefix} Could not parse NLS configuration. document is undefined.` - - expect(() => { - getNlsConfiguration(undefined as any as Document) - }).toThrowError(errorMessage) - }) it("should throw an error if no nlsConfigElement", () => { const errorMsgPrefix = "[vscode]" const errorMessage = `${errorMsgPrefix} Could not parse NLS configuration. Could not find nlsConfigElement with id: ${nlsConfigElementId}` expect(() => { - getNlsConfiguration(document) + getNlsConfiguration(_document, "") }).toThrowError(errorMessage) }) it("should throw an error if no nlsConfig", () => { - const mockElement = document.createElement("div") + const mockElement = _document.createElement("div") mockElement.setAttribute("id", nlsConfigElementId) - document.body.appendChild(mockElement) + _document.body.appendChild(mockElement) const errorMsgPrefix = "[vscode]" const errorMessage = `${errorMsgPrefix} Could not parse NLS configuration. Found nlsConfigElement but missing data-settings attribute.` expect(() => { - getNlsConfiguration(document) + getNlsConfiguration(_document, "") }).toThrowError(errorMessage) - document.body.removeChild(mockElement) + _document.body.removeChild(mockElement) }) it("should return the correct configuration", () => { - const mockElement = document.createElement("div") + const mockElement = _document.createElement("div") const dataSettings = { first: "Jane", last: "Doe", @@ -54,22 +53,52 @@ describe("vscode", () => { mockElement.setAttribute("id", nlsConfigElementId) mockElement.setAttribute("data-settings", JSON.stringify(dataSettings)) - document.body.appendChild(mockElement) - const actual = getNlsConfiguration(global.document) + _document.body.appendChild(mockElement) + const actual = getNlsConfiguration(_document, "") expect(actual).toStrictEqual(dataSettings) - document.body.removeChild(mockElement) + _document.body.removeChild(mockElement) + }) + it("should return have loadBundle property if _resolvedLangaugePackCoreLocation", () => { + const mockElement = _document.createElement("div") + const dataSettings = { + locale: "en", + availableLanguages: ["en", "de"], + _resolvedLanguagePackCoreLocation: "./", + } + + mockElement.setAttribute("id", nlsConfigElementId) + mockElement.setAttribute("data-settings", JSON.stringify(dataSettings)) + _document.body.appendChild(mockElement) + const nlsConfig = getNlsConfiguration(_document, "") + + expect(nlsConfig._resolvedLanguagePackCoreLocation).not.toBe(undefined) + expect(nlsConfig.loadBundle).not.toBe(undefined) + + _document.body.removeChild(mockElement) + }) + }) + describe("createBundlePath", () => { + it("should return the correct path", () => { + const _resolvedLangaugePackCoreLocation = "./languages" + const bundle = "/bundle.js" + const expected = "./languages/!bundle.js.nls.json" + const actual = createBundlePath(_resolvedLangaugePackCoreLocation, bundle) + expect(actual).toBe(expected) }) }) describe("setBodyBackgroundToThemeBackgroundColor", () => { + let _document: Document + let _localStorage: Storage + beforeEach(() => { // We need to set the url in the JSDOM constructor // to prevent this error "SecurityError: localStorage is not available for opaque origins" // See: https://github.com/jsdom/jsdom/issues/2304#issuecomment-622314949 - const { window } = new JSDOM("", { url: "http://localhost" }) - global.document = window.document - global.localStorage = window.localStorage + const { window: _window } = new JSDOM("", { url: "http://localhost" }) + _document = _window.document + _localStorage = _window.localStorage }) it("should return null", () => { const test = { @@ -77,49 +106,31 @@ describe("vscode", () => { [`editor.background`]: "#ff3270", }, } - localStorage.setItem("colorThemeData", JSON.stringify(test)) + _localStorage.setItem("colorThemeData", JSON.stringify(test)) - expect(setBodyBackgroundToThemeBackgroundColor(document, localStorage)).toBeNull() + expect(setBodyBackgroundToThemeBackgroundColor(_document, _localStorage)).toBeNull() - localStorage.removeItem("colorThemeData") - }) - it("should throw an error if Document is undefined", () => { - const errorMsgPrefix = "[vscode]" - const errorMessage = `${errorMsgPrefix} Could not set body background to theme background color. Document is undefined.` - - expect(() => { - // @ts-expect-error We need to test when document is undefined - setBodyBackgroundToThemeBackgroundColor(undefined, localStorage) - }).toThrowError(errorMessage) - }) - it("should throw an error if localStorage is undefined", () => { - const errorMsgPrefix = "[vscode]" - const errorMessage = `${errorMsgPrefix} Could not set body background to theme background color. localStorage is undefined.` - - expect(() => { - // @ts-expect-error We need to test when localStorage is undefined - setBodyBackgroundToThemeBackgroundColor(document, undefined) - }).toThrowError(errorMessage) + _localStorage.removeItem("colorThemeData") }) it("should throw an error if it can't find colorThemeData in localStorage", () => { const errorMsgPrefix = "[vscode]" const errorMessage = `${errorMsgPrefix} Could not set body background to theme background color. Could not find colorThemeData in localStorage.` expect(() => { - setBodyBackgroundToThemeBackgroundColor(document, localStorage) + setBodyBackgroundToThemeBackgroundColor(_document, _localStorage) }).toThrowError(errorMessage) }) it("should throw an error if there is an error parsing colorThemeData from localStorage", () => { const errorMsgPrefix = "[vscode]" const errorMessage = `${errorMsgPrefix} Could not set body background to theme background color. Could not parse colorThemeData from localStorage.` - localStorage.setItem( + _localStorage.setItem( "colorThemeData", '{"id":"vs-dark max-SS-Cyberpunk-themes-cyberpunk-umbra-color-theme-json","label":"Activate UMBRA protocol","settingsId":"Activate "errorForeground":"#ff3270","foreground":"#ffffff","sideBarTitle.foreground":"#bbbbbb"},"watch\\":::false}', ) expect(() => { - setBodyBackgroundToThemeBackgroundColor(document, localStorage) + setBodyBackgroundToThemeBackgroundColor(_document, _localStorage) }).toThrowError(errorMessage) localStorage.removeItem("colorThemeData") @@ -131,13 +142,13 @@ describe("vscode", () => { const test = { id: "hey-joe", } - localStorage.setItem("colorThemeData", JSON.stringify(test)) + _localStorage.setItem("colorThemeData", JSON.stringify(test)) expect(() => { - setBodyBackgroundToThemeBackgroundColor(document, localStorage) + setBodyBackgroundToThemeBackgroundColor(_document, _localStorage) }).toThrowError(errorMessage) - localStorage.removeItem("colorThemeData") + _localStorage.removeItem("colorThemeData") }) it("should throw an error if there is no editor.background color", () => { const errorMsgPrefix = "[vscode]" @@ -149,13 +160,13 @@ describe("vscode", () => { editor: "#fff", }, } - localStorage.setItem("colorThemeData", JSON.stringify(test)) + _localStorage.setItem("colorThemeData", JSON.stringify(test)) expect(() => { - setBodyBackgroundToThemeBackgroundColor(document, localStorage) + setBodyBackgroundToThemeBackgroundColor(_document, _localStorage) }).toThrowError(errorMessage) - localStorage.removeItem("colorThemeData") + _localStorage.removeItem("colorThemeData") }) it("should set the body background to the editor background color", () => { const test = { @@ -163,16 +174,188 @@ describe("vscode", () => { [`editor.background`]: "#ff3270", }, } - localStorage.setItem("colorThemeData", JSON.stringify(test)) + _localStorage.setItem("colorThemeData", JSON.stringify(test)) - setBodyBackgroundToThemeBackgroundColor(document, localStorage) + setBodyBackgroundToThemeBackgroundColor(_document, _localStorage) // When the body.style.backgroundColor is set using hex // it is converted to rgb // which is why we use that in the assertion - expect(document.body.style.backgroundColor).toBe("rgb(255, 50, 112)") + expect(_document.body.style.backgroundColor).toBe("rgb(255, 50, 112)") - localStorage.removeItem("colorThemeData") + _localStorage.removeItem("colorThemeData") + }) + }) + describe("getConfigurationForLoader", () => { + let _window: Window + + beforeEach(() => { + const { window: __window } = new JSDOM() + // @ts-expect-error the Window from JSDOM is not exactly the same as Window + // so we expect an error here + _window = __window + }) + it("should return a loader object (with undefined trustedTypesPolicy)", () => { + const options = { + base: ".", + csStaticBase: "/", + logLevel: 1, + } + const nlsConfig = { + first: "Jane", + last: "Doe", + locale: "en", + availableLanguages: {}, + } + const loader = getConfigurationForLoader({ + options, + _window, + nlsConfig: nlsConfig, + }) + + expect(loader).toStrictEqual({ + baseUrl: "http://localhost//lib/vscode/out", + paths: { + "iconv-lite-umd": "../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js", + jschardet: "../node_modules/jschardet/dist/jschardet.min.js", + "tas-client-umd": "../node_modules/tas-client-umd/lib/tas-client-umd.js", + "vscode-oniguruma": "../node_modules/vscode-oniguruma/release/main", + "vscode-textmate": "../node_modules/vscode-textmate/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", + }, + recordStats: true, + + // TODO@jsjoeio address trustedTypesPolicy part + // might need to look up types + // and find a way to test the function + // maybe extract function into function + // and test manually + trustedTypesPolicy: undefined, + "vs/nls": { + availableLanguages: {}, + first: "Jane", + last: "Doe", + locale: "en", + }, + }) + }) + it("should return a loader object with trustedTypesPolicy", () => { + interface PolicyOptions { + createScriptUrl: (url: string) => string + } + + function mockCreatePolicy(policyName: string, options: PolicyOptions) { + return { + name: policyName, + ...options, + } + } + + const mockFn = jest.fn(mockCreatePolicy) + + // @ts-expect-error we are adding a custom property to window + _window.trustedTypes = { + createPolicy: mockFn, + } + + const options = { + base: "/", + csStaticBase: "/", + logLevel: 1, + } + const nlsConfig = { + first: "Jane", + last: "Doe", + locale: "en", + availableLanguages: {}, + } + const loader = getConfigurationForLoader({ + options, + _window, + nlsConfig: nlsConfig, + }) + + expect(loader.trustedTypesPolicy).not.toBe(undefined) + expect(loader.trustedTypesPolicy.name).toBe("amdLoader") + }) + }) + describe("_createScriptURL", () => { + it("should return the correct url", () => { + const url = _createScriptURL("localhost/foo/bar.js", "localhost") + + expect(url).toBe("localhost/foo/bar.js") + }) + it("should throw if the value doesn't start with the origin", () => { + expect(() => { + _createScriptURL("localhost/foo/bar.js", "coder.com") + }).toThrow("Invalid script url: localhost/foo/bar.js") + }) + }) + describe("main", () => { + let _window: Window + let _document: Document + let _localStorage: Storage + + beforeEach(() => { + // We need to set the url in the JSDOM constructor + // to prevent this error "SecurityError: localStorage is not available for opaque origins" + // See: https://github.com/jsdom/jsdom/issues/2304#issuecomment-62231494 + const { window: __window } = new JSDOM("", { url: "http://localhost" }) + // @ts-expect-error the Window from JSDOM is not exactly the same as Window + // so we expect an error here + _window = __window + _document = __window.document + _localStorage = __window.localStorage + + const mockElement = _document.createElement("div") + const dataSettings = { + first: "Jane", + last: "Doe", + } + + mockElement.setAttribute("id", nlsConfigElementId) + mockElement.setAttribute("data-settings", JSON.stringify(dataSettings)) + _document.body.appendChild(mockElement) + + const test = { + colorMap: { + [`editor.background`]: "#ff3270", + }, + } + _localStorage.setItem("colorThemeData", JSON.stringify(test)) + }) + afterEach(() => { + _localStorage.removeItem("colorThemeData") + }) + it("should throw if document is missing", () => { + expect(() => { + main(undefined, _window, _localStorage) + }).toThrow("document is undefined.") + }) + it("should throw if window is missing", () => { + expect(() => { + main(_document, undefined, _localStorage) + }).toThrow("window is undefined.") + }) + it("should throw if localStorage is missing", () => { + expect(() => { + main(_document, _window, undefined) + }).toThrow("localStorage is undefined.") + }) + it("should add loader to self.require", () => { + main(_document, _window, _localStorage) + + expect(Object.prototype.hasOwnProperty.call(self, "require")).toBe(true) + }) + it("should not throw in browser context", () => { + // Assuming we call it in a normal browser context + // where everything is defined + expect(() => { + main(_document, _window, _localStorage) + }).not.toThrow() }) }) }) diff --git a/test/unit/node/util.test.ts b/test/unit/node/util.test.ts index e765be6cf..7945b6962 100644 --- a/test/unit/node/util.test.ts +++ b/test/unit/node/util.test.ts @@ -468,7 +468,7 @@ describe("pathToFsPath", () => { expect(util.pathToFsPath("/C:/far/bo", true)).toBe("C:/far/bo") }) it("should replace / with \\ on Windows", () => { - let ORIGINAL_PLATFORM = process.platform + const ORIGINAL_PLATFORM = process.platform Object.defineProperty(process, "platform", { value: "win32",