packages/core/src/shared/extensionGlobals.ts (138 lines of code) (raw):
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import { ExtensionContext, LogOutputChannel, OutputChannel } from 'vscode'
import { LoginManager } from '../auth/deprecated/loginManager'
import { AwsResourceManager } from '../dynamicResources/awsResourceManager'
import { AWSClientBuilder } from './awsClientBuilder'
import { AwsContext } from './awsContext'
import { AwsContextCommands } from './awsContextCommands'
import { RegionProvider } from './regions/regionProvider'
import { CloudFormationTemplateRegistry } from './fs/templateRegistry'
import { CodelensRootRegistry } from './fs/codelensRootRegistry'
import { SchemaService } from './schemas'
import { TelemetryLogger } from './telemetry/telemetryLogger'
import { TelemetryService } from './telemetry/telemetryService'
import { UriHandler } from './vscode/uriHandler'
import { GlobalState } from './globalState'
import { setContext } from './vscode/setContext'
import { getLogger } from './logger/logger'
import { AWSClientBuilderV3 } from './awsClientBuilderV3'
type Clock = Pick<
typeof globalThis,
| 'setTimeout'
| 'setImmediate'
| 'setInterval'
| 'clearTimeout'
| 'clearImmediate'
| 'clearInterval'
| 'Date'
| 'Promise'
>
/**
* Copies all *enumerable* properties from the global object.
* Some properties have to be added manually depending on how they exist on the prototype.
*/
function copyClock(): Clock {
const clock: any = {
setTimeout: globalThis.setTimeout.bind(globalThis),
setInterval: globalThis.setInterval.bind(globalThis),
clearTimeout: globalThis.clearTimeout.bind(globalThis),
clearInterval: globalThis.clearInterval.bind(globalThis),
Date,
Promise,
}
const browserAlternatives = getBrowserAlternatives()
if (Object.keys(browserAlternatives).length > 0) {
getLogger().info('globals: Using browser alternatives for clock functions')
Object.assign(clock, browserAlternatives)
} else {
// In node.js context
clock.setImmediate = globalThis.setImmediate.bind(globalThis)
clock.clearImmediate = globalThis.clearImmediate.bind(globalThis)
}
return clock
}
/**
* If we are in browser certain functions are not available, so
* we create alternatives for them.
*/
function getBrowserAlternatives() {
const alternatives = {} as any
if (globalThis.setImmediate === undefined) {
// "A setTimeout() callback with a 0ms delay is very similar to setImmediate()"
// https://nodejs.dev/en/learn/understanding-setimmediate/
alternatives['setImmediate'] = (callback: (...args: any[]) => void, ...args: any[]) => {
return globalThis.setTimeout(callback, 0, ...args)
}
alternatives['clearImmediate'] = (handle: any) => {
globalThis.clearTimeout(handle)
}
}
return alternatives
}
/**
* XXX: Web-mode tests (as opposed to Node.js tests) don't see changes to exported module variables.
*
* Workaround: store variables in `globalThis` so that web-mode tests can share them.
*
* See `web.md` for more info.
*
* Note: The returned globals is shared across all extensions/the entire VS Code instance.
*
*/
function resolveGlobalsObject(): ToolkitGlobals {
if ((globalThis as any).globals === undefined) {
;(globalThis as any).globals = { clock: copyClock() } as ToolkitGlobals
}
return (globalThis as any).globals
}
/**
* Throw a more intuitive error if any code tries to use `globals` before `initialize()` was called.
*/
function proxyGlobals(globals_: ToolkitGlobals): ToolkitGlobals {
return new Proxy(globals_, {
get: (target, prop) => {
// Test for initialize()
if (
!initialized &&
!target.isWeb // extension instance globals would have set this truly globally prior to the test instance globals being accessed.
) {
throw new Error(`ToolkitGlobals accessed before initialize()`)
}
// Test that the property was set before access.
// Tradeoff: not being able to do something like `globals.myValue ??= ...` without a try/catch
const propName = String(prop)
const val = (target as any)[propName]
if (
val !== undefined ||
propName.includes('Symbol') // hack for sinon.stub
) {
return val
}
throw new Error(`ToolkitGlobals.${propName} accessed, but this property is not set.`)
},
})
}
/**
* Extension globals object.
* Unless this is running in web mode, these globals are scoped only to the current extension.
*
* TODO: If multiple extensions are running in webmode, they will override and access
* each other's globals. We should partition globalThis by extension ID.
*/
let globals = proxyGlobals(resolveGlobalsObject())
export function checkDidReload(context: ExtensionContext): boolean {
// TODO: fix this
// eslint-disable-next-line aws-toolkits/no-banned-usages
return !!context.globalState.get<string>('ACTIVATION_LAUNCH_PATH_KEY')
}
let initialized = false
export function initialize(context: ExtensionContext, isWeb: boolean = false): ToolkitGlobals {
if (!isWeb) {
// Not running in web mode, let's use globals scoped to the current extension only.
globals = proxyGlobals({} as ToolkitGlobals)
}
Object.assign(globals, {
context,
clock: copyClock(),
didReload: checkDidReload(context),
// eslint-disable-next-line aws-toolkits/no-banned-usages
globalState: new GlobalState(context.globalState),
manifestPaths: {} as ToolkitGlobals['manifestPaths'],
isWeb,
})
void setContext('aws.isWebExtHost', isWeb)
initialized = true
return globals
}
export function isWeb() {
return globals.isWeb
}
export { globals as default }
/**
* Namespace for common variables used globally in the extension.
* All variables here must be initialized in the activate() method of extension.ts
*/
export interface ToolkitGlobals {
readonly context: ExtensionContext
/** Global, shared (with all vscode instances, including remote!), mutable, persisted state (survives IDE restart), namespaced to the extension (not shared with other vscode extensions). */
readonly globalState: GlobalState
/** Decides the prefix for package.json extension parameters, e.g. commands, 'setContext' values, etc. */
contextPrefix: string
//
// TODO: make the rest of these readonly (or delete them)
//
/**
* For "normal" messages, to show output from various application features (the result of
* a Lambda invocation, "sam build" output, etc.).
*/
outputChannel: OutputChannel
/**
* Log messages. Use `outputChannel` for application messages.
*/
logOutputChannel: LogOutputChannel
loginManager: LoginManager
awsContextCommands: AwsContextCommands
awsContext: AwsContext
regionProvider: RegionProvider
sdkClientBuilder: AWSClientBuilder
sdkClientBuilderV3: AWSClientBuilderV3
telemetry: TelemetryService & { logger: TelemetryLogger }
/** template.yaml registry. _Avoid_ calling this until it is actually needed (for SAM features). */
templateRegistry: Promise<CloudFormationTemplateRegistry>
schemaService: SchemaService
codelensRootRegistry: CodelensRootRegistry
resourceManager: AwsResourceManager
uriHandler: UriHandler
/** An id to differentiate the current machine being run on. Can help distinguish a remote from a local machine. */
machineId: string
/**
* Whether the current session was (likely) a reload forced by VSCode during a workspace folder operation.
*/
readonly didReload: boolean
/**
* This is a shallow copy of the global `this` object.
*
* Using a separate clock from the global one allows us to scope down behavior for testing.
* Keep in mind that this clock's `Date` constructor will be different than the global one when mocked.
*/
readonly clock: Clock
readonly manifestPaths: {
endpoints: string
lambdaSampleRequests: string
}
/** If this extension is running in Web mode (the browser), compared to running on the desktop (node) */
isWeb: boolean
}