Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions rollup/plugins/feature-isolation-check-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { Plugin, PluginContext } from 'rollup'

interface FeatureIsolationCheckOptions {
/** Matches the modules of the optional features (e.g. chat, notebook, mcp) */
featureModules: RegExp
/** Matches the entry points that are allowed to load feature modules (their service overrides) */
allowedEntries: RegExp
/**
* Maximum size (in bytes of transformed code) of the feature modules statically reachable from
* the other entry points. It covers the known remaining dependencies, and fails the build when a
* VSCode update (or a change here) makes more of the features load unconditionally
*/
maxSize: number
}

/**
* Checks that optional features are only loaded by their own service overrides, so that a workbench
* without them doesn't load (and bundle) their implementation
*/
export default ({
featureModules,
allowedEntries,
maxSize
}: FeatureIsolationCheckOptions): Plugin => {
function getSize(context: PluginContext, id: string) {
return context.getModuleInfo(id)?.code?.length ?? 0
}

function collectStaticImports(context: PluginContext, from: string[]) {
const seen = new Set<string>(from)
const queue = [...from]
while (queue.length > 0) {
const id = queue.pop()!
for (const imported of context.getModuleInfo(id)?.importedIds ?? []) {
if (!seen.has(imported)) {
seen.add(imported)
queue.push(imported)
}
}
}
return seen
}

return {
name: 'feature-isolation-check',
buildEnd(error) {
if (error != null) {
return
}
const entries = Array.from(this.getModuleIds()).filter(
(id) => this.getModuleInfo(id)!.isEntry && !allowedEntries.test(id)
)
const reachable = collectStaticImports(this, entries)
const reachableFeatureModules = Array.from(reachable).filter((id) => featureModules.test(id))
const totalSize = reachableFeatureModules.reduce((acc, id) => acc + getSize(this, id), 0)

// Where the other modules start depending on the features, and how much each of them loads
const crossings = Array.from(reachable)
.filter((id) => !featureModules.test(id))
.flatMap((importer) =>
this.getModuleInfo(importer)!
.importedIds.filter((id) => featureModules.test(id))
.map((imported) => ({ importer, imported }))
)
.map(({ importer, imported }) => ({
importer,
imported,
size: Array.from(collectStaticImports(this, [imported]))
.filter((id) => featureModules.test(id))
.reduce((acc, id) => acc + getSize(this, id), 0)
}))
.sort((a, b) => b.size - a.size)

const kb = (size: number) => `${Math.round(size / 1024)}KB`
const shorten = (id: string) => id.replace(/^.*?\/(vs|src)\//, '$1/')
const report = [
`${reachableFeatureModules.length} feature modules (${kb(totalSize)}) are loaded without their service overrides (max ${kb(maxSize)})`,
...crossings
.slice(0, 20)
.map(
({ importer, imported, size }) =>
` ${kb(size).padStart(7)} ${shorten(importer)} -> ${shorten(imported)}`
)
].join('\n')

if (totalSize > maxSize) {
this.error(report)
}
this.info(report)
}
}
}
10 changes: 10 additions & 0 deletions rollup/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import vscodeAssetGlobMetaUrl from './plugins/vscode-asset-glob-meta-url-plugin.js'
import dynamicImportPolyfillPlugin from './plugins/dynamic-import-polyfill-plugin.js'
import resolveAssetUrlPlugin from './plugins/resolve-asset-url-plugin.js'
import featureIsolationCheckPlugin from './plugins/feature-isolation-check-plugin.js'
import { typeDedupReplaceTransformer } from './tools/typeDedup.js'
import {
DIST_DIR_MAIN,
Expand Down Expand Up @@ -191,6 +192,15 @@ export default (args: Record<string, string>): rollup.RollupOptions => {
return undefined
}
},
featureIsolationCheckPlugin({
// Service identifiers (`.service` files) are shared on purpose
featureModules: /\/vs\/workbench\/contrib\/(chat|notebook|mcp)\/(?!.*\.service\.js$)/,
// Service overrides that build on top of those features
allowedEntries:
/\/src\/service-override\/(chat|notebook|mcp|interactive|ai|speech|welcome)\.ts$/,
// ~910KB with VSCode 1.138: mostly dictation, go to symbol in chat, editor tabs and chat context keys
maxSize: 1000 * 1024
}),
configuredSubpackagePlugin(),
copy({
hook: 'writeBundle',
Expand Down
9 changes: 6 additions & 3 deletions scripts/check-unsupported-decorator.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Project } from 'ts-morph'

const project = new Project({ tsConfigFilePath: 'tsconfig.json' })
const sourceFile = project.getSourceFileOrThrow('src/missing-services.ts')
const sourceFiles = [
'src/missing-services.ts',
'src/service-override/tools/missingMainThreadCustomers.ts'
].map((path) => project.getSourceFileOrThrow(path))

const issues: string[] = []

for (const cls of sourceFile.getClasses()) {
for (const cls of sourceFiles.flatMap((sourceFile) => sourceFile.getClasses())) {
for (const prop of cls.getProperties()) {
const initText = prop.getInitializer()?.getText().trim()
const usesUnsupported = initText === 'unsupported'
Expand All @@ -26,4 +29,4 @@ if (issues.length) {
console.error(issues.join('\n'))
process.exit(1)
}
console.log('✅ All consistent')
console.log('✅ All consistent')
38 changes: 11 additions & 27 deletions src/missing-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ import {
getBuiltInExtensionTranslationsUris,
getExtensionIdProvidingCurrentLocale
} from './l10n.js'
import { unsupported } from './tools.js'
import { Unsupported, unsupported } from './tools.js'

import { NullAgentHostService } from 'vs/platform/agentHost/browser/nullAgentHostService'
import { NullSSHRemoteAgentHostService } from 'vs/platform/agentHost/browser/nullSshRemoteAgentHostService'
Expand Down Expand Up @@ -543,7 +543,7 @@ import { IChatInputNotificationService } from 'vs/workbench/contrib/chat/browser
import { IChatPhoneInputPresenter } from 'vs/workbench/contrib/chat/browser/widget/input/chatPhoneInputPresenter.service'
import { IChatImageCarouselService } from 'vs/workbench/contrib/chat/browser/chatImageCarouselService.service.js'
import { IChatDebugService } from 'vs/workbench/contrib/chat/common/chatDebugService.service.js'
import { createVSCodeHarnessDescriptor } from 'vs/workbench/contrib/chat/common/customizationHarnessService'
import type { IHarnessDescriptor } from 'vs/workbench/contrib/chat/common/customizationHarnessService'
import { ICustomizationHarnessService } from 'vs/workbench/contrib/chat/common/customizationHarnessService.service'
import type { IEnablementModel } from 'vs/workbench/contrib/chat/common/enablement.js'
import { IPluginGitService } from 'vs/workbench/contrib/chat/common/plugins/pluginGitService.service.js'
Expand Down Expand Up @@ -583,29 +583,6 @@ import { IAgentHostCustomizationService } from 'vs/workbench/contrib/chat/browse
import { IAgentHostActiveClientService } from 'vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostActiveClientService.service'
import { IGitHubService } from 'vs/platform/github/common/githubService.service.js'

function Unsupported(target: object, propertyKey: string, descriptor?: PropertyDescriptor) {
function unsupported() {
throw new Error(
`Unsupported: ${target.constructor.name}.${propertyKey} is not supported. You are using a feature without registering the corresponding service override.`
)
}
if (descriptor != null) {
if (descriptor.value != null) {
descriptor.value = unsupported
} else if (descriptor.get != null) {
descriptor.get = unsupported
}
} else {
Object.defineProperty(target, propertyKey, {
get() {
unsupported()
},
set() {},
configurable: true,
enumerable: true
})
}
}
/**
* Editor services: all editor service that are not defined in vs/editor/standalone/
*/
Expand Down Expand Up @@ -7270,8 +7247,15 @@ class CustomizationHarnessService implements ICustomizationHarnessService {
URI.parse('chat-session://local/default')
)
activeHarness: ICustomizationHarnessService['activeHarness'] = constObservable(SessionType.Local)
availableHarnesses: ICustomizationHarnessService['availableHarnesses'] = constObservable([
createVSCodeHarnessDescriptor()
// Not using `createVSCodeHarnessDescriptor` on purpose: importing it loads most of the chat implementation
availableHarnesses: ICustomizationHarnessService['availableHarnesses'] = constObservable<
IHarnessDescriptor[]
>([
{
id: SessionType.Local,
label: 'Local',
icon: Codicon.vm
}
])
onDidChangeSlashCommands: ICustomizationHarnessService['onDidChangeSlashCommands'] = Event.None
onDidChangeCustomAgents: ICustomizationHarnessService['onDidChangeCustomAgents'] = Event.None
Expand Down
1 change: 1 addition & 0 deletions src/service-override/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ import 'vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSession
import 'vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution'
import 'vs/workbench/contrib/chat/browser/attachments/chatReferenceAttachmentWidget.contribution'
import 'vs/workbench/contrib/chat/browser/attachments/transcriptContextAttachmentWidget.contribution'
import 'vs/workbench/api/browser/extensionHost.chat.contribution'

class DefaultAccountService implements IDefaultAccountService {
declare _serviceBrand: undefined
Expand Down
3 changes: 2 additions & 1 deletion src/service-override/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ import {
} from 'vs/platform/extensionManagement/common/extensionsScannerService.js'
import * as platform from 'vs/base/common/platform'
import { getBuiltInExtensionTranslationsUris } from '../l10n.js'
import 'vs/workbench/api/browser/extensionHost.contribution'
import 'vs/workbench/api/browser/extensionHost.base.contribution'
import './tools/missingMainThreadCustomers.js'

export interface WorkerConfig {
url: string
Expand Down
1 change: 1 addition & 0 deletions src/service-override/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { IWorkbenchMcpGatewayService } from 'vs/workbench/contrib/mcp/common/mcp
import { BrowserMcpGatewayService } from 'vs/workbench/contrib/mcp/browser/mcpGatewayService'
import 'vs/workbench/contrib/mcp/browser/mcp.contribution'
import 'vs/workbench/contrib/mcp/browser/mcp.view.contribution'
import 'vs/workbench/api/browser/extensionHost.mcp.contribution'

export default function getServiceOverride(): IEditorOverrideServices {
return {
Expand Down
1 change: 1 addition & 0 deletions src/service-override/notebook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { NotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/brows
import { INotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.service'
import 'vs/workbench/contrib/replNotebook/browser/repl.contribution'
import 'vs/workbench/contrib/notebook/browser/notebook.contribution'
import 'vs/workbench/api/browser/extensionHost.notebook.contribution'

export default function getServiceOverride(): IEditorOverrideServices {
return {
Expand Down
Loading
Loading