Skip to content

Commit

Permalink
from ESM land bring over amdX-util and bring over one of its usages (#…
Browse files Browse the repository at this point in the history
…186748)

* from ESM land bring over amdX-util and bring over one of its usages

* fix tsec violation

* one more exemption

* grrrr

* last try, fingers crossed
  • Loading branch information
jrieken authored Jun 30, 2023
1 parent 3aaa81b commit 69cd143
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 5 deletions.
3 changes: 2 additions & 1 deletion .eslintplugin/code-import-patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,9 @@ export = new class implements eslint.Rule.RuleModule {
const restrictions = (typeof option.restrictions === 'string' ? [option.restrictions] : option.restrictions).slice(0);

if (targetIsVS) {
// Always add "vs/nls"
// Always add "vs/nls" and "vs/amdX"
restrictions.push('vs/nls');
restrictions.push('vs/amdX'); // TODO@jrieken remove after ESM is real
}

if (targetIsVS && option.layer) {
Expand Down
6 changes: 6 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,12 @@
"vs/workbench/workbench.common.main"
]
},
{
"target": "src/vs/amdX.ts",
"restrictions": [
"vs/base/common/*"
]
},
{
"target": "src/vs/workbench/{workbench.desktop.main.nls.js,workbench.web.main.nls.js}",
"restrictions": []
Expand Down
2 changes: 2 additions & 0 deletions src/tsec.exemptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"vs/workbench/services/keybinding/test/node/keyboardMapperTestUtils.ts"
],
"ban-trustedtypes-createpolicy": [
"vs/amdX.ts",
"vs/base/browser/trustedTypes.ts",
"vs/base/worker/workerMain.ts",
"vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts"
Expand All @@ -23,6 +24,7 @@
"vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts"
],
"ban-worker-importscripts": [
"vs/amdX.ts",
"vs/workbench/services/extensions/worker/polyfillNestedWorker.ts",
"vs/workbench/api/worker/extensionHostWorker.ts",
"vs/base/worker/workerMain.ts"
Expand Down
2 changes: 2 additions & 0 deletions src/typings/vscode-globals-modules.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ declare global {
net: typeof import('net');
os: typeof import('os');
module: typeof import('module');
fs: typeof import('fs'),
vm: typeof import('vm'),
['native-watchdog']: typeof import('native-watchdog')
perf_hooks: typeof import('perf_hooks');

Expand Down
204 changes: 204 additions & 0 deletions src/vs/amdX.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { isESM } from 'vs/base/common/amd';
import { AppResourcePath, FileAccess, nodeModulesAsarPath, nodeModulesPath } from 'vs/base/common/network';
import * as platform from 'vs/base/common/platform';
import { IProductConfiguration } from 'vs/base/common/product';
import { URI } from 'vs/base/common/uri';

class DefineCall {
constructor(
public readonly id: string | null | undefined,
public readonly dependencies: string[] | null | undefined,
public readonly callback: any
) { }
}

class AMDModuleImporter {
public static INSTANCE = new AMDModuleImporter();

private readonly _isWebWorker = (typeof self === 'object' && self.constructor && self.constructor.name === 'DedicatedWorkerGlobalScope');
private readonly _isRenderer = typeof document === 'object';

private readonly _defineCalls: DefineCall[] = [];
private _initialized = false;
private _amdPolicy: Pick<TrustedTypePolicy<{
createScriptURL(value: string): string;
}>, 'name' | 'createScriptURL'> | undefined;

constructor() { }

private _initialize(): void {
if (this._initialized) {
return;
}
this._initialized = true;

(<any>globalThis).define = (id: any, dependencies: any, callback: any) => {
if (typeof id !== 'string') {
callback = dependencies;
dependencies = id;
id = null;
}
if (typeof dependencies !== 'object' || !Array.isArray(dependencies)) {
callback = dependencies;
dependencies = null;
}
// if (!dependencies) {
// dependencies = ['require', 'exports', 'module'];
// }
this._defineCalls.push(new DefineCall(id, dependencies, callback));
};

(<any>globalThis).define.amd = true;

if (this._isRenderer) {
this._amdPolicy = window.trustedTypes?.createPolicy('amdLoader', {
createScriptURL(value) {
if (value.startsWith(window.location.origin)) {
return value;
}
if (value.startsWith('vscode-file://vscode-app')) {
return value;
}
throw new Error(`[trusted_script_src] Invalid script url: ${value}`);
}
});
} else if (this._isWebWorker) {
this._amdPolicy = (<any>globalThis).trustedTypes?.createPolicy('amdLoader', {
createScriptURL(value: string) {
return value;
}
});
}
}

public async load<T>(scriptSrc: string): Promise<T> {
this._initialize();
const defineCall = await (this._isWebWorker ? this._workerLoadScript(scriptSrc) : this._isRenderer ? this._rendererLoadScript(scriptSrc) : this._nodeJSLoadScript(scriptSrc));
if (!defineCall) {
throw new Error(`Did not receive a define call from script ${scriptSrc}`);
}
// TODO require, exports, module
if (Array.isArray(defineCall.dependencies) && defineCall.dependencies.length > 0) {
throw new Error(`Cannot resolve dependencies for script ${scriptSrc}. The dependencies are: ${defineCall.dependencies.join(', ')}`);
}
if (typeof defineCall.callback === 'function') {
return defineCall.callback([]);
} else {
return defineCall.callback;
}
}

private _rendererLoadScript(scriptSrc: string): Promise<DefineCall | undefined> {
return new Promise<DefineCall | undefined>((resolve, reject) => {
const scriptElement = document.createElement('script');
scriptElement.setAttribute('async', 'async');
scriptElement.setAttribute('type', 'text/javascript');

const unbind = () => {
scriptElement.removeEventListener('load', loadEventListener);
scriptElement.removeEventListener('error', errorEventListener);
};

const loadEventListener = (e: any) => {
unbind();
resolve(this._defineCalls.pop());
};

const errorEventListener = (e: any) => {
unbind();
reject(e);
};

scriptElement.addEventListener('load', loadEventListener);
scriptElement.addEventListener('error', errorEventListener);
if (this._amdPolicy) {
scriptSrc = this._amdPolicy.createScriptURL(scriptSrc) as any as string;
}
scriptElement.setAttribute('src', scriptSrc);
document.getElementsByTagName('head')[0].appendChild(scriptElement);
});
}

private _workerLoadScript(scriptSrc: string): Promise<DefineCall | undefined> {
return new Promise<DefineCall | undefined>((resolve, reject) => {
try {
if (this._amdPolicy) {
scriptSrc = this._amdPolicy.createScriptURL(scriptSrc) as any as string;
}
importScripts(scriptSrc);
resolve(this._defineCalls.pop());
} catch (err) {
reject(err);
}
});
}

private async _nodeJSLoadScript(scriptSrc: string): Promise<DefineCall | undefined> {
try {
const fs = <typeof import('fs')>globalThis._VSCODE_NODE_MODULES['fs'];
const vm = <typeof import('vm')>globalThis._VSCODE_NODE_MODULES['vm'];
const module = <typeof import('module')>globalThis._VSCODE_NODE_MODULES['module'];

const filePath = URI.parse(scriptSrc).fsPath;
const content = fs.readFileSync(filePath).toString();
const scriptSource = module.wrap(content.replace(/^#!.*/, ''));
const script = new vm.Script(scriptSource);
const compileWrapper = script.runInThisContext();
compileWrapper.apply();
return this._defineCalls.pop();

} catch (error) {
throw error;
}
}
}

const cache = new Map<string, Promise<any>>();

let _paths: Record<string, string> = {};
if (typeof globalThis.require === 'object') {
_paths = (<Record<string, any>>globalThis.require).paths ?? {};
}

/**
* e.g. pass in `vscode-textmate/release/main.js`
*/
export async function importAMDNodeModule<T>(nodeModuleName: string, pathInsideNodeModule: string, isBuilt?: boolean): Promise<T> {
if (isESM) {

if (isBuilt === undefined) {
const product = globalThis._VSCODE_PRODUCT_JSON as unknown as IProductConfiguration;
isBuilt = !!product?.commit;
}

if (_paths[nodeModuleName]) {
nodeModuleName = _paths[nodeModuleName];
}

const nodeModulePath = `${nodeModuleName}/${pathInsideNodeModule}`;
if (cache.has(nodeModulePath)) {
return cache.get(nodeModulePath)!;
}
let scriptSrc: string;
if (/^\w[\w\d+.-]*:\/\//.test(nodeModulePath)) {
// looks like a URL
// bit of a special case for: src/vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker.ts
scriptSrc = nodeModulePath;
} else {
const useASAR = (isBuilt && !platform.isWeb);
const actualNodeModulesPath = (useASAR ? nodeModulesAsarPath : nodeModulesPath);
const resourcePath: AppResourcePath = `${actualNodeModulesPath}/${nodeModulePath}`;
scriptSrc = FileAccess.asBrowserUri(resourcePath).toString(true);
}
const result = AMDModuleImporter.INSTANCE.load<T>(scriptSrc);
cache.set(nodeModulePath, result);
return result;
} else {
return await import(nodeModuleName);
}
}
7 changes: 7 additions & 0 deletions src/vs/base/common/amd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

// ESM-comment-begin
export const isESM = false;
// ESM-comment-end
// ESM-uncomment-begin
// export const isESM = true;
// ESM-uncomment-end

export abstract class LoaderStats {
abstract get amdLoad(): [string, number][];
abstract get amdInvoke(): [string, number][];
Expand Down
9 changes: 5 additions & 4 deletions src/vs/workbench/services/textfile/common/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { Readable, ReadableStream, newWriteableStream, listenStream } from 'vs/base/common/stream';
import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer';
import { importAMDNodeModule } from 'vs/amdX';

export const UTF8 = 'utf8';
export const UTF8_with_bom = 'utf8bom';
Expand Down Expand Up @@ -78,7 +79,7 @@ class DecoderStream implements IDecoderStream {
static async create(encoding: string): Promise<DecoderStream> {
let decoder: IDecoderStream | undefined = undefined;
if (encoding !== UTF8) {
const iconv = await import('@vscode/iconv-lite-umd');
const iconv = await importAMDNodeModule<typeof import('@vscode/iconv-lite-umd')>('@vscode/iconv-lite-umd', 'lib/iconv-lite-umd.js');
decoder = iconv.getDecoder(toNodeEncoding(encoding));
} else {
const utf8TextDecoder = new TextDecoder();
Expand Down Expand Up @@ -209,7 +210,7 @@ export function toDecodeStream(source: VSBufferReadableStream, options: IDecodeS
}

export async function toEncodeReadable(readable: Readable<string>, encoding: string, options?: { addBOM?: boolean }): Promise<VSBufferReadable> {
const iconv = await import('@vscode/iconv-lite-umd');
const iconv = await importAMDNodeModule<typeof import('@vscode/iconv-lite-umd')>('@vscode/iconv-lite-umd', 'lib/iconv-lite-umd.js');
const encoder = iconv.getEncoder(toNodeEncoding(encoding), options);

let bytesWritten = false;
Expand Down Expand Up @@ -258,7 +259,7 @@ export async function toEncodeReadable(readable: Readable<string>, encoding: str
}

export async function encodingExists(encoding: string): Promise<boolean> {
const iconv = await import('@vscode/iconv-lite-umd');
const iconv = await importAMDNodeModule<typeof import('@vscode/iconv-lite-umd')>('@vscode/iconv-lite-umd', 'lib/iconv-lite-umd.js');

return iconv.encodingExists(toNodeEncoding(encoding));
}
Expand Down Expand Up @@ -314,7 +315,7 @@ const IGNORE_ENCODINGS = ['ascii', 'utf-16', 'utf-32'];
* Guesses the encoding from buffer.
*/
async function guessEncodingByBuffer(buffer: VSBuffer): Promise<string | null> {
const jschardet = await import('jschardet');
const jschardet = await importAMDNodeModule<typeof import('jschardet')>('jschardet', 'dist/jschardet.min.js');

// ensure to limit buffer for guessing due to https://github.com/aadsm/jschardet/issues/53
const limitedBuffer = buffer.slice(0, AUTO_ENCODING_GUESS_MAX_BYTES);
Expand Down

0 comments on commit 69cd143

Please sign in to comment.