2024-09-11 02:37:36 +00:00
/ * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Copyright ( c ) Microsoft Corporation . All rights reserved .
* Licensed under the MIT License . See License . txt in the project root for license information .
* -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- * /
2025-03-01 02:01:53 +00:00
import es from 'event-stream' ;
import gulp from 'gulp' ;
import filter from 'gulp-filter' ;
import path from 'path' ;
import fs from 'fs' ;
import pump from 'pump' ;
import VinylFile from 'vinyl' ;
2024-09-11 02:37:36 +00:00
import * as bundle from './bundle' ;
import { gulpPostcss } from './postcss' ;
2025-03-01 02:01:53 +00:00
import esbuild from 'esbuild' ;
import sourcemaps from 'gulp-sourcemaps' ;
import fancyLog from 'fancy-log' ;
import ansiColors from 'ansi-colors' ;
2024-09-11 02:37:36 +00:00
const REPO_ROOT_PATH = path . join ( __dirname , '../..' ) ;
2025-03-01 02:01:53 +00:00
export interface IBundleESMTaskOpts {
2024-09-11 02:37:36 +00:00
/ * *
* The folder to read files from .
* /
src : string ;
/ * *
2025-03-01 02:01:53 +00:00
* The entry points to bundle .
2024-09-11 02:37:36 +00:00
* /
2025-03-01 02:01:53 +00:00
entryPoints : Array < bundle.IEntryPoint | string > ;
2024-09-11 02:37:36 +00:00
/ * *
2025-03-01 02:01:53 +00:00
* Other resources to consider ( svg , etc . )
2024-09-11 02:37:36 +00:00
* /
2025-03-01 02:01:53 +00:00
resources? : string [ ] ;
2024-09-11 02:37:36 +00:00
/ * *
2025-03-01 02:01:53 +00:00
* File contents interceptor for a given path .
2024-09-11 02:37:36 +00:00
* /
2025-03-01 02:01:53 +00:00
fileContentMapper ? : ( path : string ) = > ( ( contents : string ) = > Promise < string > | string ) | undefined ;
2024-09-11 02:37:36 +00:00
/ * *
2025-03-01 02:01:53 +00:00
* Allows to skip the removal of TS boilerplate . Use this when
* the entry point is small and the overhead of removing the
* boilerplate makes the file larger in the end .
2024-09-11 02:37:36 +00:00
* /
2025-03-01 02:01:53 +00:00
skipTSBoilerplateRemoval ? : ( entryPointName : string ) = > boolean ;
2024-09-11 02:37:36 +00:00
}
const DEFAULT_FILE_HEADER = [
'/*!--------------------------------------------------------' ,
' * Copyright (C) Microsoft Corporation. All rights reserved.' ,
' *--------------------------------------------------------*/'
] . join ( '\n' ) ;
2025-03-01 02:01:53 +00:00
function bundleESMTask ( opts : IBundleESMTaskOpts ) : NodeJS . ReadWriteStream {
2024-09-11 02:37:36 +00:00
const resourcesStream = es . through ( ) ; // this stream will contain the resources
2025-03-01 02:01:53 +00:00
const bundlesStream = es . through ( ) ; // this stream will contain the bundled files
2024-09-11 02:37:36 +00:00
2025-03-01 02:01:53 +00:00
const entryPoints = opts . entryPoints . map ( entryPoint = > {
if ( typeof entryPoint === 'string' ) {
return { name : path.parse ( entryPoint ) . name } ;
2024-09-11 02:37:36 +00:00
}
2025-03-01 02:01:53 +00:00
return entryPoint ;
} ) ;
2024-09-11 02:37:36 +00:00
const bundleAsync = async ( ) = > {
const files : VinylFile [ ] = [ ] ;
const tasks : Promise < any > [ ] = [ ] ;
for ( const entryPoint of entryPoints ) {
2025-03-01 02:01:53 +00:00
fancyLog ( ` Bundled entry point: ${ ansiColors . yellow ( entryPoint . name ) } ... ` ) ;
2024-09-11 02:37:36 +00:00
// support for 'dest' via esbuild#in/out
const dest = entryPoint . dest ? . replace ( /\.[^/.]+$/ , '' ) ? ? entryPoint . name ;
2025-03-01 02:01:53 +00:00
// banner contents
const banner = {
js : DEFAULT_FILE_HEADER ,
css : DEFAULT_FILE_HEADER
} ;
2024-09-11 02:37:36 +00:00
2025-03-01 02:01:53 +00:00
// TS Boilerplate
if ( ! opts . skipTSBoilerplateRemoval ? . ( entryPoint . name ) ) {
const tslibPath = path . join ( require . resolve ( 'tslib' ) , '../tslib.es6.js' ) ;
banner . js += await fs . promises . readFile ( tslibPath , 'utf-8' ) ;
}
const contentsMapper : esbuild.Plugin = {
name : 'contents-mapper' ,
2024-09-11 02:37:36 +00:00
setup ( build ) {
2025-03-01 02:01:53 +00:00
build . onLoad ( { filter : /\.js$/ } , async ( { path } ) = > {
const contents = await fs . promises . readFile ( path , 'utf-8' ) ;
// TS Boilerplate
let newContents : string ;
if ( ! opts . skipTSBoilerplateRemoval ? . ( entryPoint . name ) ) {
newContents = bundle . removeAllTSBoilerplate ( contents ) ;
} else {
newContents = contents ;
}
// File Content Mapper
const mapper = opts . fileContentMapper ? . ( path . replace ( /\\/g , '/' ) ) ;
if ( mapper ) {
newContents = await mapper ( newContents ) ;
}
2024-09-11 02:37:36 +00:00
return { contents : newContents } ;
} ) ;
}
} ;
2025-03-01 02:01:53 +00:00
const externalOverride : esbuild.Plugin = {
name : 'external-override' ,
setup ( build ) {
// We inline selected modules that are we depend on on startup without
// a conditional `await import(...)` by hooking into the resolution.
build . onResolve ( { filter : /^minimist$/ } , ( ) = > {
return { path : path.join ( REPO_ROOT_PATH , 'node_modules' , 'minimist' , 'index.js' ) , external : false } ;
} ) ;
} ,
} ;
2024-09-11 02:37:36 +00:00
const task = esbuild . build ( {
bundle : true ,
packages : 'external' , // "external all the things", see https://esbuild.github.io/api/#packages
platform : 'neutral' , // makes esm
format : 'esm' ,
2024-09-24 04:46:08 +00:00
sourcemap : 'external' ,
2025-03-01 02:01:53 +00:00
plugins : [ contentsMapper , externalOverride ] ,
2024-09-11 02:37:36 +00:00
target : [ 'es2022' ] ,
loader : {
'.ttf' : 'file' ,
'.svg' : 'file' ,
'.png' : 'file' ,
'.sh' : 'file' ,
} ,
assetNames : 'media/[name]' , // moves media assets into a sub-folder "media"
2024-09-24 04:46:08 +00:00
banner : entryPoint.name === 'vs/workbench/workbench.web.main' ? undefined : banner , // TODO@esm remove line when we stop supporting web-amd-esm-bridge
2024-09-11 02:37:36 +00:00
entryPoints : [
{
in : path . join ( REPO_ROOT_PATH , opts . src , ` ${ entryPoint . name } .js ` ) ,
out : dest ,
}
] ,
outdir : path.join ( REPO_ROOT_PATH , opts . src ) ,
write : false , // enables res.outputFiles
metafile : true , // enables res.metafile
2025-03-01 02:01:53 +00:00
// minify: NOT enabled because we have a separate minify task that takes care of the TSLib banner as well
2024-09-11 02:37:36 +00:00
} ) . then ( res = > {
for ( const file of res . outputFiles ) {
2024-09-24 04:46:08 +00:00
let sourceMapFile : esbuild.OutputFile | undefined = undefined ;
2024-09-11 02:37:36 +00:00
if ( file . path . endsWith ( '.js' ) ) {
2024-09-24 04:46:08 +00:00
sourceMapFile = res . outputFiles . find ( f = > f . path === ` ${ file . path } .map ` ) ;
2024-09-11 02:37:36 +00:00
}
2024-09-24 04:46:08 +00:00
const fileProps = {
2025-03-01 02:01:53 +00:00
contents : Buffer.from ( file . contents ) ,
2024-09-24 04:46:08 +00:00
sourceMap : sourceMapFile ? JSON . parse ( sourceMapFile . text ) : undefined , // support gulp-sourcemaps
2024-09-11 02:37:36 +00:00
path : file.path ,
base : path.join ( REPO_ROOT_PATH , opts . src )
2024-09-24 04:46:08 +00:00
} ;
files . push ( new VinylFile ( fileProps ) ) ;
2024-09-11 02:37:36 +00:00
}
} ) ;
tasks . push ( task ) ;
}
await Promise . all ( tasks ) ;
return { files } ;
} ;
bundleAsync ( ) . then ( ( output ) = > {
// bundle output (JS, CSS, SVG...)
es . readArray ( output . files ) . pipe ( bundlesStream ) ;
// forward all resources
2025-03-01 02:01:53 +00:00
gulp . src ( opts . resources ? ? [ ] , { base : ` ${ opts . src } ` , allowEmpty : true } ) . pipe ( resourcesStream ) ;
2024-09-11 02:37:36 +00:00
} ) ;
const result = es . merge (
bundlesStream ,
resourcesStream
) ;
return result
. pipe ( sourcemaps . write ( './' , {
sourceRoot : undefined ,
addComment : true ,
includeContent : true
} ) ) ;
}
2025-03-01 02:01:53 +00:00
export interface IBundleESMTaskOpts {
2024-09-11 02:37:36 +00:00
/ * *
2025-03-01 02:01:53 +00:00
* Destination folder for the bundled files .
2024-09-11 02:37:36 +00:00
* /
out : string ;
/ * *
2025-03-01 02:01:53 +00:00
* Bundle ESM modules ( using esbuild ) .
* /
esm : IBundleESMTaskOpts ;
2024-09-11 02:37:36 +00:00
}
2025-03-01 02:01:53 +00:00
export function bundleTask ( opts : IBundleESMTaskOpts ) : ( ) = > NodeJS . ReadWriteStream {
2024-09-11 02:37:36 +00:00
return function ( ) {
2025-03-01 02:01:53 +00:00
return bundleESMTask ( opts . esm ) . pipe ( gulp . dest ( opts . out ) ) ;
2024-09-11 02:37:36 +00:00
} ;
}
export function minifyTask ( src : string , sourceMapBaseUrl? : string ) : ( cb : any ) = > void {
const sourceMappingURL = sourceMapBaseUrl ? ( ( f : any ) = > ` ${ sourceMapBaseUrl } / ${ f . relative } .map ` ) : undefined ;
return cb = > {
const cssnano = require ( 'cssnano' ) as typeof import ( 'cssnano' ) ;
const svgmin = require ( 'gulp-svgmin' ) as typeof import ( 'gulp-svgmin' ) ;
const jsFilter = filter ( '**/*.js' , { restore : true } ) ;
const cssFilter = filter ( '**/*.css' , { restore : true } ) ;
const svgFilter = filter ( '**/*.svg' , { restore : true } ) ;
pump (
gulp . src ( [ src + '/**' , '!' + src + '/**/*.map' ] ) ,
jsFilter ,
sourcemaps . init ( { loadMaps : true } ) ,
es . map ( ( f : any , cb ) = > {
esbuild . build ( {
entryPoints : [ f . path ] ,
minify : true ,
sourcemap : 'external' ,
outdir : '.' ,
2025-03-01 02:01:53 +00:00
packages : 'external' , // "external all the things", see https://esbuild.github.io/api/#packages
platform : 'neutral' , // makes esm
2024-09-11 02:37:36 +00:00
target : [ 'es2022' ] ,
write : false
} ) . then ( res = > {
const jsFile = res . outputFiles . find ( f = > /\.js$/ . test ( f . path ) ) ! ;
const sourceMapFile = res . outputFiles . find ( f = > /\.js\.map$/ . test ( f . path ) ) ! ;
const contents = Buffer . from ( jsFile . contents ) ;
const unicodeMatch = contents . toString ( ) . match ( /[^\x00-\xFF]+/g ) ;
if ( unicodeMatch ) {
cb ( new Error ( ` Found non-ascii character ${ unicodeMatch [ 0 ] } in the minified output of ${ f . path } . Non-ASCII characters in the output can cause performance problems when loading. Please review if you have introduced a regular expression that esbuild is not automatically converting and convert it to using unicode escape sequences. ` ) ) ;
} else {
f . contents = contents ;
f . sourceMap = JSON . parse ( sourceMapFile . text ) ;
cb ( undefined , f ) ;
}
} , cb ) ;
} ) ,
jsFilter . restore ,
cssFilter ,
gulpPostcss ( [ cssnano ( { preset : 'default' } ) ] ) ,
cssFilter . restore ,
svgFilter ,
svgmin ( ) ,
svgFilter . restore ,
sourcemaps . write ( './' , {
sourceMappingURL ,
sourceRoot : undefined ,
includeContent : true ,
addComment : true
} as any ) ,
gulp . dest ( src + '-min' ) ,
( err : any ) = > cb ( err ) ) ;
} ;
}