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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ build/lib/policies/policyData.jsonc @joshspicer @rebornix @joaomoreno @pwang347
# Ensure the API team is aware of changes to the vscode-dts file
# this is only about the final API, not about proposed API changes
src/vscode-dts/vscode.d.ts @jrieken @mjbvz @alexr00
src/vs/workbench/services/extensions/common/extensionPoints.json @jrieken @mjbvz @alexr00
24 changes: 22 additions & 2 deletions build/azure-pipelines/product-sanity-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ parameters:
- name: buildCommit
displayName: Published Build Commit
type: string
default: ''

- name: npmRegistry
displayName: Custom NPM Registry URL
Expand All @@ -27,9 +28,17 @@ variables:
- name: Codeql.SkipTaskAutoInjection
value: true
- name: BUILD_COMMIT
value: ${{ parameters.buildCommit }}
${{ if ne(parameters.buildCommit, '') }}:
value: ${{ parameters.buildCommit }}
${{ else }}:
value: $(resources.pipeline.vscode.sourceCommit)
- name: BUILD_QUALITY
value: ${{ parameters.buildQuality }}
${{ if ne(parameters.buildCommit, '') }}:
value: ${{ parameters.buildQuality }}
${{ elseif startsWith(variables['Build.SourceBranch'], 'refs/heads/release/') }}:
value: stable
${{ else }}:
value: insider
- name: NPM_REGISTRY
value: ${{ parameters.npmRegistry }}

Expand All @@ -41,6 +50,17 @@ resources:
type: git
name: 1ESPipelineTemplates/1ESPipelineTemplates
ref: refs/tags/release
pipelines:
- pipeline: vscode
# allow-any-unicode-next-line
source: '⭐️ VS Code'
trigger:
stages:
- Publish
branches:
include:
- main
- release/*

extends:
template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines
Expand Down
2 changes: 1 addition & 1 deletion build/gulpfile.reh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ function packageTask(type: string, platform: string, arch: string, sourceFolderN

let productJsonContents = '';
const productJsonStream = gulp.src(['product.json'], { base: '.' })
.pipe(jsonEditor({ commit, date: readISODate('out-build'), version }))
.pipe(jsonEditor({ commit, date: readISODate(sourceFolderName), version }))
.pipe(es.through(function (file) {
productJsonContents = file.contents.toString();
this.emit('data', file);
Expand Down
9 changes: 6 additions & 3 deletions build/gulpfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import * as task from './lib/task.ts';
import * as util from './lib/util.ts';
import { useEsbuildTranspile } from './buildConfig.ts';

// Extension point names
gulp.task(compilation.compileExtensionPointNamesTask);

const require = createRequire(import.meta.url);

// API proposal names
Expand All @@ -30,12 +33,12 @@ const transpileClientTask = task.define('transpile-client', task.series(util.rim
gulp.task(transpileClientTask);

// Fast compile for development time
const compileClientTask = task.define('compile-client', task.series(util.rimraf('out'), compilation.copyCodiconsTask, compilation.compileApiProposalNamesTask, compilation.compileTask('src', 'out', false)));
const compileClientTask = task.define('compile-client', task.series(util.rimraf('out'), compilation.copyCodiconsTask, compilation.compileApiProposalNamesTask, compilation.compileExtensionPointNamesTask, compilation.compileTask('src', 'out', false)));
gulp.task(compileClientTask);

const watchClientTask = useEsbuildTranspile
? task.define('watch-client', task.parallel(compilation.watchTask('out', false, 'src', { noEmit: true }), compilation.watchApiProposalNamesTask, compilation.watchCodiconsTask))
: task.define('watch-client', task.series(util.rimraf('out'), task.parallel(compilation.watchTask('out', false), compilation.watchApiProposalNamesTask, compilation.watchCodiconsTask)));
? task.define('watch-client', task.parallel(compilation.watchTask('out', false, 'src', { noEmit: true }), compilation.watchApiProposalNamesTask, compilation.watchExtensionPointNamesTask, compilation.watchCodiconsTask))
: task.define('watch-client', task.series(util.rimraf('out'), task.parallel(compilation.watchTask('out', false), compilation.watchApiProposalNamesTask, compilation.watchExtensionPointNamesTask, compilation.watchCodiconsTask)));
gulp.task(watchClientTask);

// All
Expand Down
2 changes: 1 addition & 1 deletion build/gulpfile.vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d

let productJsonContents: string;
const productJsonStream = gulp.src(['product.json'], { base: '.' })
.pipe(jsonEditor({ commit, date: readISODate('out-build'), checksums, version }))
.pipe(jsonEditor({ commit, date: readISODate(out), checksums, version }))
.pipe(es.through(function (file) {
productJsonContents = file.contents.toString();
this.emit('data', file);
Expand Down
46 changes: 46 additions & 0 deletions build/lib/compilation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import * as tsb from './tsb/index.ts';
import sourcemaps from 'gulp-sourcemaps';


import { extractExtensionPointNamesFromFile } from './extractExtensionPoints.ts';


// --- gulp-tsb: compile and transpile --------------------------------

const reporter = createReporter();
Expand Down Expand Up @@ -351,6 +354,49 @@ export const compileApiProposalNamesTask = task.define('compile-api-proposal-nam
.pipe(apiProposalNamesReporter.end(true));
});

function generateExtensionPointNames() {
const collectedNames: string[] = [];

const input = es.through();
const output = input
.pipe(es.through(function (file: File) {
const contents = file.contents?.toString('utf-8');
if (contents && contents.includes('registerExtensionPoint')) {
const sourceFile = ts.createSourceFile(file.path, contents, ts.ScriptTarget.Latest, true);
collectedNames.push(...extractExtensionPointNamesFromFile(sourceFile));
}
}, function () {
collectedNames.sort();
const content = JSON.stringify(collectedNames, undefined, '\t') + '\n';
this.emit('data', new File({
path: 'vs/workbench/services/extensions/common/extensionPoints.json',
contents: Buffer.from(content)
}));
this.emit('end');
}));

return es.duplex(input, output);
}

const extensionPointNamesReporter = createReporter('extension-point-names');

export const compileExtensionPointNamesTask = task.define('compile-extension-point-names', () => {
return gulp.src('src/vs/workbench/**/*.ts')
.pipe(generateExtensionPointNames())
.pipe(gulp.dest('src'))
.pipe(extensionPointNamesReporter.end(true));
});

