diff --git a/packages/vite/src/node/__tests__/plugins/hooks.spec.ts b/packages/vite/src/node/__tests__/plugins/hooks.spec.ts new file mode 100644 index 00000000000000..16205f78783c5f --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/hooks.spec.ts @@ -0,0 +1,279 @@ +import path from 'node:path' +import { describe, expect, onTestFinished, test } from 'vitest' +import { build } from '../../build' +import type { Plugin } from '../../plugin' +import { resolveConfig } from '../../config' +import { createServer } from '../../server' +import { preview } from '../../preview' +import { promiseWithResolvers } from '../../../shared/utils' + +const resolveConfigWithPlugin = ( + plugin: Plugin, + command: 'serve' | 'build' = 'serve', +) => { + return resolveConfig( + { configFile: false, plugins: [plugin], logLevel: 'error' }, + command, + ) +} + +const createServerWithPlugin = async (plugin: Plugin) => { + const server = await createServer({ + configFile: false, + root: import.meta.dirname, + plugins: [plugin], + logLevel: 'error', + server: { + middlewareMode: true, + }, + }) + onTestFinished(() => server.close()) + return server +} + +const createPreviewServerWithPlugin = async (plugin: Plugin) => { + const server = await preview({ + configFile: false, + root: import.meta.dirname, + plugins: [ + { + name: 'mock-preview', + configurePreviewServer({ httpServer }) { + // NOTE: make httpServer.listen no-op to avoid starting a server + httpServer.listen = (...args: unknown[]) => { + const listener = args.at(-1) as () => void + listener() + return httpServer as any + } + }, + }, + plugin, + ], + logLevel: 'error', + }) + onTestFinished(() => server.close()) + return server +} + +const buildWithPlugin = async (plugin: Plugin) => { + await build({ + root: path.resolve(import.meta.dirname, '../packages/build-project'), + logLevel: 'error', + build: { + write: false, + }, + plugins: [ + { + name: 'resolve-entry.js', + resolveId(id) { + if (id === 'entry.js') { + return '\0' + id + } + }, + load(id) { + if (id === '\0entry.js') { + return 'export default {}' + } + }, + }, + plugin, + ], + }) +} + +describe('supports plugin context', () => { + test('config hook', async () => { + expect.assertions(3) + + await resolveConfigWithPlugin({ + name: 'test', + config() { + expect(this).toMatchObject({ + debug: expect.any(Function), + info: expect.any(Function), + warn: expect.any(Function), + error: expect.any(Function), + meta: expect.any(Object), + }) + expect(this.meta.rollupVersion).toBeTypeOf('string') + // @ts-expect-error watchMode should not exist in types + expect(this.meta.watchMode).toBeUndefined() + }, + }) + }) + + test('configEnvironment hook', async () => { + expect.assertions(3) + + await resolveConfigWithPlugin({ + name: 'test', + configEnvironment(name) { + if (name !== 'client') return + + expect(this).toMatchObject({ + debug: expect.any(Function), + info: expect.any(Function), + warn: expect.any(Function), + error: expect.any(Function), + meta: expect.any(Object), + }) + expect(this.meta.rollupVersion).toBeTypeOf('string') + // @ts-expect-error watchMode should not exist in types + expect(this.meta.watchMode).toBeUndefined() + }, + }) + }) + + test('configResolved hook', async () => { + expect.assertions(3) + + await resolveConfigWithPlugin({ + name: 'test', + configResolved() { + expect(this).toMatchObject({ + debug: expect.any(Function), + info: expect.any(Function), + warn: expect.any(Function), + error: expect.any(Function), + meta: expect.any(Object), + }) + expect(this.meta.rollupVersion).toBeTypeOf('string') + expect(this.meta.watchMode).toBe(true) + }, + }) + }) + + test('configureServer hook', async () => { + expect.assertions(3) + + await createServerWithPlugin({ + name: 'test', + configureServer() { + expect(this).toMatchObject({ + debug: expect.any(Function), + info: expect.any(Function), + warn: expect.any(Function), + error: expect.any(Function), + meta: expect.any(Object), + }) + expect(this.meta.rollupVersion).toBeTypeOf('string') + expect(this.meta.watchMode).toBe(true) + }, + }) + }) + + test('configurePreviewServer hook', async () => { + expect.assertions(3) + + await createPreviewServerWithPlugin({ + name: 'test', + configurePreviewServer() { + expect(this).toMatchObject({ + debug: expect.any(Function), + info: expect.any(Function), + warn: expect.any(Function), + error: expect.any(Function), + meta: expect.any(Object), + }) + expect(this.meta.rollupVersion).toBeTypeOf('string') + expect(this.meta.watchMode).toBe(false) + }, + }) + }) + + test('transformIndexHtml hook in dev', async () => { + expect.assertions(3) + + const server = await createServerWithPlugin({ + name: 'test', + transformIndexHtml() { + expect(this).toMatchObject({ + debug: expect.any(Function), + info: expect.any(Function), + warn: expect.any(Function), + error: expect.any(Function), + meta: expect.any(Object), + }) + expect(this.meta.rollupVersion).toBeTypeOf('string') + expect(this.meta.watchMode).toBe(true) + }, + }) + await server.transformIndexHtml('/index.html', '') + }) + + test('transformIndexHtml hook in build', async () => { + expect.assertions(3) + + await buildWithPlugin({ + name: 'test', + transformIndexHtml() { + expect(this).toMatchObject({ + debug: expect.any(Function), + info: expect.any(Function), + warn: expect.any(Function), + error: expect.any(Function), + meta: expect.any(Object), + }) + expect(this.meta.rollupVersion).toBeTypeOf('string') + expect(this.meta.watchMode).toBe(false) + }, + }) + }) + + test('handleHotUpdate hook', async () => { + expect.assertions(3) + + const { promise, resolve } = promiseWithResolvers() + const server = await createServerWithPlugin({ + name: 'test', + handleHotUpdate() { + expect(this).toMatchObject({ + debug: expect.any(Function), + info: expect.any(Function), + warn: expect.any(Function), + error: expect.any(Function), + meta: expect.any(Object), + }) + expect(this.meta.rollupVersion).toBeTypeOf('string') + expect(this.meta.watchMode).toBe(true) + resolve() + }, + }) + server.watcher.emit( + 'change', + path.resolve(import.meta.dirname, 'index.html'), + ) + + await promise + }) + + test('hotUpdate hook', async () => { + expect.assertions(3) + + const { promise, resolve } = promiseWithResolvers() + const server = await createServerWithPlugin({ + name: 'test', + hotUpdate() { + if (this.environment.name !== 'client') return + + expect(this).toMatchObject({ + debug: expect.any(Function), + info: expect.any(Function), + warn: expect.any(Function), + error: expect.any(Function), + meta: expect.any(Object), + environment: expect.any(Object), + }) + expect(this.meta.rollupVersion).toBeTypeOf('string') + expect(this.meta.watchMode).toBe(true) + resolve() + }, + }) + server.watcher.emit( + 'change', + path.resolve(import.meta.dirname, 'index.html'), + ) + + await promise + }) +}) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index bca15149ec57cd..d6f4522064d18d 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -8,7 +8,7 @@ import { createRequire } from 'node:module' import crypto from 'node:crypto' import colors from 'picocolors' import type { Alias, AliasOptions } from 'dep-types/alias' -import type { RollupOptions } from 'rollup' +import type { PluginContextMeta, RollupOptions } from 'rollup' import picomatch from 'picomatch' import { build } from 'esbuild' import type { AnymatchFn } from '../types/anymatch' @@ -76,6 +76,7 @@ import { nodeLikeBuiltins, normalizeAlias, normalizePath, + rollupVersion, } from './utils' import { createPluginHookUtils, @@ -103,6 +104,7 @@ import { PartialEnvironment } from './baseEnvironment' import { createIdResolver } from './idResolver' import { runnerImport } from './ssr/runnerImport' import { getAdditionalAllowedHosts } from './server/middlewares/hostCheck' +import { BasicMinimalPluginContext } from './server/pluginContainer' const debug = createDebugger('vite:config', { depth: 10 }) const promisifiedRealpath = promisify(fs.realpath) @@ -1224,6 +1226,7 @@ export async function resolveConfig( await runConfigEnvironmentHook( config.environments, userPlugins, + logger, configEnv, config.ssr?.target === 'webworker', ) @@ -1365,6 +1368,16 @@ export async function resolveConfig( const BASE_URL = resolvedBase + const resolvedConfigContext = new BasicMinimalPluginContext( + { + rollupVersion, + watchMode: + (command === 'serve' && !isPreview) || + (command === 'build' && !!resolvedBuildOptions.watch), + } satisfies PluginContextMeta, + logger, + ) + let resolved: ResolvedConfig let createUserWorkerPlugins = config.worker?.plugins @@ -1423,7 +1436,7 @@ export async function resolveConfig( await Promise.all( createPluginHookUtils(resolvedWorkerPlugins) .getSortedPluginHooks('configResolved') - .map((hook) => hook(workerResolved)), + .map((hook) => hook.call(resolvedConfigContext, workerResolved)), ) return { @@ -1583,7 +1596,7 @@ export async function resolveConfig( await Promise.all( resolved .getSortedPluginHooks('configResolved') - .map((hook) => hook(resolved)), + .map((hook) => hook.call(resolvedConfigContext, resolved)), ) optimizeDepsDisabledBackwardCompatibility(resolved, resolved.optimizeDeps) @@ -2082,10 +2095,18 @@ async function runConfigHook( ): Promise { let conf = config + const tempLogger = createLogger(config.logLevel, { + allowClearScreen: config.clearScreen, + customLogger: config.customLogger, + }) + const context = new BasicMinimalPluginContext< + Omit + >({ rollupVersion }, tempLogger) + for (const p of getSortedPluginsByHook('config', plugins)) { const hook = p.config const handler = getHookHandler(hook) - const res = await handler(conf, configEnv) + const res = await handler.call(context, conf, configEnv) if (res && res !== conf) { conf = mergeConfig(conf, res) } @@ -2097,15 +2118,20 @@ async function runConfigHook( async function runConfigEnvironmentHook( environments: Record, plugins: Plugin[], + logger: Logger, configEnv: ConfigEnv, isSsrTargetWebworkerSet: boolean, ): Promise { + const context = new BasicMinimalPluginContext< + Omit + >({ rollupVersion }, logger) + const environmentNames = Object.keys(environments) for (const p of getSortedPluginsByHook('configEnvironment', plugins)) { const hook = p.configEnvironment const handler = getHookHandler(hook) for (const name of environmentNames) { - const res = await handler(name, environments[name], { + const res = await handler.call(context, name, environments[name], { ...configEnv, isSsrTargetWebworker: isSsrTargetWebworkerSet && name === 'ssr', }) diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index b1fab8c9f598bf..f368f6622cf507 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -1,8 +1,10 @@ import type { CustomPluginOptions, LoadResult, + MinimalPluginContext, ObjectHook, PluginContext, + PluginContextMeta, ResolveIdResult, Plugin as RollupPlugin, TransformPluginContext, @@ -60,10 +62,14 @@ export interface PluginContextExtension { environment: Environment } -export interface HotUpdatePluginContext { - environment: DevEnvironment +export interface ConfigPluginContext + extends Omit { + meta: Omit } +export interface MinimalPluginContextWithoutEnvironment + extends Omit {} + // Augment Rollup types to have the PluginContextExtension declare module 'rollup' { export interface MinimalPluginContext extends PluginContextExtension {} @@ -97,7 +103,7 @@ export interface Plugin extends RollupPlugin { */ hotUpdate?: ObjectHook< ( - this: HotUpdatePluginContext, + this: MinimalPluginContext & { environment: DevEnvironment }, options: HotUpdateOptions, ) => | Array @@ -207,7 +213,7 @@ export interface Plugin extends RollupPlugin { */ config?: ObjectHook< ( - this: void, + this: ConfigPluginContext, config: UserConfig, env: ConfigEnv, ) => @@ -228,7 +234,7 @@ export interface Plugin extends RollupPlugin { */ configEnvironment?: ObjectHook< ( - this: void, + this: ConfigPluginContext, name: string, config: EnvironmentOptions, env: ConfigEnv & { @@ -248,7 +254,10 @@ export interface Plugin extends RollupPlugin { * Use this hook to read and store the final resolved vite config. */ configResolved?: ObjectHook< - (this: void, config: ResolvedConfig) => void | Promise + ( + this: MinimalPluginContextWithoutEnvironment, + config: ResolvedConfig, + ) => void | Promise > /** * Configure the vite server. The hook receives the {@link ViteDevServer} @@ -309,7 +318,7 @@ export interface Plugin extends RollupPlugin { */ handleHotUpdate?: ObjectHook< ( - this: void, + this: MinimalPluginContextWithoutEnvironment, ctx: HmrContext, ) => Array | void | Promise | void> > diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 3e9d94dca2f17e..1ca7a9e349c0b8 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -11,7 +11,7 @@ import colors from 'picocolors' import type { DefaultTreeAdapterMap, ParserError, Token } from 'parse5' import { stripLiteral } from 'strip-literal' import escapeHtml from 'escape-html' -import type { Plugin } from '../plugin' +import type { MinimalPluginContextWithoutEnvironment, Plugin } from '../plugin' import type { ViteDevServer } from '../server' import { encodeURIPath, @@ -404,7 +404,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { } // pre-transform - html = await applyHtmlTransforms(html, preHooks, { + html = await applyHtmlTransforms(html, preHooks, this, { path: publicPath, filename: id, }) @@ -985,6 +985,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { result = await applyHtmlTransforms( result, [...normalHooks, ...postHooks], + this, { path: '/' + relativeUrlPath, filename: normalizedId, @@ -1113,7 +1114,7 @@ export interface IndexHtmlTransformContext { } export type IndexHtmlTransformHook = ( - this: void, + this: MinimalPluginContextWithoutEnvironment, html: string, ctx: IndexHtmlTransformContext, ) => IndexHtmlTransformResult | void | Promise @@ -1396,10 +1397,11 @@ function headTagInsertCheck( export async function applyHtmlTransforms( html: string, hooks: IndexHtmlTransformHook[], + pluginContext: MinimalPluginContextWithoutEnvironment, ctx: IndexHtmlTransformContext, ): Promise { for (const hook of hooks) { - const res = await hook(html, ctx) + const res = await hook.call(pluginContext, html, ctx) if (!res) { continue } diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index 184d96bc53c7b4..90cc1db70f6fc3 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -28,6 +28,7 @@ import { getServerUrlByHost, resolveHostname, resolveServerUrls, + rollupVersion, setupSIGTERMListener, shouldServeFile, teardownSIGTERMListener, @@ -40,6 +41,8 @@ import type { InlineConfig, ResolvedConfig } from './config' import { DEFAULT_PREVIEW_PORT } from './constants' import type { RequiredExceptFor } from './typeUtils' import { hostCheckMiddleware } from './server/middlewares/hostCheck' +import { BasicMinimalPluginContext } from './server/pluginContainer' +import type { MinimalPluginContextWithoutEnvironment } from './plugin' export interface PreviewOptions extends CommonServerOptions {} @@ -104,7 +107,7 @@ export interface PreviewServer { } export type PreviewServerHook = ( - this: void, + this: MinimalPluginContextWithoutEnvironment, server: PreviewServer, ) => (() => void) | void | Promise<(() => void) | void> @@ -191,9 +194,16 @@ export async function preview( setupSIGTERMListener(closeServerAndExit) // apply server hooks from plugins + const configurePreviewServerContext = new BasicMinimalPluginContext( + { + rollupVersion, + watchMode: false, + }, + config.logger, + ) const postHooks: ((() => void) | void)[] = [] for (const hook of config.getSortedPluginHooks('configurePreviewServer')) { - postHooks.push(await hook(server)) + postHooks.push(await hook.call(configurePreviewServerContext, server)) } // cors diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 2c98a259f7bdde..5a2063a03dcd77 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -10,7 +10,7 @@ import type { InvokeSendData, } from '../../shared/invokeMethods' import { CLIENT_DIR } from '../constants' -import { createDebugger, normalizePath } from '../utils' +import { createDebugger, normalizePath, rollupVersion } from '../utils' import type { InferCustomEventPayload, ViteDevServer } from '..' import { getHookHandler } from '../plugins' import { isCSSRequest } from '../plugins/css' @@ -27,6 +27,10 @@ import type { EnvironmentModuleNode } from './moduleGraph' import type { ModuleNode } from './mixedModuleGraph' import type { DevEnvironment } from './environment' import { prepareError } from './middlewares/error' +import { + BasicMinimalPluginContext, + MinimalPluginContext, +} from './pluginContainer' import type { HttpServer } from '.' import { restartServerWithUrls } from '.' @@ -460,9 +464,22 @@ export async function handleHMRUpdate( modules: [...mixedMods], } + const contextForHandleHotUpdate = new BasicMinimalPluginContext( + { + rollupVersion, + watchMode: true, + }, + config.logger, + ) const clientEnvironment = server.environments.client const ssrEnvironment = server.environments.ssr - const clientContext = { environment: clientEnvironment } + const clientContext = new MinimalPluginContext( + { + rollupVersion, + watchMode: true, + }, + clientEnvironment, + ) const clientHotUpdateOptions = hotMap.get(clientEnvironment)!.options const ssrHotUpdateOptions = hotMap.get(ssrEnvironment)?.options try { @@ -506,9 +523,9 @@ export async function handleHMRUpdate( ) // later on, we'll need: if (runtime === 'client') // Backward compatibility with mixed client and ssr moduleGraph - const filteredModules = await getHookHandler(plugin.handleHotUpdate!)( - mixedHmrContext, - ) + const filteredModules = await getHookHandler( + plugin.handleHotUpdate!, + ).call(contextForHandleHotUpdate, mixedHmrContext) if (filteredModules) { mixedHmrContext.modules = filteredModules clientHotUpdateOptions.modules = @@ -553,12 +570,18 @@ export async function handleHMRUpdate( for (const environment of Object.values(server.environments)) { if (environment.name === 'client') continue const hot = hotMap.get(environment)! - const environmentThis = { environment } + const context = new MinimalPluginContext( + { + rollupVersion, + watchMode: true, + }, + environment, + ) try { for (const plugin of getSortedHotUpdatePlugins(environment)) { if (plugin.hotUpdate) { const filteredModules = await getHookHandler(plugin.hotUpdate).call( - environmentThis, + context, hot.options, ) if (filteredModules) { diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 24e9b74fe440db..17a91f6bc6def5 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -35,6 +35,7 @@ import { normalizePath, resolveHostname, resolveServerUrls, + rollupVersion, setupSIGTERMListener, teardownSIGTERMListener, } from '../utils' @@ -62,8 +63,13 @@ import { import { initPublicFiles } from '../publicDir' import { getEnvFilesForMode } from '../env' import type { RequiredExceptFor } from '../typeUtils' +import type { MinimalPluginContextWithoutEnvironment } from '../plugin' import type { PluginContainer } from './pluginContainer' -import { ERR_CLOSED_SERVER, createPluginContainer } from './pluginContainer' +import { + BasicMinimalPluginContext, + ERR_CLOSED_SERVER, + createPluginContainer, +} from './pluginContainer' import type { WebSocketServer } from './ws' import { createWebSocketServer } from './ws' import { baseMiddleware } from './middlewares/base' @@ -240,7 +246,7 @@ export interface FileSystemServeOptions { } export type ServerHook = ( - this: void, + this: MinimalPluginContextWithoutEnvironment, server: ViteDevServer, ) => (() => void) | void | Promise<(() => void) | void> @@ -846,9 +852,16 @@ export async function _createServer( } // apply server configuration hooks from plugins + const configureServerContext = new BasicMinimalPluginContext( + { + rollupVersion, + watchMode: true, + }, + config.logger, + ) const postHooks: ((() => void) | void)[] = [] for (const hook of config.getSortedPluginHooks('configureServer')) { - postHooks.push(await hook(reflexServer)) + postHooks.push(await hook.call(configureServerContext, reflexServer)) } // Internal middlewares ------------------------------------------------------ diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index ad3a51c7877d0d..c70e305ea51435 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -37,6 +37,7 @@ import { joinUrlSegments, normalizePath, processSrcSetSync, + rollupVersion, stripBase, } from '../../utils' import { checkPublicFile } from '../../publicDir' @@ -44,6 +45,7 @@ import { isCSSRequest } from '../../plugins/css' import { getCodeWithSourcemap, injectSourcesContent } from '../sourcemap' import { cleanUrl, unwrapId, wrapId } from '../../../shared/utils' import { getNodeAssetAttributes } from '../../assetSource' +import { BasicMinimalPluginContext } from '../pluginContainer' interface AssetNode { start: number @@ -80,13 +82,17 @@ export function createDevHtmlTransformFn( injectNonceAttributeTagHook(config), postImportMapHook(), ] + const pluginContext = new BasicMinimalPluginContext( + { rollupVersion, watchMode: true }, + config.logger, + ) return ( server: ViteDevServer, url: string, html: string, originalUrl?: string, ): Promise => { - return applyHtmlTransforms(html, transformHooks, { + return applyHtmlTransforms(html, transformHooks, pluginContext, { path: url, filename: getHtmlFilename(url, server), server, diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index ae8305c5a53d5e..64d3db4e29f710 100644 --- a/packages/vite/src/node/server/pluginContainer.ts +++ b/packages/vite/src/node/server/pluginContainer.ts @@ -86,6 +86,7 @@ import { import { cleanUrl, unwrapId } from '../../shared/utils' import type { PluginHookUtils } from '../config' import type { Environment } from '../environment' +import type { Logger } from '../logger' import type { DevEnvironment } from './environment' import { buildErrorMessage } from './middlewares/error' import type { @@ -557,10 +558,10 @@ class EnvironmentPluginContainer { } } -class MinimalPluginContext implements RollupMinimalPluginContext { +export class BasicMinimalPluginContext { constructor( - public meta: PluginContextMeta, - public environment: Environment, + public meta: Meta, + private _logger: Logger, ) {} debug(rawLog: string | RollupLog | (() => string | RollupLog)): void { @@ -572,7 +573,7 @@ class MinimalPluginContext implements RollupMinimalPluginContext { info(rawLog: string | RollupLog | (() => string | RollupLog)): void { const log = this._normalizeRawLog(rawLog) const msg = buildErrorMessage(log, [`info: ${log.message}`], false) - this.environment.logger.info(msg, { clear: true, timestamp: true }) + this._logger.info(msg, { clear: true, timestamp: true }) } warn(rawLog: string | RollupLog | (() => string | RollupLog)): void { @@ -582,7 +583,7 @@ class MinimalPluginContext implements RollupMinimalPluginContext { [colors.yellow(`warning: ${log.message}`)], false, ) - this.environment.logger.warn(msg, { clear: true, timestamp: true }) + this._logger.warn(msg, { clear: true, timestamp: true }) } error(e: string | RollupError): never { @@ -598,6 +599,17 @@ class MinimalPluginContext implements RollupMinimalPluginContext { } } +export class MinimalPluginContext + extends BasicMinimalPluginContext + implements RollupMinimalPluginContext +{ + public environment: T + constructor(meta: PluginContextMeta, environment: T) { + super(meta, environment.logger) + this.environment = environment + } +} + class PluginContext extends MinimalPluginContext implements Omit