Skip to content

Commit

Permalink
Remove dependency on code/browser/workbench/workbench (#137)
Browse files Browse the repository at this point in the history
* remove dependency on code/browser/workbench/workbench

* complete

* polish
  • Loading branch information
aeschli authored Sep 4, 2024
1 parent b4efa3f commit ed9ff7a
Show file tree
Hide file tree
Showing 14 changed files with 2,342 additions and 72 deletions.
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/out/index.js",
"program": "${workspaceFolder}/out/server/index.js",
"args": [
"--browserType=chromium",
"--extensionDevelopmentPath=${workspaceFolder}/sample",
Expand All @@ -58,7 +58,7 @@
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/out/index.js",
"program": "${workspaceFolder}/out/server/index.js",
"args": [
"--browserType=chromium",
"--extensionDevelopmentPath=${workspaceFolder}/sample",
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"version": "0.0.57",
"scripts": {
"install-extensions": "npm i --prefix=fs-provider && npm i --prefix=sample",
"compile": "tsc -p ./ && npm run compile-fs-provider",
"watch": "tsc -w -p ./",
"compile": "tsc -b ./ && npm run compile-fs-provider",
"watch": "tsc -b -w ./",
"prepack": "npm run compile",
"test": "eslint src && tsc --noEmit",
"preversion": "npm test",
Expand All @@ -15,9 +15,9 @@
"sample-tests": "npm run compile && npm run compile-sample && node . --extensionDevelopmentPath=sample --extensionTestsPath=sample/dist/web/test/suite/index.js --headless=true sample/test-workspace",
"empty": "npm run compile && node ."
},
"main": "./out/index.js",
"main": "./out/server/index.js",
"bin": {
"vscode-test-web": "./out/index.js"
"vscode-test-web": "./out/server/index.js"
},
"engines": {
"node": ">=16"
Expand Down
272 changes: 272 additions & 0 deletions src/browser/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
///<amd-module name='vscode-web-browser-main'/>

import { create, IWorkspaceProvider, IWorkbenchConstructionOptions, UriComponents, IWorkspace, URI, IURLCallbackProvider, Emitter, IDisposable} from './workbench.api';

class WorkspaceProvider implements IWorkspaceProvider {

private static QUERY_PARAM_EMPTY_WINDOW = 'ew';
private static QUERY_PARAM_FOLDER = 'folder';
private static QUERY_PARAM_WORKSPACE = 'workspace';

private static QUERY_PARAM_PAYLOAD = 'payload';

static create(config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents }) {
let foundWorkspace = false;
let workspace: IWorkspace;
let payload = Object.create(null);

const query = new URL(document.location.href).searchParams;
query.forEach((value, key) => {
switch (key) {

// Folder
case WorkspaceProvider.QUERY_PARAM_FOLDER:
workspace = { folderUri: URI.parse(value) };
foundWorkspace = true;
break;

// Workspace
case WorkspaceProvider.QUERY_PARAM_WORKSPACE:
workspace = { workspaceUri: URI.parse(value) };
foundWorkspace = true;
break;

// Empty
case WorkspaceProvider.QUERY_PARAM_EMPTY_WINDOW:
workspace = undefined;
foundWorkspace = true;
break;

// Payload
case WorkspaceProvider.QUERY_PARAM_PAYLOAD:
try {
payload = JSON.parse(value);
} catch (error) {
console.error(error); // possible invalid JSON
}
break;
}
});

// If no workspace is provided through the URL, check for config
// attribute from server
if (!foundWorkspace) {
if (config.folderUri) {
workspace = { folderUri: URI.revive(config.folderUri) };
} else if (config.workspaceUri) {
workspace = { workspaceUri: URI.revive(config.workspaceUri) };
}
}

return new WorkspaceProvider(workspace, payload);
}

readonly trusted = true;

private constructor(
readonly workspace: IWorkspace,
readonly payload: object,
) {
}

async open(workspace: IWorkspace, options?: { reuse?: boolean; payload?: object }): Promise<boolean> {
if (options?.reuse && !options.payload && this.isSame(this.workspace, workspace)) {
return true; // return early if workspace and environment is not changing and we are reusing window
}

const targetHref = this.createTargetUrl(workspace, options);
if (targetHref) {
if (options?.reuse) {
window.location.href = targetHref;
return true;
} else {
return !!window.open(targetHref);
}
}
return false;
}

private createTargetUrl(workspace: IWorkspace, options?: { reuse?: boolean; payload?: object }): string | undefined {

// Empty
let targetHref: string | undefined = undefined;
if (!workspace) {
targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_EMPTY_WINDOW}=true`;
}

// Folder
else if ('folderUri' in workspace) {
const queryParamFolder = encodeURIComponent(workspace.folderUri.toString(true));
targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${queryParamFolder}`;
}

// Workspace
else if ('workspaceUri' in workspace) {
const queryParamWorkspace = encodeURIComponent(workspace.workspaceUri.toString(true));
targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${queryParamWorkspace}`;
}

// Append payload if any
if (options?.payload) {
targetHref += `&${WorkspaceProvider.QUERY_PARAM_PAYLOAD}=${encodeURIComponent(JSON.stringify(options.payload))}`;
}

return targetHref;
}

private isSame(workspaceA: IWorkspace, workspaceB: IWorkspace): boolean {
if (!workspaceA || !workspaceB) {
return workspaceA === workspaceB; // both empty
}

if ('folderUri' in workspaceA && 'folderUri' in workspaceB) {
return isEqual(workspaceA.folderUri, workspaceB.folderUri); // same workspace
}

if ('workspaceUri' in workspaceA && 'workspaceUri' in workspaceB) {
return isEqual(workspaceA.workspaceUri, workspaceB.workspaceUri); // same workspace
}

return false;
}

}

class LocalStorageURLCallbackProvider implements IURLCallbackProvider, IDisposable {

private static REQUEST_ID = 0;

private static QUERY_KEYS: ('scheme' | 'authority' | 'path' | 'query' | 'fragment')[] = [
'scheme',
'authority',
'path',
'query',
'fragment'
];

private readonly _onCallback = new Emitter<URI>();
readonly onCallback = this._onCallback.event;

private pendingCallbacks = new Set<number>();
private lastTimeChecked = Date.now();
private checkCallbacksTimeout: unknown | undefined = undefined;
private onDidChangeLocalStorageDisposable: IDisposable | undefined;

constructor(private readonly _callbackRoute: string) {
}

create(options: Partial<UriComponents> = {}): URI {
const id = ++LocalStorageURLCallbackProvider.REQUEST_ID;
const queryParams: string[] = [`vscode-reqid=${id}`];

for (const key of LocalStorageURLCallbackProvider.QUERY_KEYS) {
const value = options[key];

if (value) {
queryParams.push(`vscode-${key}=${encodeURIComponent(value)}`);
}
}

// TODO@joao remove eventually
// https://github.com/microsoft/vscode-dev/issues/62
// https://github.com/microsoft/vscode/blob/159479eb5ae451a66b5dac3c12d564f32f454796/extensions/github-authentication/src/githubServer.ts#L50-L50
if (!(options.authority === 'vscode.github-authentication' && options.path === '/dummy')) {
const key = `vscode-web.url-callbacks[${id}]`;
localStorage.removeItem(key);

this.pendingCallbacks.add(id);
this.startListening();
}

return URI.parse(window.location.href).with({ path: this._callbackRoute, query: queryParams.join('&') });
}

private startListening(): void {
if (this.onDidChangeLocalStorageDisposable) {
return;
}

const fn = () => this.onDidChangeLocalStorage();
window.addEventListener('storage', fn);
this.onDidChangeLocalStorageDisposable = { dispose: () => window.removeEventListener('storage', fn) };
}

private stopListening(): void {
this.onDidChangeLocalStorageDisposable?.dispose();
this.onDidChangeLocalStorageDisposable = undefined;
}

// this fires every time local storage changes, but we
// don't want to check more often than once a second
private async onDidChangeLocalStorage(): Promise<void> {
const ellapsed = Date.now() - this.lastTimeChecked;

if (ellapsed > 1000) {
this.checkCallbacks();
} else if (this.checkCallbacksTimeout === undefined) {
this.checkCallbacksTimeout = setTimeout(() => {
this.checkCallbacksTimeout = undefined;
this.checkCallbacks();
}, 1000 - ellapsed);
}
}

private checkCallbacks(): void {
let pendingCallbacks: Set<number> | undefined;

for (const id of this.pendingCallbacks) {
const key = `vscode-web.url-callbacks[${id}]`;
const result = localStorage.getItem(key);

if (result !== null) {
try {
this._onCallback.fire(URI.revive(JSON.parse(result)));
} catch (error) {
console.error(error);
}

pendingCallbacks = pendingCallbacks ?? new Set(this.pendingCallbacks);
pendingCallbacks.delete(id);
localStorage.removeItem(key);
}
}

if (pendingCallbacks) {
this.pendingCallbacks = pendingCallbacks;

if (this.pendingCallbacks.size === 0) {
this.stopListening();
}
}

this.lastTimeChecked = Date.now();
}

dispose(): void {
this._onCallback.dispose();
}
}

function isEqual(a: UriComponents, b: UriComponents): boolean {
return a.scheme === b.scheme && a.authority === b.authority && a.path === b.path;
}

(function () {
const configElement = window.document.getElementById('vscode-workbench-web-configuration');
const configElementAttribute = configElement ? configElement.getAttribute('data-settings') : undefined;
if (!configElement || !configElementAttribute) {
throw new Error('Missing web configuration element');
}
const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents; callbackRoute: string } = JSON.parse(configElementAttribute);

create(window.document.body, {
...config,
workspaceProvider: WorkspaceProvider.create(config),
urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute)
});

})();
22 changes: 22 additions & 0 deletions src/browser/tsconfig-amd.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "AMD",
"lib": [
"ES2022",
"DOM",
],
"outDir": "../../out/browser/amd",
"declaration": true,
"strict": true,
"noImplicitAny": false,
"noImplicitThis": true,
"noUnusedLocals": true,
"alwaysStrict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": false,
"newLine": "lf",
"removeComments": true
}
}
22 changes: 22 additions & 0 deletions src/browser/tsconfig-esm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": [
"ES2022",
"DOM",
],
"outDir": "../../out/browser/esm",
"declaration": true,
"strict": true,
"noImplicitAny": false,
"noImplicitThis": true,
"noUnusedLocals": true,
"alwaysStrict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": false,
"newLine": "lf",
"removeComments": true
}
}
Loading

0 comments on commit ed9ff7a

Please sign in to comment.