export const watchExtensionPointNamesTask = task.define('watch-extension-point-names', () => {
const task = () => gulp.src('src/vs/workbench/**/*.ts')
.pipe(generateExtensionPointNames())
.pipe(extensionPointNamesReporter.end(true));

return watch('src/vs/workbench/**/*.ts', { readDelay: 200 })
.pipe(util.debounce(task))
.pipe(gulp.dest('src'));
});

export const watchApiProposalNamesTask = task.define('watch-api-proposal-names', () => {
const task = () => gulp.src('src/vscode-dts/**')
.pipe(generateApiProposalNames())
Expand Down
10 changes: 9 additions & 1 deletion build/lib/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,13 @@ export function writeISODate(outDir: string) {

export function readISODate(outDir: string): string {
const outDirectory = path.join(root, outDir);
return fs.readFileSync(path.join(outDirectory, 'date'), 'utf8');
try {
return fs.readFileSync(path.join(outDirectory, 'date'), 'utf8');
} catch {
// Fallback to out-build (old build writes date there, esbuild writes to bundle output dir)
if (outDir !== 'out-build') {
return fs.readFileSync(path.join(root, 'out-build', 'date'), 'utf8');
}
throw new Error(`Could not find date file in ${outDir}`);
}
}
224 changes: 224 additions & 0 deletions build/lib/extractExtensionPoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

/**
* Extracts extension point names from TypeScript source files by parsing the AST
* to find all calls to `ExtensionsRegistry.registerExtensionPoint(...)`.
*
* Handles:
* - Inline string literals: `{ extensionPoint: 'foo' }`
* - Enum member values passed via function parameters
* - Imported descriptor variables where the `extensionPoint` property is in another file
*
* This module can be used standalone (`node build/lib/extractExtensionPoints.ts`)
* to regenerate the extension points file, or imported for use in gulp build tasks.
*/

import ts from 'typescript';
import path from 'path';
import fs from 'fs';

/**
* Extract extension point names registered via `registerExtensionPoint` from
* a single TypeScript source file's AST. No type checker is needed.
*/
export function extractExtensionPointNamesFromFile(sourceFile: ts.SourceFile): string[] {
const results: string[] = [];
visit(sourceFile);
return results;

function visit(node: ts.Node): void {
if (ts.isCallExpression(node)) {
const expr = node.expression;
if (ts.isPropertyAccessExpression(expr) && expr.name.text === 'registerExtensionPoint') {
handleRegisterCall(node);
}
}
ts.forEachChild(node, visit);
}

function handleRegisterCall(call: ts.CallExpression): void {
const arg = call.arguments[0];
if (!arg) {
return;
}
if (ts.isObjectLiteralExpression(arg)) {
handleInlineDescriptor(call, arg);
} else if (ts.isIdentifier(arg)) {
handleImportedDescriptor(arg);
}
}

function handleInlineDescriptor(call: ts.CallExpression, obj: ts.ObjectLiteralExpression): void {
const epProp = findExtensionPointProperty(obj);
if (!epProp) {
return;
}
if (ts.isStringLiteral(epProp.initializer)) {
results.push(epProp.initializer.text);
} else if (ts.isIdentifier(epProp.initializer)) {
// The value references a function parameter - resolve via call sites
handleParameterReference(call, epProp.initializer.text);
}
}

function handleParameterReference(registerCall: ts.CallExpression, paramName: string): void {
// Walk up to find the containing function
let current: ts.Node | undefined = registerCall.parent;
while (current && !ts.isFunctionDeclaration(current) && !ts.isFunctionExpression(current) && !ts.isArrowFunction(current)) {
current = current.parent;
}
if (!current) {
return;
}
const fn = current as ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction;

// Find which parameter position matches paramName
const paramIndex = fn.parameters.findIndex(
p => ts.isIdentifier(p.name) && p.name.text === paramName
);
if (paramIndex < 0) {
return;
}

// Find the function name to locate call sites
const fnName = ts.isFunctionDeclaration(fn) && fn.name ? fn.name.text : undefined;
if (!fnName) {
return;
}

// Find all call sites of this function in the same file
ts.forEachChild(sourceFile, function findCalls(node) {
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === fnName) {
const callArg = node.arguments[paramIndex];
if (callArg) {
const value = resolveStringValue(callArg);
if (value) {
results.push(value);
}
}
}
ts.forEachChild(node, findCalls);
});
}

function handleImportedDescriptor(identifier: ts.Identifier): void {
const name = identifier.text;
for (const stmt of sourceFile.statements) {
if (!ts.isImportDeclaration(stmt) || !stmt.importClause?.namedBindings) {
continue;
}
if (!ts.isNamedImports(stmt.importClause.namedBindings)) {
continue;
}
for (const element of stmt.importClause.namedBindings.elements) {
if (element.name.text !== name || !ts.isStringLiteral(stmt.moduleSpecifier)) {
continue;
}
const modulePath = stmt.moduleSpecifier.text;
const resolvedPath = path.resolve(
path.dirname(sourceFile.fileName),
modulePath.replace(/\.js$/, '.ts')
);
try {
const content = fs.readFileSync(resolvedPath, 'utf-8');
const importedFile = ts.createSourceFile(resolvedPath, content, ts.ScriptTarget.Latest, true);
const originalName = element.propertyName?.text || element.name.text;
const value = findExtensionPointInVariable(importedFile, originalName);
if (value) {
results.push(value);
}
} catch {
// Imported file not found, skip
}
return;
}
}
}

function resolveStringValue(node: ts.Node): string | undefined {
if (ts.isStringLiteral(node)) {
return node.text;
}
// Property access: Enum.Member
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) {
const enumName = node.expression.text;
const memberName = node.name.text;
for (const stmt of sourceFile.statements) {
if (ts.isEnumDeclaration(stmt) && stmt.name.text === enumName) {
for (const member of stmt.members) {
if (ts.isIdentifier(member.name) && member.name.text === memberName
&& member.initializer && ts.isStringLiteral(member.initializer)) {
return member.initializer.text;
}
}
}
}
}
return undefined;
}
}

function findExtensionPointProperty(obj: ts.ObjectLiteralExpression): ts.PropertyAssignment | undefined {
for (const prop of obj.properties) {
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'extensionPoint') {
return prop;
}
}
return undefined;
}

