diff --git a/build/gulpfile.extensions.ts b/build/gulpfile.extensions.ts index f48738be53a..cae158ea590 100644 --- a/build/gulpfile.extensions.ts +++ b/build/gulpfile.extensions.ts @@ -166,7 +166,7 @@ const tasks = compilations.map(function (tsconfigFile) { const compileTask = task.define(`compile-extension:${name}`, task.series(cleanTask, async () => { const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'], { dot: true })); const copyNonTs = util.streamToPromise(nonts.pipe(gulp.dest(out))); - const tsgo = spawnTsgo(absolutePath, { reporterId: 'extensions' }, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl)); + const tsgo = spawnTsgo(absolutePath, { taskName: 'extensions' }, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl)); await Promise.all([copyNonTs, tsgo]); })); @@ -175,7 +175,7 @@ const tasks = compilations.map(function (tsconfigFile) { const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'], { dot: true })); const watchInput = watcher(src, { ...srcOpts, ...{ readDelay: 200 } }); const watchNonTs = watchInput.pipe(filter(['**', '!**/*.ts'], { dot: true })).pipe(gulp.dest(out)); - const tsgoStream = watchInput.pipe(util.debounce(() => createTsgoStream(absolutePath, { reporterId: 'extensions' }, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl)), 200)); + const tsgoStream = watchInput.pipe(util.debounce(() => createTsgoStream(absolutePath, { taskName: 'extensions' }, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl)), 200)); const watchStream = es.merge(nonts.pipe(gulp.dest(out)), watchNonTs, tsgoStream); return watchStream; @@ -276,9 +276,9 @@ gulp.task(watchWebExtensionsTask); async function buildWebExtensions(isWatch: boolean): Promise { const extensionsPath = path.join(root, 'extensions'); - // Find all esbuild-browser.ts files + // Find all esbuild.browser.mts files const esbuildConfigLocations = await nodeUtil.promisify(glob)( - path.join(extensionsPath, '**', 'esbuild-browser.ts'), + path.join(extensionsPath, '**', 'esbuild.browser.mts'), { ignore: ['**/node_modules'] } ); @@ -293,7 +293,11 @@ async function buildWebExtensions(isWatch: boolean): Promise { // Esbuild for extensions if (esbuildConfigLocations.length > 0) { - promises.push(ext.esbuildExtensions('packaging web extension (esbuild)', isWatch, esbuildConfigLocations.map(script => ({ script })))); + promises.push( + ext.esbuildExtensions('packaging web extension (esbuild)', isWatch, esbuildConfigLocations.map(script => ({ script }))), + // Also run type check on extensions + ...esbuildConfigLocations.map(script => ext.typeCheckExtension(path.dirname(script), true)) + ); } // Run webpack for remaining extensions diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index cea54bff8b9..fac7946fc98 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -25,6 +25,7 @@ import { getProductionDependencies } from './dependencies.ts'; import { type IExtensionDefinition, getExtensionStream } from './builtInExtensions.ts'; import { getVersion } from './getVersion.ts'; import { fetchUrls, fetchGithub } from './fetch.ts'; +import { createTsgoStream, spawnTsgo } from './tsgo.ts'; import vzip from 'gulp-vinyl-zip'; import { createRequire } from 'module'; @@ -67,23 +68,27 @@ function updateExtensionPackageJSON(input: Stream, update: (data: any) => any): function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolean): Stream { const esbuildConfigFileName = forWeb - ? 'esbuild-browser.ts' - : 'esbuild.ts'; + ? 'esbuild.browser.mts' + : 'esbuild.mts'; const webpackConfigFileName = forWeb ? `extension-browser.webpack.config.js` : `extension.webpack.config.js`; const hasEsbuild = fs.existsSync(path.join(extensionPath, esbuildConfigFileName)); - const isWebPacked = fs.existsSync(path.join(extensionPath, webpackConfigFileName)); + const hasWebpack = fs.existsSync(path.join(extensionPath, webpackConfigFileName)); let input: Stream; let isBundled = false; if (hasEsbuild) { - input = fromLocalEsbuild(extensionPath, esbuildConfigFileName); + // Unlike webpack, esbuild only does bundling so we still want to run a separate type check step + input = es.merge( + fromLocalEsbuild(extensionPath, esbuildConfigFileName), + typeCheckExtensionStream(extensionPath, forWeb), + ); isBundled = true; - } else if (isWebPacked) { + } else if (hasWebpack) { input = fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle); isBundled = true; } else { @@ -105,6 +110,17 @@ function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolea return input; } +export function typeCheckExtension(extensionPath: string, forWeb: boolean): Promise { + const tsconfigFileName = forWeb ? 'tsconfig.browser.json' : 'tsconfig.json'; + const tsconfigPath = path.join(extensionPath, tsconfigFileName); + return spawnTsgo(tsconfigPath, { taskName: 'typechecking extension (tsgo)', noEmit: true }); +} + +export function typeCheckExtensionStream(extensionPath: string, forWeb: boolean): Stream { + const tsconfigFileName = forWeb ? 'tsconfig.browser.json' : 'tsconfig.json'; + const tsconfigPath = path.join(extensionPath, tsconfigFileName); + return createTsgoStream(tsconfigPath, { taskName: 'typechecking extension (tsgo)', noEmit: true }); +} function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, disableMangle: boolean): Stream { const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); @@ -267,6 +283,7 @@ function fromLocalEsbuild(extensionPath: string, esbuildConfigFileName: string): if (error) { return reject(error); } + const matches = (stderr || '').match(/\> (.+): error: (.+)?/g); fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), esbuildConfigFileName))} with ${matches ? matches.length : 0} errors.`); for (const match of matches || []) { @@ -632,17 +649,6 @@ export function translatePackageJSON(packageJSON: string, packageNLSPath: string const extensionsPath = path.join(root, 'extensions'); -// Additional projects to run esbuild on. These typically build code for webviews -const esbuildMediaScripts = [ - 'ipynb/esbuild.mjs', - 'markdown-language-features/esbuild-notebook.mjs', - 'markdown-language-features/esbuild-preview.mjs', - 'markdown-math/esbuild.mjs', - 'mermaid-chat-features/esbuild-chat-webview.mjs', - 'notebook-renderers/esbuild.mjs', - 'simple-browser/esbuild-preview.mjs', -]; - export async function webpackExtensions(taskName: string, isWatch: boolean, webpackConfigLocations: { configPath: string; outputRoot?: string }[]) { const webpack = require('webpack') as typeof import('webpack'); @@ -742,6 +748,18 @@ export async function esbuildExtensions(taskName: string, isWatch: boolean, scri await Promise.all(tasks); } + +// Additional projects to run esbuild on. These typically build code for webviews +const esbuildMediaScripts = [ + 'ipynb/esbuild.notebook.mts', + 'markdown-language-features/esbuild.notebook.mts', + 'markdown-language-features/esbuild.webview.mts', + 'markdown-math/esbuild.notebook.mts', + 'mermaid-chat-features/esbuild.webview.mts', + 'notebook-renderers/esbuild.notebook.mts', + 'simple-browser/esbuild.webview.mts', +]; + export function buildExtensionMedia(isWatch: boolean, outputRoot?: string): Promise { return esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ script: path.join(extensionsPath, p), diff --git a/build/lib/tsgo.ts b/build/lib/tsgo.ts index 3a245fe5cb6..421f4c1cc1b 100644 --- a/build/lib/tsgo.ts +++ b/build/lib/tsgo.ts @@ -3,37 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import ansiColors from 'ansi-colors'; import * as cp from 'child_process'; import es from 'event-stream'; +import fancyLog from 'fancy-log'; import * as path from 'path'; -import { createReporter } from './reporter.ts'; const root = path.dirname(path.dirname(import.meta.dirname)); const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx'; const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; -export function spawnTsgo(projectPath: string, config: { reporterId: string }, onComplete?: () => Promise | void): Promise { - const reporter = createReporter(config.reporterId); - let report: NodeJS.ReadWriteStream | undefined; - - const beginReport = (emitError: boolean) => { - if (report) { - report.end(); +export function spawnTsgo(projectPath: string, config: { taskName: string; noEmit?: boolean }, onComplete?: () => Promise | void): Promise { + function reporter(stdError: string) { + const matches = (stdError || '').match(/^error \w+: (.+)?/g); + fancyLog(`Finished ${ansiColors.green(config.taskName)} ${projectPath} with ${matches ? matches.length : 0} errors.`); + for (const match of matches || []) { + fancyLog.error(match); } - report = reporter.end(emitError); - }; + } - const endReport = () => { - if (!report) { - return; - } - report.end(); - report = undefined; - }; - - beginReport(false); - - const args = ['tsgo', '--project', projectPath, '--pretty', 'false', '--sourceMap', '--inlineSources']; + const args = ['tsgo', '--project', projectPath, '--pretty', 'false']; + if (config.noEmit) { + args.push('--noEmit'); + } else { + args.push('--sourceMap', '--inlineSources'); + } const child = cp.spawn(npx, args, { cwd: root, stdio: ['ignore', 'pipe', 'pipe'], @@ -47,23 +41,13 @@ export function spawnTsgo(projectPath: string, config: { reporterId: string }, o return; } if (/Starting compilation|File change detected/i.test(trimmed)) { - beginReport(false); return; } if (/Compilation complete/i.test(trimmed)) { - endReport(); return; } - const match = /(.*\(\d+,\d+\): )(.*: )(.*)/.exec(trimmed); - - if (match) { - const fullpath = path.isAbsolute(match[1]) ? match[1] : path.join(root, match[1]); - const message = match[3]; - reporter(fullpath + message); - } else { - reporter(trimmed); - } + reporter(trimmed); }; const handleData = (data: Buffer) => { @@ -84,7 +68,7 @@ export function spawnTsgo(projectPath: string, config: { reporterId: string }, o handleLine(buffer); buffer = ''; } - endReport(); + if (code === 0) { Promise.resolve(onComplete?.()).then(() => resolve(), reject); } else { @@ -93,15 +77,13 @@ export function spawnTsgo(projectPath: string, config: { reporterId: string }, o }); child.on('error', err => { - endReport(); reject(err); }); }); } -export function createTsgoStream(projectPath: string, config: { reporterId: string }, onComplete?: () => Promise | void): NodeJS.ReadWriteStream { +export function createTsgoStream(projectPath: string, config: { taskName: string; noEmit?: boolean }, onComplete?: () => Promise | void): NodeJS.ReadWriteStream { const stream = es.through(); - spawnTsgo(projectPath, config, onComplete).then(() => { stream.emit('end'); }).catch(() => { diff --git a/extensions/esbuild-extension-common.ts b/extensions/esbuild-extension-common.mts similarity index 100% rename from extensions/esbuild-extension-common.ts rename to extensions/esbuild-extension-common.mts diff --git a/extensions/esbuild-webview-common.mjs b/extensions/esbuild-webview-common.mts similarity index 62% rename from extensions/esbuild-webview-common.mjs rename to extensions/esbuild-webview-common.mts index 76d03abad7d..a170e5e344f 100644 --- a/extensions/esbuild-webview-common.mjs +++ b/extensions/esbuild-webview-common.mts @@ -2,27 +2,22 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check + /** - * @fileoverview Common build script for extension scripts used in in webviews. + * Common build script for extension scripts used in in webviews. */ import path from 'node:path'; import esbuild from 'esbuild'; -/** - * @typedef {Partial & { - * entryPoints: string[] | Record | { in: string, out: string }[]; - * outdir: string; - * }} BuildOptions - */ +export type BuildOptions = Partial & { + entryPoints: string[] | Record | { in: string; out: string }[]; + outdir: string; +}; /** * Build the source code once using esbuild. - * - * @param {BuildOptions} options - * @param {(outDir: string) => unknown} [didBuild] */ -async function build(options, didBuild) { +async function build(options: BuildOptions, didBuild?: (outDir: string) => unknown): Promise { await esbuild.build({ bundle: true, minify: true, @@ -38,11 +33,8 @@ async function build(options, didBuild) { /** * Build the source code once using esbuild, logging errors instead of throwing. - * - * @param {BuildOptions} options - * @param {(outDir: string) => unknown} [didBuild] */ -async function tryBuild(options, didBuild) { +async function tryBuild(options: BuildOptions, didBuild?: (outDir: string) => unknown): Promise { try { await build(options, didBuild); } catch (err) { @@ -50,17 +42,16 @@ async function tryBuild(options, didBuild) { } } -/** - * @param {{ - * srcDir: string; - * outdir: string; - * entryPoints: string[] | Record | { in: string, out: string }[]; - * additionalOptions?: Partial - * }} config - * @param {string[]} args - * @param {(outDir: string) => unknown} [didBuild] - */ -export async function run(config, args, didBuild) { +export async function run( + config: { + srcDir: string; + outdir: string; + entryPoints: BuildOptions['entryPoints']; + additionalOptions?: Partial; + }, + args: string[], + didBuild?: (outDir: string) => unknown +): Promise { let outdir = config.outdir; const outputRootIndex = args.indexOf('--outputRoot'); if (outputRootIndex >= 0) { @@ -69,8 +60,7 @@ export async function run(config, args, didBuild) { outdir = path.join(outputRoot, outputDirName); } - /** @type {BuildOptions} */ - const resolvedOptions = { + const resolvedOptions: BuildOptions = { entryPoints: config.entryPoints, outdir, logOverride: { diff --git a/extensions/ipynb/esbuild.mjs b/extensions/ipynb/esbuild.notebook.mts similarity index 90% rename from extensions/ipynb/esbuild.mjs rename to extensions/ipynb/esbuild.notebook.mts index 3003959c1eb..4d45f388574 100644 --- a/extensions/ipynb/esbuild.mjs +++ b/extensions/ipynb/esbuild.notebook.mts @@ -2,9 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check import path from 'node:path'; -import { run } from '../esbuild-webview-common.mjs'; +import { run } from '../esbuild-webview-common.mts'; const srcDir = path.join(import.meta.dirname, 'notebook-src'); const outDir = path.join(import.meta.dirname, 'notebook-out'); diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index 89a24e5cc15..7396e270a47 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -158,7 +158,7 @@ "scripts": { "compile": "npx gulp compile-extension:ipynb && npm run build-notebook", "watch": "npx gulp watch-extension:ipynb", - "build-notebook": "node ./esbuild.mjs" + "build-notebook": "node ./esbuild.notebook.mts" }, "dependencies": { "@enonic/fnv-plus": "^1.3.0", diff --git a/extensions/markdown-language-features/.vscodeignore b/extensions/markdown-language-features/.vscodeignore index a5b7a3ec72c..315b1d78770 100644 --- a/extensions/markdown-language-features/.vscodeignore +++ b/extensions/markdown-language-features/.vscodeignore @@ -2,16 +2,12 @@ test/** test-workspace/** src/** notebook/** -tsconfig.json -tsconfig.*.json +tsconfig*.json +esbuild* out/test/** out/** -extension.webpack.config.js -extension-browser.webpack.config.js cgmanifest.json package-lock.json preview-src/** -webpack.config.js -esbuild-* .gitignore **/*.d.ts diff --git a/extensions/markdown-language-features/esbuild-browser.ts b/extensions/markdown-language-features/esbuild.browser.mts similarity index 96% rename from extensions/markdown-language-features/esbuild-browser.ts rename to extensions/markdown-language-features/esbuild.browser.mts index 2c46e390c06..ddf0c5a99dc 100644 --- a/extensions/markdown-language-features/esbuild-browser.ts +++ b/extensions/markdown-language-features/esbuild.browser.mts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist', 'browser'); diff --git a/extensions/markdown-language-features/esbuild.ts b/extensions/markdown-language-features/esbuild.mts similarity index 95% rename from extensions/markdown-language-features/esbuild.ts rename to extensions/markdown-language-features/esbuild.mts index 67835c9a1d7..a1cf6eb5fa8 100644 --- a/extensions/markdown-language-features/esbuild.ts +++ b/extensions/markdown-language-features/esbuild.mts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist'); diff --git a/extensions/markdown-language-features/esbuild-notebook.mjs b/extensions/markdown-language-features/esbuild.notebook.mts similarity index 90% rename from extensions/markdown-language-features/esbuild-notebook.mjs rename to extensions/markdown-language-features/esbuild.notebook.mts index 933e77d21a5..d9d511c5e82 100644 --- a/extensions/markdown-language-features/esbuild-notebook.mjs +++ b/extensions/markdown-language-features/esbuild.notebook.mts @@ -2,9 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check import path from 'path'; -import { run } from '../esbuild-webview-common.mjs'; +import { run } from '../esbuild-webview-common.mts'; const srcDir = path.join(import.meta.dirname, 'notebook'); const outDir = path.join(import.meta.dirname, 'notebook-out'); diff --git a/extensions/markdown-language-features/esbuild-preview.mjs b/extensions/markdown-language-features/esbuild.webview.mts similarity index 90% rename from extensions/markdown-language-features/esbuild-preview.mjs rename to extensions/markdown-language-features/esbuild.webview.mts index 1d3fc48b9bc..c4141cf50a5 100644 --- a/extensions/markdown-language-features/esbuild-preview.mjs +++ b/extensions/markdown-language-features/esbuild.webview.mts @@ -2,9 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check import path from 'path'; -import { run } from '../esbuild-webview-common.mjs'; +import { run } from '../esbuild-webview-common.mts'; const srcDir = path.join(import.meta.dirname, 'preview-src'); const outDir = path.join(import.meta.dirname, 'media'); diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index edffec39d74..c9d0de68d86 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -757,14 +757,20 @@ ] }, "scripts": { - "compile": "gulp compile-extension:markdown-language-features-languageService && gulp compile-extension:markdown-language-features && npm run build-preview && npm run build-notebook", - "watch": "npm run build-preview && gulp watch-extension:markdown-language-features watch-extension:markdown-language-features-languageService", - "vscode:prepublish": "npm run build-ext && npm run build-preview", - "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:markdown-language-features ./tsconfig.json", - "build-notebook": "node ./esbuild-notebook.mjs", - "build-preview": "node ./esbuild-preview.mjs", - "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", - "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch" + "compile": "npm-run-all2 -lp build-ext build-webview build-notebook", + "watch": "npm-run-all2 -lp watch-ext watch-webview watch-notebook", + "build-ext": "gulp compile-extension:markdown-language-features", + "watch-ext": "gulp watch-extension:markdown-language-features", + "build-notebook": "node ./esbuild.notebook.mts", + "watch-notebook": "node ./esbuild.notebook.mts --watch", + "build-webview": "node ./esbuild.webview.mts", + "watch-webview": "node ./esbuild.webview.mts --watch", + "compile-web": "npm-run-all2 -lp bundle-web typecheck-web", + "bundle-web": "node ./esbuild.browser.mts", + "typecheck-web": "tsgo --project ./tsconfig.browser.json --noEmit", + "watch-web": "npm-run-all2 -lp watch-bundle-web watch-typecheck-web", + "watch-bundle-web": "node ./esbuild.browser.mts --watch", + "watch-typecheck-web": "tsgo --project ./tsconfig.browser.json --noEmit --watch" }, "dependencies": { "@vscode/extension-telemetry": "^0.9.8", diff --git a/extensions/markdown-language-features/tsconfig.browser.json b/extensions/markdown-language-features/tsconfig.browser.json index dbacbb22fdf..790349e7fec 100644 --- a/extensions/markdown-language-features/tsconfig.browser.json +++ b/extensions/markdown-language-features/tsconfig.browser.json @@ -3,5 +3,8 @@ "compilerOptions": {}, "exclude": [ "./src/test/**" + ], + "files": [ + "./src/extension.browser.ts" ] } diff --git a/extensions/markdown-math/.vscodeignore b/extensions/markdown-math/.vscodeignore index 5df4a1cb8ab..90098845502 100644 --- a/extensions/markdown-math/.vscodeignore +++ b/extensions/markdown-math/.vscodeignore @@ -1,10 +1,7 @@ src/** notebook/** -extension-browser.webpack.config.js -extension.webpack.config.js -esbuild.* +tsconfig*.json +esbuild* cgmanifest.json package-lock.json -webpack.config.js -tsconfig.json .gitignore diff --git a/extensions/media-preview/esbuild-browser.ts b/extensions/markdown-math/esbuild.browser.mts similarity index 93% rename from extensions/media-preview/esbuild-browser.ts rename to extensions/markdown-math/esbuild.browser.mts index a2659e5ff46..e3fa7792d05 100644 --- a/extensions/media-preview/esbuild-browser.ts +++ b/extensions/markdown-math/esbuild.browser.mts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist', 'browser'); diff --git a/extensions/mermaid-chat-features/esbuild.ts b/extensions/markdown-math/esbuild.mts similarity index 91% rename from extensions/mermaid-chat-features/esbuild.ts rename to extensions/markdown-math/esbuild.mts index 232f589197b..5fafb57ab75 100644 --- a/extensions/mermaid-chat-features/esbuild.ts +++ b/extensions/markdown-math/esbuild.mts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist'); diff --git a/extensions/markdown-math/esbuild.mjs b/extensions/markdown-math/esbuild.notebook.mts similarity index 89% rename from extensions/markdown-math/esbuild.mjs rename to extensions/markdown-math/esbuild.notebook.mts index 910acbb06a8..c5ac472b3bd 100644 --- a/extensions/markdown-math/esbuild.mjs +++ b/extensions/markdown-math/esbuild.notebook.mts @@ -2,18 +2,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -//@ts-check - -import path from 'path'; import fse from 'fs-extra'; -import { run } from '../esbuild-webview-common.mjs'; - -const args = process.argv.slice(2); +import path from 'path'; +import { run } from '../esbuild-webview-common.mts'; const srcDir = path.join(import.meta.dirname, 'notebook'); const outDir = path.join(import.meta.dirname, 'notebook-out'); -function postBuild(outDir) { +function postBuild(outDir: string) { fse.copySync( path.join(import.meta.dirname, 'node_modules', 'katex', 'dist', 'katex.min.css'), path.join(outDir, 'katex.min.css')); diff --git a/extensions/markdown-math/package.json b/extensions/markdown-math/package.json index 5af72e0b513..19f20fcd04a 100644 --- a/extensions/markdown-math/package.json +++ b/extensions/markdown-math/package.json @@ -108,7 +108,7 @@ "scripts": { "compile": "npm run build-notebook", "watch": "npm run build-notebook", - "build-notebook": "node ./esbuild.mjs" + "build-notebook": "node ./esbuild.notebook.mts" }, "devDependencies": { "@types/markdown-it": "^0.0.0", diff --git a/extensions/markdown-math/tsconfig.browser.json b/extensions/markdown-math/tsconfig.browser.json new file mode 100644 index 00000000000..715a07ebfb8 --- /dev/null +++ b/extensions/markdown-math/tsconfig.browser.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [], + "typeRoots": [ + "./node_modules/@types" + ] + } +} diff --git a/extensions/media-preview/.vscodeignore b/extensions/media-preview/.vscodeignore index 532c87f6f2e..8621eb9e9f4 100644 --- a/extensions/media-preview/.vscodeignore +++ b/extensions/media-preview/.vscodeignore @@ -1,10 +1,9 @@ test/** src/** -tsconfig.json +tsconfig*.json +esbuild* out/test/** out/** -extension.webpack.config.js -extension-browser.webpack.config.js cgmanifest.json package-lock.json preview-src/** diff --git a/extensions/markdown-math/esbuild-browser.ts b/extensions/media-preview/esbuild.browser.mts similarity index 93% rename from extensions/markdown-math/esbuild-browser.ts rename to extensions/media-preview/esbuild.browser.mts index a2659e5ff46..e3fa7792d05 100644 --- a/extensions/markdown-math/esbuild-browser.ts +++ b/extensions/media-preview/esbuild.browser.mts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist', 'browser'); diff --git a/extensions/markdown-math/esbuild.ts b/extensions/media-preview/esbuild.mts similarity index 91% rename from extensions/markdown-math/esbuild.ts rename to extensions/media-preview/esbuild.mts index 232f589197b..5fafb57ab75 100644 --- a/extensions/markdown-math/esbuild.ts +++ b/extensions/media-preview/esbuild.mts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist'); diff --git a/extensions/media-preview/tsconfig.browser.json b/extensions/media-preview/tsconfig.browser.json new file mode 100644 index 00000000000..3694afc77ee --- /dev/null +++ b/extensions/media-preview/tsconfig.browser.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/extensions/mermaid-chat-features/esbuild-browser.ts b/extensions/mermaid-chat-features/esbuild.browser.mts similarity index 93% rename from extensions/mermaid-chat-features/esbuild-browser.ts rename to extensions/mermaid-chat-features/esbuild.browser.mts index a2659e5ff46..e3fa7792d05 100644 --- a/extensions/mermaid-chat-features/esbuild-browser.ts +++ b/extensions/mermaid-chat-features/esbuild.browser.mts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist', 'browser'); diff --git a/extensions/media-preview/esbuild.ts b/extensions/mermaid-chat-features/esbuild.mts similarity index 91% rename from extensions/media-preview/esbuild.ts rename to extensions/mermaid-chat-features/esbuild.mts index 232f589197b..5fafb57ab75 100644 --- a/extensions/media-preview/esbuild.ts +++ b/extensions/mermaid-chat-features/esbuild.mts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist'); diff --git a/extensions/mermaid-chat-features/esbuild-chat-webview.mjs b/extensions/mermaid-chat-features/esbuild.webview.mts similarity index 92% rename from extensions/mermaid-chat-features/esbuild-chat-webview.mjs rename to extensions/mermaid-chat-features/esbuild.webview.mts index e242585b1c3..41cfa12139e 100644 --- a/extensions/mermaid-chat-features/esbuild-chat-webview.mjs +++ b/extensions/mermaid-chat-features/esbuild.webview.mts @@ -2,9 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check import path from 'path'; -import { run } from '../esbuild-webview-common.mjs'; +import { run } from '../esbuild-webview-common.mts'; const srcDir = path.join(import.meta.dirname, 'chat-webview-src'); const outDir = path.join(import.meta.dirname, 'chat-webview-out'); diff --git a/extensions/mermaid-chat-features/package.json b/extensions/mermaid-chat-features/package.json index 16b6a03ce48..64c31782461 100644 --- a/extensions/mermaid-chat-features/package.json +++ b/extensions/mermaid-chat-features/package.json @@ -117,7 +117,7 @@ "watch": "npm run build-chat-webview && gulp watch-extension:mermaid-chat-features", "vscode:prepublish": "npm run build-ext && npm run build-chat-webview", "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:mermaid-chat-features", - "build-chat-webview": "node ./esbuild-chat-webview.mjs", + "build-chat-webview": "node ./esbuild.webview.mts", "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, diff --git a/extensions/mermaid-chat-features/tsconfig.browser.json b/extensions/mermaid-chat-features/tsconfig.browser.json new file mode 100644 index 00000000000..3694afc77ee --- /dev/null +++ b/extensions/mermaid-chat-features/tsconfig.browser.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/extensions/notebook-renderers/esbuild.mjs b/extensions/notebook-renderers/esbuild.notebook.mts similarity index 90% rename from extensions/notebook-renderers/esbuild.mjs rename to extensions/notebook-renderers/esbuild.notebook.mts index 890aacd19bf..ab241d8601d 100644 --- a/extensions/notebook-renderers/esbuild.mjs +++ b/extensions/notebook-renderers/esbuild.notebook.mts @@ -2,9 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check import path from 'path'; -import { run } from '../esbuild-webview-common.mjs'; +import { run } from '../esbuild-webview-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'renderer-out'); diff --git a/extensions/notebook-renderers/package.json b/extensions/notebook-renderers/package.json index 77c042ee663..715cfc03e85 100644 --- a/extensions/notebook-renderers/package.json +++ b/extensions/notebook-renderers/package.json @@ -44,7 +44,7 @@ "scripts": { "compile": "npx gulp compile-extension:notebook-renderers && npm run build-notebook", "watch": "npx gulp compile-watch:notebook-renderers", - "build-notebook": "node ./esbuild.mjs" + "build-notebook": "node ./esbuild.notebook.mts" }, "devDependencies": { "@types/jsdom": "^21.1.0", diff --git a/extensions/simple-browser/esbuild-preview.mjs b/extensions/simple-browser/esbuild.webview.mts similarity index 92% rename from extensions/simple-browser/esbuild-preview.mjs rename to extensions/simple-browser/esbuild.webview.mts index 3ce58360a30..0f91843610b 100644 --- a/extensions/simple-browser/esbuild-preview.mjs +++ b/extensions/simple-browser/esbuild.webview.mts @@ -2,9 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check import path from 'path'; -import { run } from '../esbuild-webview-common.mjs'; +import { run } from '../esbuild-webview-common.mts'; const srcDir = path.join(import.meta.dirname, 'preview-src'); const outDir = path.join(import.meta.dirname, 'media'); diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 0d558eeebf6..d372992c897 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -67,11 +67,11 @@ ] }, "scripts": { - "compile": "gulp compile-extension:simple-browser && npm run build-preview", - "watch": "npm run build-preview && gulp watch-extension:simple-browser", - "vscode:prepublish": "npm run build-ext && npm run build-preview", + "compile": "gulp compile-extension:simple-browser && npm run build-webview", + "watch": "npm run build-webview && gulp watch-extension:simple-browser", + "vscode:prepublish": "npm run build-ext && npm run build-webview", "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:simple-browser ./tsconfig.json", - "build-preview": "node ./esbuild-preview.mjs", + "build-webview": "node ./esbuild.webview.mts", "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index ecc66db7e38..03d98f3efe7 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -1296,7 +1296,7 @@ "title": "%configuration.inlayHints%", "order": 24, "properties": { - "typescript.inlayHints.parameterNames.enabled": { + "js/ts.inlayHints.parameterNames.enabled": { "type": "string", "enum": [ "none", @@ -1310,49 +1310,11 @@ ], "default": "none", "markdownDescription": "%configuration.inlayHints.parameterNames.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", - "scope": "resource" - }, - "typescript.inlayHints.parameterTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.variableTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.variableTypes.suppressWhenTypeMatchesName": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", - "scope": "resource" - }, - "typescript.inlayHints.propertyDeclarationTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.functionLikeReturnTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.enumMemberValues.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.enumMemberValues.enabled%", - "scope": "resource" + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] }, "javascript.inlayHints.parameterNames.enabled": { "type": "string", @@ -1368,42 +1330,184 @@ ], "default": "none", "markdownDescription": "%configuration.inlayHints.parameterNames.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.parameterNames.enabled.unifiedDeprecationMessage%", "scope": "resource" }, + "typescript.inlayHints.parameterNames.enabled": { + "type": "string", + "enum": [ + "none", + "literals", + "all" + ], + "enumDescriptions": [ + "%inlayHints.parameterNames.none%", + "%inlayHints.parameterNames.literals%", + "%inlayHints.parameterNames.all%" + ], + "default": "none", + "markdownDescription": "%configuration.inlayHints.parameterNames.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.parameterNames.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, "javascript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { "type": "boolean", "default": true, "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", + "markdownDeprecationMessage": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName.unifiedDeprecationMessage%", "scope": "resource" }, + "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", + "markdownDeprecationMessage": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.inlayHints.parameterTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, "javascript.inlayHints.parameterTypes.enabled": { "type": "boolean", "default": false, "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.parameterTypes.enabled.unifiedDeprecationMessage%", "scope": "resource" }, + "typescript.inlayHints.parameterTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.parameterTypes.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.inlayHints.variableTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, "javascript.inlayHints.variableTypes.enabled": { "type": "boolean", "default": false, "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.variableTypes.enabled.unifiedDeprecationMessage%", "scope": "resource" }, + "typescript.inlayHints.variableTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.variableTypes.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.inlayHints.variableTypes.suppressWhenTypeMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, "javascript.inlayHints.variableTypes.suppressWhenTypeMatchesName": { "type": "boolean", "default": true, "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", + "markdownDeprecationMessage": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName.unifiedDeprecationMessage%", "scope": "resource" }, + "typescript.inlayHints.variableTypes.suppressWhenTypeMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", + "markdownDeprecationMessage": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.inlayHints.propertyDeclarationTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, "javascript.inlayHints.propertyDeclarationTypes.enabled": { "type": "boolean", "default": false, "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.propertyDeclarationTypes.enabled.unifiedDeprecationMessage%", "scope": "resource" }, + "typescript.inlayHints.propertyDeclarationTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.propertyDeclarationTypes.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.inlayHints.functionLikeReturnTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, "javascript.inlayHints.functionLikeReturnTypes.enabled": { "type": "boolean", "default": false, "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.functionLikeReturnTypes.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "typescript.inlayHints.functionLikeReturnTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.functionLikeReturnTypes.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.inlayHints.enumMemberValues.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.enumMemberValues.enabled%", + "scope": "language-overridable", + "tags": [ + "TypeScript" + ] + }, + "typescript.inlayHints.enumMemberValues.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.enumMemberValues.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.enumMemberValues.enabled.unifiedDeprecationMessage%", "scope": "resource" } } diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 536eab3ce03..cdbce28c5a9 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -103,38 +103,46 @@ "The text inside the ``` block is code and should not be localized." ] }, + "configuration.inlayHints.parameterNames.enabled.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.parameterNames.enabled#` instead.", "configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName": "Suppress parameter name hints on arguments whose text is identical to the parameter name.", + "configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.parameterNames.suppressWhenArgumentMatchesName#` instead.", "configuration.inlayHints.parameterTypes.enabled": { "message": "Enable/disable inlay hints for implicit parameter types:\n```typescript\n\nel.addEventListener('click', e /* :MouseEvent */ => ...)\n \n```", "comment": [ "The text inside the ``` block is code and should not be localized." ] }, + "configuration.inlayHints.parameterTypes.enabled.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.parameterTypes.enabled#` instead.", "configuration.inlayHints.variableTypes.enabled": { "message": "Enable/disable inlay hints for implicit variable types:\n```typescript\n\nconst foo /* :number */ = Date.now();\n \n```", "comment": [ "The text inside the ``` block is code and should not be localized." ] }, + "configuration.inlayHints.variableTypes.enabled.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.variableTypes.enabled#` instead.", "configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName": "Suppress type hints on variables whose name is identical to the type name.", + "configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.variableTypes.suppressWhenTypeMatchesName#` instead.", "configuration.inlayHints.propertyDeclarationTypes.enabled": { "message": "Enable/disable inlay hints for implicit types on property declarations:\n```typescript\n\nclass Foo {\n\tprop /* :number */ = Date.now();\n}\n \n```", "comment": [ "The text inside the ``` block is code and should not be localized." ] }, + "configuration.inlayHints.propertyDeclarationTypes.enabled.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.propertyDeclarationTypes.enabled#` instead.", "configuration.inlayHints.functionLikeReturnTypes.enabled": { "message": "Enable/disable inlay hints for implicit return types on function signatures:\n```typescript\n\nfunction foo() /* :number */ {\n\treturn Date.now();\n} \n \n```", "comment": [ "The text inside the ``` block is code and should not be localized." ] }, + "configuration.inlayHints.functionLikeReturnTypes.enabled.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.functionLikeReturnTypes.enabled#` instead.", "configuration.inlayHints.enumMemberValues.enabled": { "message": "Enable/disable inlay hints for member values in enum declarations:\n```typescript\n\nenum MyValue {\n\tA /* = 0 */;\n\tB /* = 1 */;\n}\n \n```", "comment": [ "The text inside the ``` block is code and should not be localized." ] }, + "configuration.inlayHints.enumMemberValues.enabled.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.enumMemberValues.enabled#` instead.", "taskDefinition.tsconfig.description": "The tsconfig file that defines the TS build.", "javascript.suggestionActions.enabled": "Enable/disable suggestion diagnostics for JavaScript files in the editor.", "typescript.suggestionActions.enabled": "Enable/disable suggestion diagnostics for TypeScript files in the editor.", diff --git a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts index 6da5bb74cd7..f6ede823fcd 100644 --- a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts @@ -10,6 +10,7 @@ import { isTypeScriptDocument } from '../configuration/languageIds'; import { API } from '../tsServer/api'; import type * as Proto from '../tsServer/protocol/protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; +import { readUnifiedConfig } from '../utils/configuration'; import { Disposable } from '../utils/dispose'; import { equals } from '../utils/objects'; import { ResourceMap } from '../utils/resourceMap'; @@ -206,7 +207,7 @@ export default class FileConfigurationManager extends Disposable { disableLineTextInReferences: true, interactiveInlayHints: true, includeCompletionsForModuleExports: config.get('suggest.autoImports'), - ...getInlayHintsPreferences(config), + ...getInlayHintsPreferences(document, isTypeScriptDocument(document) ? 'typescript' : 'javascript'), ...this.getOrganizeImportsPreferences(preferencesConfig), maximumHoverLength: this.getMaximumHoverLength(document), }; @@ -274,31 +275,32 @@ function withDefaultAsUndefined(value: T, def: O): Exclude return value === def ? undefined : value as Exclude; } -export class InlayHintSettingNames { - static readonly parameterNamesSuppressWhenArgumentMatchesName = 'inlayHints.parameterNames.suppressWhenArgumentMatchesName'; - static readonly parameterNamesEnabled = 'inlayHints.parameterTypes.enabled'; - static readonly variableTypesEnabled = 'inlayHints.variableTypes.enabled'; - static readonly variableTypesSuppressWhenTypeMatchesName = 'inlayHints.variableTypes.suppressWhenTypeMatchesName'; - static readonly propertyDeclarationTypesEnabled = 'inlayHints.propertyDeclarationTypes.enabled'; - static readonly functionLikeReturnTypesEnabled = 'inlayHints.functionLikeReturnTypes.enabled'; - static readonly enumMemberValuesEnabled = 'inlayHints.enumMemberValues.enabled'; -} +export const InlayHintSettingNames = Object.freeze({ + parameterNamesEnabled: 'inlayHints.parameterNames.enabled', + parameterNamesSuppressWhenArgumentMatchesName: 'inlayHints.parameterNames.suppressWhenArgumentMatchesName', + parameterTypesEnabled: 'inlayHints.parameterTypes.enabled', + variableTypesEnabled: 'inlayHints.variableTypes.enabled', + variableTypesSuppressWhenTypeMatchesName: 'inlayHints.variableTypes.suppressWhenTypeMatchesName', + propertyDeclarationTypesEnabled: 'inlayHints.propertyDeclarationTypes.enabled', + functionLikeReturnTypesEnabled: 'inlayHints.functionLikeReturnTypes.enabled', + enumMemberValuesEnabled: 'inlayHints.enumMemberValues.enabled', +}); -export function getInlayHintsPreferences(config: vscode.WorkspaceConfiguration) { +export function getInlayHintsPreferences(scope: vscode.ConfigurationScope, fallbackSection: string) { return { - includeInlayParameterNameHints: getInlayParameterNameHintsPreference(config), - includeInlayParameterNameHintsWhenArgumentMatchesName: !config.get(InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, true), - includeInlayFunctionParameterTypeHints: config.get(InlayHintSettingNames.parameterNamesEnabled, false), - includeInlayVariableTypeHints: config.get(InlayHintSettingNames.variableTypesEnabled, false), - includeInlayVariableTypeHintsWhenTypeMatchesName: !config.get(InlayHintSettingNames.variableTypesSuppressWhenTypeMatchesName, true), - includeInlayPropertyDeclarationTypeHints: config.get(InlayHintSettingNames.propertyDeclarationTypesEnabled, false), - includeInlayFunctionLikeReturnTypeHints: config.get(InlayHintSettingNames.functionLikeReturnTypesEnabled, false), - includeInlayEnumMemberValueHints: config.get(InlayHintSettingNames.enumMemberValuesEnabled, false), + includeInlayParameterNameHints: getInlayParameterNameHintsPreference(scope, fallbackSection), + includeInlayParameterNameHintsWhenArgumentMatchesName: !readUnifiedConfig(InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, true, { scope, fallbackSection }), + includeInlayFunctionParameterTypeHints: readUnifiedConfig(InlayHintSettingNames.parameterTypesEnabled, false, { scope, fallbackSection }), + includeInlayVariableTypeHints: readUnifiedConfig(InlayHintSettingNames.variableTypesEnabled, false, { scope, fallbackSection }), + includeInlayVariableTypeHintsWhenTypeMatchesName: !readUnifiedConfig(InlayHintSettingNames.variableTypesSuppressWhenTypeMatchesName, true, { scope, fallbackSection }), + includeInlayPropertyDeclarationTypeHints: readUnifiedConfig(InlayHintSettingNames.propertyDeclarationTypesEnabled, false, { scope, fallbackSection }), + includeInlayFunctionLikeReturnTypeHints: readUnifiedConfig(InlayHintSettingNames.functionLikeReturnTypesEnabled, false, { scope, fallbackSection }), + includeInlayEnumMemberValueHints: readUnifiedConfig(InlayHintSettingNames.enumMemberValuesEnabled, false, { scope, fallbackSection }), } as const; } -function getInlayParameterNameHintsPreference(config: vscode.WorkspaceConfiguration) { - switch (config.get('inlayHints.parameterNames.enabled')) { +function getInlayParameterNameHintsPreference(scope: vscode.ConfigurationScope, fallbackSection: string) { + switch (readUnifiedConfig(InlayHintSettingNames.parameterNamesEnabled, 'none', { scope, fallbackSection })) { case 'none': return 'none'; case 'literals': return 'literals'; case 'all': return 'all'; diff --git a/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts b/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts index 4fa38e4986b..16bf7dd62db 100644 --- a/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts +++ b/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts @@ -11,20 +11,13 @@ import { API } from '../tsServer/api'; import type * as Proto from '../tsServer/protocol/protocol'; import { Location, Position } from '../typeConverters'; import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; +import { unifiedConfigSection } from '../utils/configuration'; import { Disposable } from '../utils/dispose'; import FileConfigurationManager, { InlayHintSettingNames, getInlayHintsPreferences } from './fileConfigurationManager'; import { conditionalRegistration, requireMinVersion, requireSomeCapability } from './util/dependentRegistration'; -const inlayHintSettingNames = Object.freeze([ - InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, - InlayHintSettingNames.parameterNamesEnabled, - InlayHintSettingNames.variableTypesEnabled, - InlayHintSettingNames.variableTypesSuppressWhenTypeMatchesName, - InlayHintSettingNames.propertyDeclarationTypesEnabled, - InlayHintSettingNames.functionLikeReturnTypesEnabled, - InlayHintSettingNames.enumMemberValuesEnabled, -]); +const inlayHintSettingNames = Object.values(InlayHintSettingNames); class TypeScriptInlayHintsProvider extends Disposable implements vscode.InlayHintsProvider { @@ -44,7 +37,10 @@ class TypeScriptInlayHintsProvider extends Disposable implements vscode.InlayHin super(); this._register(vscode.workspace.onDidChangeConfiguration(e => { - if (inlayHintSettingNames.some(settingName => e.affectsConfiguration(language.id + '.' + settingName))) { + if (inlayHintSettingNames.some(settingName => + e.affectsConfiguration(unifiedConfigSection + '.' + settingName) || + e.affectsConfiguration(language.id + '.' + settingName) + )) { this._onDidChangeInlayHints.fire(); } })); @@ -131,8 +127,7 @@ function fromProtocolInlayHintKind(kind: Proto.InlayHintKind): vscode.InlayHintK } function areInlayHintsEnabledForFile(language: LanguageDescription, document: vscode.TextDocument) { - const config = vscode.workspace.getConfiguration(language.id, document); - const preferences = getInlayHintsPreferences(config); + const preferences = getInlayHintsPreferences(document, language.id); return preferences.includeInlayParameterNameHints === 'literals' || preferences.includeInlayParameterNameHints === 'all' || diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts index d59d0c04949..e37b11c22cd 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { parse as parseJSONC } from '../../../../../base/common/jsonc.js'; +import { setProperty, applyEdits } from '../../../../../base/common/jsonEdit.js'; +import { FormattingOptions } from '../../../../../base/common/jsonFormatter.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; @@ -102,7 +105,7 @@ async function addHookToFile( if (fileExists) { const existingContent = await fileService.readFile(hookFileUri); try { - hooksContent = JSON.parse(existingContent.value.toString()); + hooksContent = parseJSONC(existingContent.value.toString()); // Ensure hooks object exists if (!hooksContent.hooks) { hooksContent.hooks = {}; @@ -144,19 +147,25 @@ async function addHookToFile( // Use existing key if found, otherwise use the detected naming convention const keyToUse = existingKeyForType ?? hookTypeKeyName; - // Add the new hook entry (append if hook type already exists) + // Determine the new hook index (append if hook type already exists) const newHookEntry = buildNewHookEntry(sourceFormat); - let newHookIndex: number; - if (!hooksContent.hooks[keyToUse]) { - hooksContent.hooks[keyToUse] = [newHookEntry]; - newHookIndex = 0; - } else { - hooksContent.hooks[keyToUse].push(newHookEntry); - newHookIndex = hooksContent.hooks[keyToUse].length - 1; - } + const existingHooks = hooksContent.hooks[keyToUse]; + const newHookIndex = Array.isArray(existingHooks) ? existingHooks.length : 0; - // Write the file - const jsonContent = JSON.stringify(hooksContent, null, '\t'); + // Generate the new JSON content using setProperty to preserve comments + let jsonContent: string; + if (fileExists) { + // Use setProperty to make targeted edits that preserve comments + const originalText = (await fileService.readFile(hookFileUri)).value.toString(); + const detectedEol = originalText.includes('\r\n') ? '\r\n' : '\n'; + const formattingOptions: FormattingOptions = { tabSize: 1, insertSpaces: false, eol: detectedEol }; + const edits = setProperty(originalText, ['hooks', keyToUse, newHookIndex], newHookEntry, formattingOptions); + jsonContent = applyEdits(originalText, edits); + } else { + // New file - use JSON.stringify since there are no comments to preserve + const newContent = { hooks: { [keyToUse]: [newHookEntry] } }; + jsonContent = JSON.stringify(newContent, null, '\t'); + } // Check if the file is already open in an editor const existingEditor = editorService.editors.find(e => isEqual(e.resource, hookFileUri)); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts index c159acfa4c3..eb567363a8e 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts @@ -128,28 +128,9 @@ export function parseClaudeHooks( const commands: IHookCommand[] = []; for (const item of hookArray) { - if (!item || typeof item !== 'object') { - continue; - } - - const itemObj = item as Record; - - // Claude can have nested hooks with matchers: { matcher: "Bash", hooks: [...] } - const nestedHooks = (itemObj as { hooks?: unknown }).hooks; - if (nestedHooks !== undefined && Array.isArray(nestedHooks)) { - for (const nestedHook of nestedHooks) { - const resolved = resolveClaudeCommand(nestedHook as Record, workspaceRootUri, userHome); - if (resolved) { - commands.push(resolved); - } - } - } else { - // Direct hook command - const resolved = resolveClaudeCommand(itemObj, workspaceRootUri, userHome); - if (resolved) { - commands.push(resolved); - } - } + // Use shared helper that handles both direct commands and nested matcher structures + const extracted = extractHookCommandsFromItem(item, workspaceRootUri, userHome); + commands.push(...extracted); } if (commands.length > 0) { @@ -166,19 +147,59 @@ export function parseClaudeHooks( } /** - * Resolves a Claude hook command to our IHookCommand format. - * Claude commands can be: { type: "command", command: "..." } or { command: "..." } + * Helper to extract hook commands from an item that could be: + * 1. A direct command object: { type: 'command', command: '...' } + * 2. A nested structure with matcher (Claude style): { matcher: '...', hooks: [{ type: 'command', command: '...' }] } + * + * This allows Copilot format to handle Claude-style entries if pasted. + * Also handles Claude's leniency where 'type' field can be omitted. */ -function resolveClaudeCommand( - raw: Record, +export function extractHookCommandsFromItem( + item: unknown, workspaceRootUri: URI | undefined, userHome: string -): IHookCommand | undefined { - // Claude might not require 'type' field, so we're more lenient - const hasValidType = raw.type === undefined || raw.type === 'command'; - if (!hasValidType) { - return undefined; +): IHookCommand[] { + if (!item || typeof item !== 'object') { + return []; } - return resolveHookCommand(raw, workspaceRootUri, userHome); + const itemObj = item as Record; + const commands: IHookCommand[] = []; + + // Check for nested hooks with matcher (Claude style): { matcher: "...", hooks: [...] } + const nestedHooks = itemObj.hooks; + if (nestedHooks !== undefined && Array.isArray(nestedHooks)) { + for (const nestedHook of nestedHooks) { + if (!nestedHook || typeof nestedHook !== 'object') { + continue; + } + const normalized = normalizeForResolve(nestedHook as Record); + const resolved = resolveHookCommand(normalized, workspaceRootUri, userHome); + if (resolved) { + commands.push(resolved); + } + } + } else { + // Direct command object + const normalized = normalizeForResolve(itemObj); + const resolved = resolveHookCommand(normalized, workspaceRootUri, userHome); + if (resolved) { + commands.push(resolved); + } + } + + return commands; +} + +/** + * Normalizes a hook command object for resolving. + * Claude format allows omitting the 'type' field, treating it as 'command'. + * This ensures compatibility when Claude-style hooks are pasted into Copilot format. + */ +function normalizeForResolve(raw: Record): Record { + // If type is missing or already 'command', ensure it's set to 'command' + if (raw.type === undefined || raw.type === 'command') { + return { ...raw, type: 'command' }; + } + return raw; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts index 6bdf4afdc89..d00fd26cb1e 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts @@ -5,8 +5,8 @@ import { URI } from '../../../../../base/common/uri.js'; import { basename, dirname } from '../../../../../base/common/path.js'; -import { HookType, IHookCommand, toHookType, resolveHookCommand } from './hookSchema.js'; -import { parseClaudeHooks } from './hookClaudeCompat.js'; +import { HookType, IHookCommand, toHookType } from './hookSchema.js'; +import { parseClaudeHooks, extractHookCommandsFromItem } from './hookClaudeCompat.js'; import { resolveCopilotCliHookType } from './hookCopilotCliCompat.js'; /** @@ -97,10 +97,9 @@ export function parseCopilotHooks( const commands: IHookCommand[] = []; for (const item of hookArray) { - const resolved = resolveHookCommand(item as Record, workspaceRootUri, userHome); - if (resolved) { - commands.push(resolved); - } + // Use helper that handles both direct commands and Claude-style nested matcher structures + const extracted = extractHookCommandsFromItem(item, workspaceRootUri, userHome); + commands.push(...extracted); } if (commands.length > 0) { diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts index 6f852ed7285..76b2ac54b07 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts @@ -6,13 +6,98 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { HookType } from '../../../common/promptSyntax/hookSchema.js'; -import { parseClaudeHooks, resolveClaudeHookType, getClaudeHookTypeName } from '../../../common/promptSyntax/hookClaudeCompat.js'; +import { parseClaudeHooks, resolveClaudeHookType, getClaudeHookTypeName, extractHookCommandsFromItem } from '../../../common/promptSyntax/hookClaudeCompat.js'; import { getHookSourceFormat, HookSourceFormat, buildNewHookEntry } from '../../../common/promptSyntax/hookCompatibility.js'; import { URI } from '../../../../../../base/common/uri.js'; suite('HookClaudeCompat', () => { ensureNoDisposablesAreLeakedInTestSuite(); + suite('extractHookCommandsFromItem', () => { + const workspaceRoot = URI.file('/workspace'); + const userHome = '/home/user'; + + test('extracts direct command object', () => { + const item = { type: 'command', command: 'echo "test"' }; + + const result = extractHookCommandsFromItem(item, workspaceRoot, userHome); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].command, 'echo "test"'); + }); + + test('extracts from nested matcher structure', () => { + const item = { + matcher: 'Bash', + hooks: [ + { type: 'command', command: 'echo "nested"' } + ] + }; + + const result = extractHookCommandsFromItem(item, workspaceRoot, userHome); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].command, 'echo "nested"'); + }); + + test('extracts multiple hooks from matcher structure', () => { + const item = { + matcher: 'Write', + hooks: [ + { type: 'command', command: 'echo "first"' }, + { type: 'command', command: 'echo "second"' } + ] + }; + + const result = extractHookCommandsFromItem(item, workspaceRoot, userHome); + + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0].command, 'echo "first"'); + assert.strictEqual(result[1].command, 'echo "second"'); + }); + + test('handles command without type field (Claude format)', () => { + const item = { command: 'echo "no type"' }; + + const result = extractHookCommandsFromItem(item, workspaceRoot, userHome); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].command, 'echo "no type"'); + }); + + test('handles nested command without type field', () => { + const item = { + matcher: 'Bash', + hooks: [ + { command: 'echo "no type nested"' } + ] + }; + + const result = extractHookCommandsFromItem(item, workspaceRoot, userHome); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].command, 'echo "no type nested"'); + }); + + test('returns empty array for null item', () => { + const result = extractHookCommandsFromItem(null, workspaceRoot, userHome); + assert.strictEqual(result.length, 0); + }); + + test('returns empty array for undefined item', () => { + const result = extractHookCommandsFromItem(undefined, workspaceRoot, userHome); + assert.strictEqual(result.length, 0); + }); + + test('returns empty array for invalid type', () => { + const item = { type: 'script', command: 'echo "wrong type"' }; + + const result = extractHookCommandsFromItem(item, workspaceRoot, userHome); + + assert.strictEqual(result.length, 0); + }); + }); + suite('resolveClaudeHookType', () => { test('resolves PreToolUse', () => { assert.strictEqual(resolveClaudeHookType('PreToolUse'), HookType.PreToolUse); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts index 7d4ba6ffe51..ede5eeb5e52 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts @@ -52,6 +52,94 @@ suite('HookCompatibility', () => { assert.strictEqual(result.size, 0); }); }); + + suite('Claude-style matcher compatibility', () => { + test('parses Claude-style nested matcher structure', () => { + // When Claude format is pasted into Copilot hooks file + const json = { + hooks: { + PreToolUse: [ + { + matcher: 'Bash', + hooks: [ + { type: 'command', command: 'echo "from matcher"' } + ] + } + ] + } + }; + + const result = parseCopilotHooks(json, workspaceRoot, userHome); + + assert.strictEqual(result.size, 1); + const entry = result.get(HookType.PreToolUse)!; + assert.strictEqual(entry.hooks.length, 1); + assert.strictEqual(entry.hooks[0].command, 'echo "from matcher"'); + }); + + test('parses Claude-style nested matcher with multiple hooks', () => { + const json = { + hooks: { + PostToolUse: [ + { + matcher: 'Write', + hooks: [ + { type: 'command', command: 'echo "first"' }, + { type: 'command', command: 'echo "second"' } + ] + } + ] + } + }; + + const result = parseCopilotHooks(json, workspaceRoot, userHome); + + const entry = result.get(HookType.PostToolUse)!; + assert.strictEqual(entry.hooks.length, 2); + assert.strictEqual(entry.hooks[0].command, 'echo "first"'); + assert.strictEqual(entry.hooks[1].command, 'echo "second"'); + }); + + test('handles mixed direct and nested matcher entries', () => { + const json = { + hooks: { + PreToolUse: [ + { type: 'command', command: 'echo "direct"' }, + { + matcher: 'Bash', + hooks: [ + { type: 'command', command: 'echo "nested"' } + ] + } + ] + } + }; + + const result = parseCopilotHooks(json, workspaceRoot, userHome); + + const entry = result.get(HookType.PreToolUse)!; + assert.strictEqual(entry.hooks.length, 2); + assert.strictEqual(entry.hooks[0].command, 'echo "direct"'); + assert.strictEqual(entry.hooks[1].command, 'echo "nested"'); + }); + + test('handles Claude-style hook without type field', () => { + // Claude allows omitting the type field + const json = { + hooks: { + SessionStart: [ + { command: 'echo "no type"' } + ] + } + }; + + const result = parseCopilotHooks(json, workspaceRoot, userHome); + + const entry = result.get(HookType.SessionStart)!; + assert.strictEqual(entry.hooks.length, 1); + assert.strictEqual(entry.hooks[0].command, 'echo "no type"'); + }); + }); }); suite('parseHooksFromFile', () => {