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
2 changes: 2 additions & 0 deletions .eslint-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
**/extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts
**/extensions/terminal-suggest/third_party/**
**/extensions/typescript-language-features/test-workspace/**
**/extensions/typescript-language-features/extension.webpack.config.js
**/extensions/typescript-language-features/extension-browser.webpack.config.js
**/extensions/typescript-language-features/package-manager/node-maintainer/**
**/extensions/vscode-api-tests/testWorkspace/**
**/extensions/vscode-api-tests/testWorkspace2/**
Expand Down
12 changes: 12 additions & 0 deletions build/gulpfile.extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,13 @@ async function buildWebExtensions(isWatch: boolean): Promise<void> {
{ ignore: ['**/node_modules'] }
);

// Find all webpack configs, excluding those that will be esbuilt
const esbuildExtensionDirs = new Set(esbuildConfigLocations.map(p => path.dirname(p)));
const webpackConfigLocations = (await nodeUtil.promisify(glob)(
path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'),
{ ignore: ['**/node_modules'] }
)).filter(configPath => !esbuildExtensionDirs.has(path.dirname(configPath)));

const promises: Promise<unknown>[] = [];

// Esbuild for extensions
Expand All @@ -323,5 +330,10 @@ async function buildWebExtensions(isWatch: boolean): Promise<void> {
);
}

// Run webpack for remaining extensions
if (webpackConfigLocations.length > 0) {
promises.push(ext.webpackExtensions('packaging web extension', isWatch, webpackConfigLocations.map(configPath => ({ configPath }))));
}

await Promise.all(promises);
}
5 changes: 5 additions & 0 deletions build/gulpfile.vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,11 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d
darwinMiniAppName: embedded.nameShort,
darwinMiniAppBundleIdentifier: embedded.darwinBundleIdentifier,
darwinMiniAppIcon: 'resources/darwin/sessions.icns',
darwinMiniAppBundleURLTypes: [{
role: 'Viewer',
name: embedded.nameLong,
urlSchemes: [embedded.urlProtocol]
}],
win32ProxyAppName: embedded.nameShort,
win32ProxyIcon: 'resources/win32/sessions.ico',
} : {})
Expand Down
208 changes: 204 additions & 4 deletions build/lib/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import fancyLog from 'fancy-log';
import ansiColors from 'ansi-colors';
import buffer from 'gulp-buffer';
import * as jsoncParser from 'jsonc-parser';
import webpack from 'webpack';
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';
Expand All @@ -30,8 +32,8 @@ import { createRequire } from 'module';
const require = createRequire(import.meta.url);

const root = path.dirname(path.dirname(import.meta.dirname));
// const commit = getVersion(root);
// const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`;
const commit = getVersion(root);
const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`;

function minifyExtensionResources(input: Stream): Stream {
const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true });
Expand Down Expand Up @@ -63,24 +65,32 @@ function updateExtensionPackageJSON(input: Stream, update: (data: any) => any):
.pipe(packageJsonFilter.restore);
}

function fromLocal(extensionPath: string, forWeb: boolean, _disableMangle: boolean): Stream {
function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolean): Stream {

const esbuildConfigFileName = forWeb
? '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 hasWebpack = fs.existsSync(path.join(extensionPath, webpackConfigFileName));

let input: Stream;
let isBundled = false;

if (hasEsbuild) {
// Esbuild only does bundling so we still want to run a separate type check step
// Unlike webpack, esbuild only does bundling so we still want to run a separate type check step
input = es.merge(
fromLocalEsbuild(extensionPath, esbuildConfigFileName),
...getBuildRootsForExtension(extensionPath).map(root => typeCheckExtensionStream(root, forWeb)),
);
isBundled = true;
} else if (hasWebpack) {
input = fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle);
isBundled = true;
} else {
input = fromLocalNormal(extensionPath);
}
Expand Down Expand Up @@ -112,6 +122,132 @@ export function typeCheckExtensionStream(extensionPath: string, forWeb: boolean)
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');
const webpack = require('webpack');
const webpackGulp = require('webpack-stream');
const result = es.through();

const packagedDependencies: string[] = [];
const stripOutSourceMaps: string[] = [];
const packageJsonConfig = require(path.join(extensionPath, 'package.json'));
if (packageJsonConfig.dependencies) {
const webpackConfig = require(path.join(extensionPath, webpackConfigFileName));
const webpackRootConfig = webpackConfig.default;
for (const key in webpackRootConfig.externals) {
if (key in packageJsonConfig.dependencies) {
packagedDependencies.push(key);
}
}

if (webpackConfig.StripOutSourceMaps) {
for (const filePath of webpackConfig.StripOutSourceMaps) {
stripOutSourceMaps.push(filePath);
}
}
}

// TODO: add prune support based on packagedDependencies to vsce.PackageManager.Npm similar
// to vsce.PackageManager.Yarn.
// A static analysis showed there are no webpack externals that are dependencies of the current
// local extensions so we can use the vsce.PackageManager.None config to ignore dependencies list
// as a temporary workaround.
vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.None, packagedDependencies }).then(fileNames => {
const files = fileNames
.map(fileName => path.join(extensionPath, fileName))
.map(filePath => new File({
path: filePath,
stat: fs.statSync(filePath),
base: extensionPath,
contents: fs.createReadStream(filePath)
}));

// check for a webpack configuration files, then invoke webpack
// and merge its output with the files stream.
const webpackConfigLocations = (glob.sync(
path.join(extensionPath, '**', webpackConfigFileName),
{ ignore: ['**/node_modules'] }
) as string[]);
const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => {

const webpackDone = (err: Error | undefined, stats: any) => {
fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`);
if (err) {
result.emit('error', err);
}
const { compilation } = stats;
if (compilation.errors.length > 0) {
result.emit('error', compilation.errors.join('\n'));
}
if (compilation.warnings.length > 0) {
result.emit('error', compilation.warnings.join('\n'));
}
};