function findExtensionPointInVariable(sourceFile: ts.SourceFile, varName: string): string | undefined {
for (const stmt of sourceFile.statements) {
if (!ts.isVariableStatement(stmt)) {
continue;
}
for (const decl of stmt.declarationList.declarations) {
if (ts.isIdentifier(decl.name) && decl.name.text === varName
&& decl.initializer && ts.isObjectLiteralExpression(decl.initializer)) {
const epProp = findExtensionPointProperty(decl.initializer);
if (epProp && ts.isStringLiteral(epProp.initializer)) {
return epProp.initializer.text;
}
}
}
}
return undefined;
}

// --- Standalone CLI ---

const rootDir = path.resolve(import.meta.dirname, '..', '..');
const srcDir = path.join(rootDir, 'src');
const outputPath = path.join(srcDir, 'vs', 'workbench', 'services', 'extensions', 'common', 'extensionPoints.json');

function scanDirectory(dir: string): string[] {
const names: string[] = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
names.push(...scanDirectory(fullPath));
} else if (entry.name.endsWith('.ts')) {
const content = fs.readFileSync(fullPath, 'utf-8');
if (content.includes('registerExtensionPoint')) {
const sourceFile = ts.createSourceFile(fullPath, content, ts.ScriptTarget.Latest, true);
names.push(...extractExtensionPointNamesFromFile(sourceFile));
}
}
}
return names;
}

function main(): void {
const names = scanDirectory(path.join(srcDir, 'vs', 'workbench'));
names.sort();
const output = JSON.stringify(names, undefined, '\t') + '\n';
fs.writeFileSync(outputPath, output, 'utf-8');
console.log(`Wrote ${names.length} extension points to ${path.relative(rootDir, outputPath)}`);
}

if (import.meta.main) {
main();
}
Loading
Loading