const exportedConfig = require(webpackConfigPath).default;
return (Array.isArray(exportedConfig) ? exportedConfig : [exportedConfig]).map(config => {
const webpackConfig = {
...config,
...{ mode: 'production' }
};
if (disableMangle) {
if (Array.isArray(config.module.rules)) {
for (const rule of config.module.rules) {
if (Array.isArray(rule.use)) {
for (const use of rule.use) {
if (String(use.loader).endsWith('mangle-loader.js')) {
use.options.disabled = true;
}
}
}
}
}
}
const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path);

return webpackGulp(webpackConfig, webpack, webpackDone)
.pipe(es.through(function (data) {
data.stat = data.stat || {};
data.base = extensionPath;
this.emit('data', data);
}))
.pipe(es.through(function (data: File) {
// source map handling:
// * rewrite sourceMappingURL
// * save to disk so that upload-task picks this up
if (path.extname(data.basename) === '.js') {
if (stripOutSourceMaps.indexOf(data.relative) >= 0) { // remove source map
const contents = (data.contents as Buffer).toString('utf8');
data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8');
} else {
const contents = (data.contents as Buffer).toString('utf8');
data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) {
return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`;
}), 'utf8');
}
}

this.emit('data', data);
}));
});
});

es.merge(...webpackStreams, es.readArray(files))
// .pipe(es.through(function (data) {
// // debug
// console.log('out', data.path, data.contents.length);
// this.emit('data', data);
// }))
.pipe(result);

}).catch(err => {
console.error(extensionPath);
console.error(packagedDependencies);
result.emit('error', err);
});

return result.pipe(createStatsStream(path.basename(extensionPath)));
}

function fromLocalNormal(extensionPath: string): Stream {
const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce');
Expand Down Expand Up @@ -513,6 +649,70 @@ export function translatePackageJSON(packageJSON: string, packageNLSPath: string

const extensionsPath = path.join(root, 'extensions');

export async function webpackExtensions(taskName: string, isWatch: boolean, webpackConfigLocations: { configPath: string; outputRoot?: string }[]) {
const webpack = require('webpack') as typeof import('webpack');

const webpackConfigs: webpack.Configuration[] = [];

for (const { configPath, outputRoot } of webpackConfigLocations) {
const configOrFnOrArray = require(configPath).default;
function addConfig(configOrFnOrArray: webpack.Configuration | ((env: unknown, args: unknown) => webpack.Configuration) | webpack.Configuration[]) {
for (const configOrFn of Array.isArray(configOrFnOrArray) ? configOrFnOrArray : [configOrFnOrArray]) {
const config = typeof configOrFn === 'function' ? configOrFn({}, {}) : configOrFn;
if (outputRoot) {
config.output!.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output!.path!));
}
webpackConfigs.push(config);
}
}
addConfig(configOrFnOrArray);
}

function reporter(fullStats: any) {
if (Array.isArray(fullStats.children)) {
for (const stats of fullStats.children) {
const outputPath = stats.outputPath;
if (outputPath) {
const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/');
const match = relativePath.match(/[^\/]+(\/server|\/client)?/);
fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match![0])} with ${stats.errors.length} errors.`);
}
if (Array.isArray(stats.errors)) {
stats.errors.forEach((error: any) => {
fancyLog.error(error);
});
}
if (Array.isArray(stats.warnings)) {
stats.warnings.forEach((warning: any) => {
fancyLog.warn(warning);
});
}
}
}
}
return new Promise<void>((resolve, reject) => {
if (isWatch) {
webpack(webpackConfigs).watch({}, (err, stats) => {
if (err) {
reject();
} else {
reporter(stats?.toJson());
}
});
} else {
webpack(webpackConfigs).run((err, stats) => {
if (err) {
fancyLog.error(err);
reject();
} else {
reporter(stats?.toJson());
resolve();
}
});
}
});
}

export async function esbuildExtensions(taskName: string, isWatch: boolean, scripts: { script: string; outputRoot?: string }[]): Promise<void> {
function reporter(stdError: string, script: string) {
const matches = (stdError || '').match(/\> (.+): error: (.+)?/g);
Expand Down
Loading
Loading