use esbuild for client/web builds (#57365)

Use [esbuild](https://esbuild.github.io/) instead of Webpack for builds of `client/web`, for faster builds (dev and prod) and greater dev-prod parity. This PR completely removes all use of Webpack in this repository.

`client/web` is the last build target that still uses Webpack; all others have been recently migrated to esbuild. Most devs here have been using esbuild for local dev of `client/web` for the last 6-12 months anyway. The change here is that now our production builds will be built by esbuild.

All sg commands, integration/e2e tests, etc., continue to work as-is. The bundlesize report will take a while to stabilize because the new build products use different filenames.

## Benchmarks

Running `pnpm run generate && time pnpm -C client/web run task:gulp webBuild` and taking the `time` output from the last command:

- Webpack: 62.5s
- esbuild: 6.7s

Note: This understates esbuild's victory for 2 reasons: (1) because esbuild is building both the main and embed entrypoints, whereas Webpack only builds the main entrypoint in this benchmark) and (2) because a lot of it is in the fixed startup time of `gulp`; esbuild incremental rebuilds during local dev only take ~1s.

## Notes

We no longer use Babel to produce web builds (we use esbuild), so we don't need any Babel plugins that optimize the output or improve browser compatibility. Right now, Babel is only used by Jest (for tests) and by Bazel as an intermediate step.
This commit is contained in:
Quinn Slack 2023-10-23 10:59:06 -07:00 committed by GitHub
parent 7d4ba7f4c9
commit de613e92b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
200 changed files with 710 additions and 3921 deletions

View File

@ -1,19 +0,0 @@
# This file is used in our production build to adjust CSS and JS output to support specified browsers. For additional information regarding the format and rule options, please see: https://github.com/browserslist/browserslist#queries
# Run `pnpm browserslist` to see which browsers are targeted by these queries
### Primary browsers ###
# Target browsers with the highest usage.
> 1%
### Fallback browsers ###
# Target the latest versions of browsers with moderate usage.
# Note: This is primarily to catch any new browser versions that haven't yet reached 1% usage.
last 1 versions AND not dead AND not <0.25%
### Additional configuration ###
# Any browser-specific configuration that we need.
# Note: We include additional Safari versions due to the slower upgrade cycle on MacOS.
last 1 Chrome versions
last 2 Safari major versions
not IE > 0

View File

@ -46,21 +46,6 @@ pnpm-debug.log
/cmd/frontend/internal/graphqlbackend/node_modules
/ui/artifacts
/ui/node_modules
/ui/assets/_goTemplates.js
/ui/assets/analytics.js
/ui/assets/bundle.js
/ui/assets/bundle.css
/ui/assets/sourcebox.js
/ui/assets/sourcebox.css
/ui/assets/test.js
/ui/assets/test.css
/ui/assets/vs
/ui/assets/*.html
/ui/assets/*.map
/ui/assets/extension
/ui/.tmp
*.json.actual
eb-bundle.zip

View File

@ -99,6 +99,8 @@ See https://handbook.sourcegraph.com/community/faq#is-all-of-sourcegraph-open-so
'!@sourcegraph/branded/src/search-ui/experimental',
'!@sourcegraph/*/src/testing',
'!@sourcegraph/*/src/stories',
'!@sourcegraph/build-config/src/esbuild/*',
'!@sourcegraph/build-config/src/*',
],
message:
'Imports from package internals are banned. Add relevant export to the entry point of the package to import it from the outside world.',

25
.gitignore vendored
View File

@ -55,29 +55,6 @@ pnpm-debug.log
/cmd/frontend/internal/graphqlbackend/node_modules
/ui/artifacts
/ui/node_modules
/ui/assets/_goTemplates.js
/ui/assets/analytics.js
/ui/assets/bundle.js
/ui/assets/bundle.css
/ui/assets/sourcebox.js
/ui/assets/sourcebox.css
/ui/assets/test.js
/ui/assets/test.css
/ui/assets/web.manifest.json
/ui/assets/vs
/ui/assets/*.html
/ui/assets/*.map
/ui/assets/*.txt
/ui/assets/extension
/ui/.tmp
/ui/assets/scripts/
/ui/assets/styles/
/ui/assets/*.br
/ui/assets/*.gz
/ui/assets/stats-*
*.json.actual
eb-bundle.zip
@ -116,7 +93,7 @@ out/
client/shared/src/schema/*.d.ts
puppeteer/
package-lock.json
/dist
dist/
sourcegraph-webapp-*.tgz
*.tsbuildinfo
graphql-operations.ts

View File

@ -11,7 +11,6 @@ internal/database/migration/shared/data/**/*.json
cmd/xlang-python/python-langserver/
package-lock.json
package.json
ui/assets/
src-tauri/assets/
src-tauri/target/
vendor/

13
.vscode/launch.json vendored
View File

@ -96,19 +96,6 @@
],
"smartStep": false
},
{
"type": "node",
"request": "launch",
"name": "Gulp",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/node_modules/gulp/bin/gulp.js",
"args": ["webpack"],
"env": {
// prettier-ignore
"TS_NODE_COMPILER_OPTIONS": "{\"module\":\"commonjs\"}"
},
"internalConsoleOptions": "openOnSessionStart"
},
{
"name": "Webapp Chrome",
"type": "chrome",

View File

@ -75,12 +75,6 @@ js_library(
],
)
copy_to_bin(
name = "browserslist",
srcs = [".browserslistrc"],
visibility = ["//visibility:public"],
)
copy_to_bin(
name = "package_json",
srcs = ["package.json"],
@ -134,14 +128,10 @@ js_library(
],
visibility = ["//visibility:public"],
deps = [
"//:node_modules/@babel/plugin-transform-runtime",
"//:node_modules/@babel/plugin-transform-typescript",
"//:node_modules/@babel/preset-env",
"//:node_modules/@babel/preset-react",
"//:node_modules/@babel/preset-typescript",
"//:node_modules/@babel/runtime",
"//:node_modules/babel-plugin-lodash",
"//:node_modules/babel-plugin-webpack-chunkname",
"//:node_modules/semver",
"//:node_modules/signale",
],

View File

@ -226,27 +226,6 @@ esbuild_register_toolchains(
esbuild_version = LATEST_VERSION,
)
# rules_webpack setup ===========================
# Commit to include unreleased https://github.com/aspect-build/rules_webpack/commit/4a5f04a4bc504f71d32825124c7872ff721aa1b0
http_archive(
name = "aspect_rules_webpack",
sha256 = "8d81f8d018127c72270ea4b7287be5c4ff63d9656a34334c305d52f14e0c922f",
strip_prefix = "rules_webpack-4a5f04a4bc504f71d32825124c7872ff721aa1b0",
url = "https://github.com/aspect-build/rules_webpack/archive/4a5f04a4bc504f71d32825124c7872ff721aa1b0.tar.gz",
)
load("@aspect_rules_webpack//webpack:dependencies.bzl", "rules_webpack_dependencies")
rules_webpack_dependencies()
load("@aspect_rules_webpack//webpack:repositories.bzl", "webpack_repositories")
webpack_repositories(name = "webpack")
load("@webpack//:npm_repositories.bzl", webpack_npm_repositories = "npm_repositories")
webpack_npm_repositories()
# Go toolchain setup
load("@rules_proto_grpc//:repositories.bzl", "rules_proto_grpc_repos", "rules_proto_grpc_toolchains")

View File

@ -1,7 +1,6 @@
// @ts-check
const path = require('path')
const semver = require('semver')
const logger = require('signale')
/** @type {import('@babel/core').ConfigFunction} */
@ -36,25 +35,10 @@ module.exports = api => {
{
// Node (used for testing) doesn't support modules, so compile to CommonJS for testing.
modules: process.env.BABEL_MODULE ?? (isTest ? 'commonjs' : false),
bugfixes: true,
useBuiltIns: 'entry',
include: [
// Polyfill URL because Chrome and Firefox are not spec-compliant
// Hostnames of URIs with custom schemes (e.g. git) are not parsed out
'web.url',
// URLSearchParams.prototype.keys() is not iterable in Firefox
'web.url-search-params',
// Commonly needed by extensions (used by vscode-jsonrpc)
'web.immediate',
// Always define Symbol.observable before libraries are loaded, ensuring interopability between different libraries.
'esnext.symbol.observable',
],
// See https://github.com/zloirock/core-js#babelpreset-env
corejs: semver.minVersion(require('./package.json').dependencies['core-js']),
},
],
]),
'@babel/preset-typescript',
['@babel/preset-typescript', { isTSX: true, allExtensions: true }],
[
'@babel/preset-react',
{
@ -62,27 +46,5 @@ module.exports = api => {
},
],
],
plugins: [
'@babel/plugin-transform-runtime',
['@babel/plugin-transform-typescript', { isTSX: true }],
'babel-plugin-lodash',
[
'webpack-chunkname',
{
/**
* Autogenerate `webpackChunkName` for dynamic imports.
*
* import('./pages/Home') -> import(/* webpackChunkName: 'sg_pages_Home' *\/'./pages/Home')
*/
getChunkName: (/** @type string */ importPath) => {
const chunkName = importPath
.replace(/[./]+/g, '_') // replace "." and "/" with "_".
.replace(/(^_+)/g, '') // remove all leading "_".
return `sg_${chunkName}`
},
},
],
],
}
}

View File

@ -27,5 +27,3 @@
5. **shared** contains utility functions, types, polyfills, etc which is not a part of the Wildcard component library. These modules should be moved into **utils** package and other new packages: e.g. **api** for GraphQL client and type generators, etc.
6. Packages should use package name (e.g. `@sourcegraph/wildcard`) for imports instead of the relative paths (e.g. `../../../../wildcard/src/components/Markdown`) to avoid long relative-paths and make dependency graph between packages clear. (Typescript will warn if packages have circular dependencies). It's easy to refactor such isolated packages, extract functionality into new ones, or even into new repositories.
7. **build** or **config** package should be added later to encapsulate all the configurations reused between packages which will allow removing `jest.config`, `babel.config` from the root of the repo.

View File

@ -1,4 +1,4 @@
import { listen, Event } from '@tauri-apps/api/event'
import { listen, type Event } from '@tauri-apps/api/event'
import { invoke } from '@tauri-apps/api/tauri'
import { logger } from '@sourcegraph/common'
@ -8,7 +8,7 @@ import { logger } from '@sourcegraph/common'
// * app-shell.tsx: before the Go backend has started, this is served. If the Go backend crashes,
// then the Tauri Rust application can bring the user back here to present debugging/error handling
// options.
// * app-main.tsx: served by the Go backend, renders the Sourcegraph web UI that you see everywhere else.
// * app/main.tsx: served by the Go backend, renders the Sourcegraph web UI that you see everywhere else.
function addRedirectParamToSignInUrl(url: string, returnTo: string): string {
const urlObject = new URL(url)

View File

@ -1,8 +0,0 @@
// @ts-check
/** @type {import('@babel/core').TransformOptions} */
const config = {
extends: '../../babel.config.js',
}
module.exports = config

View File

@ -1,6 +0,0 @@
# This file is used in our production build to adjust CSS and JS output to support specified browsers. For additional information regarding the format and rule options, please see: https://github.com/browserslist/browserslist#queries
# Run `pnpm --filter @sourcegraph/browser browserslist` to see which browsers are targeted by these queries
last 3 Chrome versions
last 3 Firefox versions

View File

@ -359,8 +359,6 @@ esbuild(
":package_styles",
# BUNDLE CONFIG
"//:babel_config",
"//:browserslist",
"//:package_json",
# STATIC ASSETS

View File

@ -1,8 +0,0 @@
// @ts-check
/** @type {import('@babel/core').TransformOptions} */
const config = {
extends: '../../babel.config.js',
}
module.exports = config

View File

@ -2,7 +2,8 @@ import path from 'path'
import type * as esbuild from 'esbuild'
import { ROOT_PATH, stylePlugin } from '@sourcegraph/build-config'
import { ROOT_PATH } from '@sourcegraph/build-config'
import { stylePlugin } from '@sourcegraph/build-config/src/esbuild/plugins'
import { generateBundleUID } from './utils'

View File

@ -127,10 +127,10 @@ export function copyIntegrationAssets(): void {
shelljs.cp('build/dist/css/contentPage.main.bundle.css', 'build/integration/css')
shelljs.cp('src/native-integration/extensionHostFrame.html', 'build/integration')
copyInlineExtensions('build/integration')
// Copy to the ui/assets directory so that these files can be served by
// Copy to the dist directory so that these files can be served by
// the webapp.
shelljs.mkdir('-p', '../../ui/assets/extension')
shelljs.cp('-r', 'build/integration/*', '../../ui/assets/extension')
shelljs.mkdir('-p', '../../client/web/dist/extension')
shelljs.cp('-r', 'build/integration/*', '../../client/web/dist/extension')
}
const BROWSER_TITLES = {

View File

@ -28,21 +28,15 @@ ts_project(
"src/esbuild/stylePlugin.ts",
"src/esbuild/workerPlugin.ts",
"src/index.ts",
"src/monaco-editor.ts",
"src/paths.ts",
"src/utils/environment-config.ts",
"src/webpack/babel-loader.ts",
"src/webpack/css-loader.ts",
"src/webpack/getCacheConfig.ts",
"src/webpack/monaco-editor.ts",
"src/webpack/plugins.ts",
],
module = "commonjs",
tsconfig = ":tsconfig",
deps = [
":node_modules/@statoscope/webpack-plugin",
":node_modules/@types/sass",
":node_modules/buffer", #keep
":node_modules/css-loader", #keep
":node_modules/enhanced-resolve",
":node_modules/esbuild",
":node_modules/monaco-editor-webpack-plugin",
@ -50,9 +44,6 @@ ts_project(
":node_modules/postcss-modules",
":node_modules/process", #keep
":node_modules/sass",
":node_modules/style-loader", #keep
":node_modules/terser-webpack-plugin",
":node_modules/webpack",
# TODO: figure out why is this needed for integration tests even though it's defined in babel_config
"//:node_modules/@babel/runtime", #keep

View File

@ -1,5 +0,0 @@
// @ts-check
module.exports = {
extends: '../../babel.config.js',
}

View File

@ -13,14 +13,7 @@
"devDependencies": {
"@types/sass": "1.16.1"
},
"peerDependencies": {
"css-loader": "*",
"postcss-loader": "*",
"sass-loader": "*",
"style-loader": "*"
},
"dependencies": {
"@statoscope/webpack-plugin": "^5.24.0",
"buffer": "^6.0.3",
"enhanced-resolve": "^5.9.3",
"esbuild": "^0.17.7",
@ -31,8 +24,11 @@
"process": "^0.11.10",
"sass": "^1.32.4",
"signale": "^1.4.0",
"terser-webpack-plugin": "^5.3.6",
"typed-scss-modules": "^4.1.1",
"webpack": "^5.75.0"
"typed-scss-modules": "^4.1.1"
},
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": ["webpack"]
}
}
}
}

View File

@ -6,8 +6,8 @@ import { type EditorFeature, featuresArr } from 'monaco-editor-webpack-plugin/ou
// eslint-disable-next-line no-restricted-imports
import { type EditorLanguage, languagesArr } from 'monaco-editor-webpack-plugin/out/languages'
import type { MONACO_LANGUAGES_AND_FEATURES } from '../monaco-editor'
import { ROOT_PATH } from '../paths'
import type { MONACO_LANGUAGES_AND_FEATURES } from '../webpack/monaco-editor'
const monacoModulePath = (modulePath: string): string =>
require.resolve(path.join('monaco-editor/esm', modulePath), {
@ -54,9 +54,7 @@ export const monacoPlugin = ({
// https://github.com/microsoft/monaco-editor-webpack-plugin does it. The
// monaco-editor-webpack-plugin approach relies on injecting a different central module
// file, rather than zeroing out each feature/language module. Our approach necessitates the
// ALWAYS_ENABLED_FEATURES hack above. Our approach is fine for when esbuild is still an
// optional prototype build method for local dev, but this implementation should be fixed if
// we switch to esbuild by default.
// ALWAYS_ENABLED_FEATURES hack above.
build.onLoad({ filter }, () => ({ contents: '', loader: 'js' }))
},
})

View File

@ -32,7 +32,7 @@ async function buildWorker(
/**
* An esbuild plugin that bundles a Web Worker (classic, not a module worker) given an import of a
* `.worker.ts` file. Similar to https://github.com/webpack-contrib/worker-loader.
* `.worker.ts` file.
*
* TODO(sqs): This could be improved to use the new `new Worker(new URL(..., import.meta.url))`
* worker syntax that many bundlers are starting to prefer.

View File

@ -1,8 +1,2 @@
export * from './esbuild/plugins'
export * from './paths'
export * from './webpack/babel-loader'
export * from './webpack/css-loader'
export * from './webpack/getCacheConfig'
export * from './webpack/monaco-editor'
export * from './webpack/plugins'
export * from './utils/environment-config'

View File

@ -1,21 +1,4 @@
import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'
import type { WebpackPluginInstance, RuleSetRule } from 'webpack'
import { MONACO_EDITOR_PATH } from '../paths'
// CSS rule for monaco-editor and other external plain CSS (skip SASS and PostCSS for build perf)
export const getMonacoCSSRule = (): RuleSetRule => ({
test: /\.css$/,
include: [MONACO_EDITOR_PATH],
use: ['style-loader', { loader: 'css-loader' }],
})
// TTF rule for monaco-editor
export const getMonacoTTFRule = (): RuleSetRule => ({
test: /\.ttf$/,
include: [MONACO_EDITOR_PATH],
type: 'asset/resource',
})
import type MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'
/**
* Configuration for https://github.com/microsoft/monaco-editor-webpack-plugin.
@ -48,6 +31,3 @@ export const MONACO_LANGUAGES_AND_FEATURES: Required<
'suggest',
],
}
export const getMonacoWebpackPlugin = (): WebpackPluginInstance =>
new MonacoWebpackPlugin(MONACO_LANGUAGES_AND_FEATURES)

View File

@ -5,11 +5,9 @@ import path from 'path'
// TODO(bazel): drop when non-bazel removed.
const IS_BAZEL = !!(process.env.JS_BINARY__TARGET || process.env.BAZEL_BINDIR || process.env.BAZEL_TEST)
// NOTE: use fs.realpathSync() in addition to path.resolve() to resolve
// symlinks to the real path. This is required for webpack plugins using
// `include: [...file path...]` when the file path contains symlinks such
// as when using pnpm.
export function resolveWithSymlink(...args: string[]): string {
// NOTE: use fs.realpathSync() in addition to path.resolve() to resolve symlinks to the real path.
// This canonicalizes the path, which avoids potential bugs in the frontend bundler step.
function resolveWithSymlink(...args: string[]): string {
const resolvedPath = path.resolve(...args)
try {
@ -19,20 +17,12 @@ export function resolveWithSymlink(...args: string[]): string {
}
}
export function resolveAssetsPath(root: string): string {
function resolveAssetsPath(root: string): string {
if (IS_BAZEL && process.env.WEB_BUNDLE_PATH) {
return resolveWithSymlink(root, process.env.WEB_BUNDLE_PATH)
}
if (process.env.NODE_ENV && process.env.NODE_ENV === 'development') {
return resolveWithSymlink(root, 'ui/assets')
}
// With Bazel we changed how assets gets bundled. Previsouly, we would just put the assets at /ui/assets/.assets
// and be done with it. With Bazel, we have different loaders on the backend where the assets gets embedded. So
// what we do here is "simulate" what happens in bazel, by putting the assets in the correct relative directory
// so that when the backend is compiled the assets gets embedded properly
return resolveWithSymlink(root, 'ui/assets/enterprise')
return resolveWithSymlink(root, 'client/web/dist')
}
export const ROOT_PATH = IS_BAZEL ? process.cwd() : resolveWithSymlink(__dirname, '../../../')

View File

@ -1,13 +0,0 @@
import path from 'path'
import type { RuleSetRule } from 'webpack'
import { ROOT_PATH } from '../paths'
export const getBabelLoader = (): RuleSetRule => ({
loader: 'babel-loader',
options: {
cacheDirectory: true,
configFile: path.join(ROOT_PATH, 'babel.config.js'),
},
})

View File

@ -1,50 +0,0 @@
import path from 'path'
import type webpack from 'webpack'
import { ROOT_PATH, NODE_MODULES_PATH } from '../paths'
export const getBasicCSSLoader = (): webpack.RuleSetUseItem => ({
loader: 'css-loader',
options: { url: false },
})
interface CSSModulesLoaderOptions {
/**
* Allow to enable/disable handling the CSS functions `url` and `image-set`.
* Documentation: https://webpack.js.org/loaders/css-loader/#url.
*/
url?: boolean
sourceMap?: boolean
}
export const getCSSModulesLoader = ({ sourceMap, url }: CSSModulesLoaderOptions): webpack.RuleSetUseItem => ({
loader: 'css-loader',
options: {
sourceMap,
modules: {
exportLocalsConvention: 'camelCase',
localIdentName: '[name]__[local]_[hash:base64:5]',
},
url,
},
})
/**
* Generates array of CSS loaders both for regular CSS and CSS modules.
* Useful to ensure that we use the same configuration for shared loaders: postcss-loader, sass-loader, etc.
* */
export const getCSSLoaders = (...loaders: webpack.RuleSetUseItem[]): webpack.RuleSetUse => [
...loaders,
'postcss-loader',
{
loader: 'sass-loader',
options: {
sassOptions: {
includePaths: [NODE_MODULES_PATH, path.resolve(ROOT_PATH, 'client')],
},
},
},
]
export const getBazelCSSLoaders = (...loaders: webpack.RuleSetUseItem[]): webpack.RuleSetUse => loaders

View File

@ -1,26 +0,0 @@
import path from 'path'
import type webpack from 'webpack'
import { ROOT_PATH } from '../paths'
// TODO(bazel): drop when non-bazel removed.
const IS_BAZEL = !!(process.env.JS_BINARY__TARGET || process.env.BAZEL_BINDIR)
interface CacheConfigOptions {
invalidateCacheFiles?: string[]
}
export const getCacheConfig = ({ invalidateCacheFiles = [] }: CacheConfigOptions): webpack.Configuration['cache'] => ({
type: 'filesystem',
buildDependencies: {
// Invalidate cache on config change.
config: IS_BAZEL
? invalidateCacheFiles
: [
...invalidateCacheFiles,
path.resolve(ROOT_PATH, 'babel.config.js'),
path.resolve(ROOT_PATH, 'postcss.config.js'),
],
},
})

View File

@ -1,58 +0,0 @@
import path from 'path'
import StatoscopeWebpackPlugin from '@statoscope/webpack-plugin'
import TerserPlugin from 'terser-webpack-plugin'
import webpack, { type StatsOptions } from 'webpack'
import { STATIC_ASSETS_PATH } from '../paths'
export const getTerserPlugin = (): TerserPlugin =>
new TerserPlugin({
terserOptions: {
compress: {
// Don't inline functions, which causes name collisions with uglify-es:
// https://github.com/mishoo/UglifyJS2/issues/2842
inline: 1,
},
},
})
export const getProvidePlugin = (): webpack.ProvidePlugin =>
new webpack.ProvidePlugin({
// Adding the file extension is necessary to make importing this file
// work inside JavaScript modules. The alternative is to set
// `fullySpecified: false` (https://webpack.js.org/configuration/module/#resolvefullyspecified).
process: require.resolve('process/browser.js'),
// Based on the issue: https://github.com/webpack/changelog-v5/issues/10
Buffer: [require.resolve('buffer/index.js'), 'Buffer'],
})
const STATOSCOPE_STATS: StatsOptions = {
all: false, // disable all the stats
hash: true, // compilation hash
entrypoints: true,
chunks: true,
chunkModules: true, // modules
reasons: true, // modules reasons
ids: true, // IDs of modules and chunks (webpack 5)
dependentModules: true, // dependent modules of chunks (webpack 5)
chunkRelations: true, // chunk parents, children and siblings (webpack 5)
cachedAssets: true, // information about the cached assets (webpack 5)
nestedModules: true, // concatenated modules
usedExports: true,
providedExports: true, // provided imports
assets: true,
chunkOrigins: true, // chunks origins stats (to find out which modules require a chunk)
version: true, // webpack version
builtAt: true, // build at time
timings: true, // modules timing information
performance: true, // info about oversized assets
}
export const getStatoscopePlugin = (name = '[name]'): StatoscopeWebpackPlugin =>
new StatoscopeWebpackPlugin({
statsOptions: STATOSCOPE_STATS as Record<string, unknown>,
saveStatsTo: path.join(STATIC_ASSETS_PATH, `stats-${name}-[hash].json`),
saveReportTo: path.join(STATIC_ASSETS_PATH, `report-${name}-[hash].html`),
})

View File

@ -1,5 +0,0 @@
// @ts-check
module.exports = {
extends: '../../babel.config.js',
}

View File

@ -1,5 +0,0 @@
// @ts-check
module.exports = {
extends: '../../babel.config.js',
}

View File

@ -1,5 +0,0 @@
// @ts-check
module.exports = {
extends: '../../babel.config.js',
}

View File

@ -1,5 +0,0 @@
// @ts-check
module.exports = {
extends: '../../babel.config.js',
}

View File

@ -4,7 +4,12 @@ import path from 'path'
import * as esbuild from 'esbuild'
import { rm } from 'shelljs'
import { packageResolutionPlugin, stylePlugin, workerPlugin, buildTimerPlugin } from '@sourcegraph/build-config'
import {
packageResolutionPlugin,
stylePlugin,
workerPlugin,
buildTimerPlugin,
} from '@sourcegraph/build-config/src/esbuild/plugins'
const rootPath = path.resolve(__dirname, '../../../')
const jetbrainsWorkspacePath = path.resolve(rootPath, 'client', 'jetbrains')

View File

@ -1,5 +0,0 @@
// @ts-check
module.exports = {
extends: '../../babel.config.js',
}

View File

@ -59,9 +59,9 @@ ts_project(
jest_test(
name = "test",
data = [
"src/webBundleSize/__mocks__/assets/scripts/app.bundle.js",
"src/webBundleSize/__mocks__/assets/scripts/app.bundle.js.br",
"src/webBundleSize/__mocks__/assets/scripts/app.bundle.js.gz",
"src/webBundleSize/__mocks__/assets/scripts/main.js",
"src/webBundleSize/__mocks__/assets/scripts/main.js.br",
"src/webBundleSize/__mocks__/assets/scripts/main.js.gz",
"src/webBundleSize/__mocks__/assets/scripts/sg_home.js",
"src/webBundleSize/__mocks__/assets/scripts/sg_home.js.br",
"src/webBundleSize/__mocks__/assets/scripts/sg_home.js.gz",
@ -70,6 +70,6 @@ jest_test(
"src/webBundleSize/__mocks__/assets/styles/app.123.bundle.css.gz",
":observability-server_tests",
],
# TODO(bazel): requires webpack setup for testing
# TODO(bazel): requires dev server setup for testing
tags = ["manual"],
)

View File

@ -1,5 +0,0 @@
// @ts-check
module.exports = {
extends: '../../babel.config.js',
}

View File

@ -9,11 +9,11 @@ jest.mock(
() => ({
files: [
{
path: MOCK_ASSETS_PATH + '/scripts/*.br',
path: MOCK_ASSETS_PATH + '/scripts/*.js',
maxSize: '10kb',
},
{
path: MOCK_ASSETS_PATH + '/styles/*.br',
path: MOCK_ASSETS_PATH + '/styles/*.css',
maxSize: '10kb',
},
],
@ -24,8 +24,8 @@ jest.mock(
jest.mock(
'web.manifest.json',
() => ({
'app.js': '/.assets/scripts/app.bundle.js',
'app.css': '/.assets/styles/app.123.bundle.css',
'main.js': '/.assets/scripts/main.js',
'main.css': '/.assets/styles/main.css',
}),
{ virtual: true }
)
@ -39,33 +39,17 @@ describe('getBundleSizeStats', () => {
})
expect(stats).toEqual({
'scripts/app.bundle.js': {
'scripts/main.js': {
raw: 15,
gzip: 6,
brotli: 4,
isInitial: true,
isDynamicImport: false,
isDefaultVendors: false,
isCss: false,
isJs: true,
},
'scripts/sg_home.js': {
brotli: 4,
gzip: 6,
isCss: false,
isDefaultVendors: false,
isDynamicImport: true,
isInitial: false,
isJs: true,
raw: 15,
},
'styles/app.123.bundle.css': {
'styles/main.css': {
raw: 25,
gzip: 6,
brotli: 4,
isInitial: true,
isDynamicImport: false,
isDefaultVendors: false,
isCss: true,
isJs: false,
},

View File

@ -14,11 +14,8 @@ interface BundleSizeConfig {
interface BundleSizeStats {
[baseFilePath: string]: {
raw: number
gzip: number
brotli: number
isInitial: boolean
isDynamicImport: boolean
isDefaultVendors: boolean
isCss: boolean
isJs: boolean
}
@ -39,28 +36,21 @@ export function getBundleSizeStats(options: GetBundleSizeStatsOptions): BundleSi
const webBuildManifest = require(webBuildManifestPath) as Record<string, string>
const initialResources = new Set(
Object.values(webBuildManifest).map(resourcePath =>
path.join(staticAssetsPath, resourcePath.replace('/.assets/', ''))
)
Object.values(webBuildManifest)
.filter(value => typeof value === 'string')
.map(resourcePath => path.join(staticAssetsPath, resourcePath.replace('/.assets/', '')))
)
return bundleSizeConfig.files.reduce<BundleSizeStats>((result, file) => {
const filePaths = glob.sync(file.path)
const fileStats = filePaths.reduce((fileStats, brotliFilePath) => {
const { dir, name } = path.parse(brotliFilePath)
const noCompressionFilePath = path.join(dir, name)
const gzipFilePath = `${noCompressionFilePath}.gz`
const fileStats = filePaths.reduce((fileStats, noCompressionFilePath) => {
const name = path.basename(noCompressionFilePath)
return {
...fileStats,
[noCompressionFilePath.replace(`${staticAssetsPath}/`, '')]: {
raw: statSync(noCompressionFilePath).size,
gzip: statSync(gzipFilePath).size,
brotli: statSync(brotliFilePath).size,
isInitial: initialResources.has(noCompressionFilePath),
isDynamicImport: name.startsWith('sg_'),
isDefaultVendors: /\d+.chunk.js/.test(name),
isDynamicImport: name.startsWith('chunk-'),
isCss: path.parse(name).ext === '.css',
isJs: path.parse(name).ext === '.js',
},

View File

@ -2,7 +2,7 @@
* The script collects web application bundlesize information from the disk and uploads it to Honeycomb.
*
* 1. Build web application using:
* NODE_ENV=production DISABLE_TYPECHECKING=true WEBPACK_USE_NAMED_CHUNKS=true pnpm build-web
* NODE_ENV=production DISABLE_TYPECHECKING=true pnpm build-web
*
* 2. Upload bundlesize information to Honeycomb:
* HONEYCOMB_API_KEY=XXX pnpm --filter @sourcegraph/observability-server run bundlesize:web:upload
@ -48,11 +48,8 @@ for (const [baseFilePath, fileInfo] of Object.entries(bundleSizeStats)) {
'bundle.file.name': baseFilePath,
'bundle.file.size.raw': fileInfo.raw,
'bundle.file.size.gzip': fileInfo.gzip,
'bundle.file.size.brotli': fileInfo.brotli,
'bundle.file.isInitial': fileInfo.isInitial,
'bundle.file.isDynamicImport': fileInfo.isDynamicImport,
'bundle.file.isDefaultVendors': fileInfo.isDefaultVendors,
'bundle.file.isCss': fileInfo.isCss,
'bundle.file.isJs': fileInfo.isJs,
'bundle.env': environment.NODE_ENV,

View File

@ -1,8 +0,0 @@
// @ts-check
/** @type {import('@babel/core').TransformOptions} */
const config = {
extends: '../../babel.config.js',
}
module.exports = config

View File

@ -8,15 +8,13 @@ declare module '*.css' {
}
/**
* For Web Worker entrypoints using Webpack's worker-loader.
*
* See https://github.com/webpack-contrib/worker-loader#integrating-with-typescript.
* For Web Worker entrypoints using workerPlugin.
*/
declare module '*.worker.ts' {
class WebpackWorker extends Worker {
class _Worker extends Worker {
constructor()
}
export default WebpackWorker
export default _Worker
}
/**

View File

@ -179,10 +179,7 @@ function getDebugExpressionFromRegexp(tag: string, regexp: string): string {
// Console logs with these keywords will be removed from the console output.
const MUTE_CONSOLE_KEYWORDS = [
'[webpack-dev-server]',
'Download the React DevTools',
'[HMR]',
'[WDS]',
'Warning: componentWillReceiveProps has been renamed',
'Download the Apollo DevTools',
'Compiled in DEBUG mode',

View File

@ -3,7 +3,7 @@ import React, { type Attributes, type PropsWithChildren, type PropsWithRef } fro
/**
* Returns a lazy-loaded reference to a React component in another module.
*
* This should be used in URL routes and anywhere else that Webpack code splitting can occur, to
* This should be used in URL routes and anywhere else that code splitting can occur, to
* avoid all referenced components being in the initial bundle.
*
* @param componentFactory Asynchronously imports the component's module; e.g., `() =>

View File

@ -1,5 +0,0 @@
// @ts-check
module.exports = {
extends: '../../babel.config.js',
}

View File

@ -1,5 +0,0 @@
// @ts-check
module.exports = {
extends: '../../babel.config.js',
}

View File

@ -1,5 +0,0 @@
// @ts-check
module.exports = {
extends: '../../babel.config.js',
}

View File

@ -10,7 +10,7 @@ import {
workerPlugin,
RXJS_RESOLUTIONS,
buildTimerPlugin,
} from '@sourcegraph/build-config'
} from '@sourcegraph/build-config/src/esbuild/plugins'
const minify = process.env.NODE_ENV === 'production'
const outdir = path.join(__dirname, '../dist')

View File

@ -45,7 +45,7 @@ SRCS = [
".env.oss",
"//client/wildcard:sass-breakpoints",
"//client/wildcard:global-style-sources",
"//ui/assets/img:copy",
"//client/web/dist/img:copy",
] + glob(
["src/" + d for d in [
"**/*.scss",
@ -133,7 +133,7 @@ vite_bin.vite(
env = {
"BAZEL": "1",
},
visibility = ["//ui/assets/enterprise:__pkg__"],
visibility = ["//client/web/dist:__pkg__"],
# silent_on_success = False,
)

View File

@ -1,3 +1,3 @@
export { default as logoLight } from '$root/ui/assets/img/sourcegraph-logo-light.svg'
export { default as logoDark } from '$root/ui/assets/img/sourcegraph-logo-dark.svg'
export { default as mark } from '$root/ui/assets/img/sourcegraph-mark.svg'
export { default as logoLight } from '$root/client/web/dist/img/sourcegraph-logo-light.svg'
export { default as logoDark } from '$root/client/web/dist/img/sourcegraph-logo-dark.svg'
export { default as mark } from '$root/client/web/dist/img/sourcegraph-mark.svg'

View File

@ -6,16 +6,16 @@ import { vitePreprocess } from '@sveltejs/kit/vite'
let adapter
if (process.env.BAZEL || process.env.DEPLOY_TYPE === 'dev') {
// The folder inside ui/assets to write the production files to.
// The folder to write the production files to.
// We store the files in a separate folder to avoid any conflicts
// with files generated by webpack.
// with files generated by the web builder.
const OUTPUT_DIR = '_sk'
let out = 'build/'
if (process.env.DEPLOY_TYPE === 'dev' && !process.env.BAZEL) {
// When DEPLOY_TYPE is set to 'dev' we copy output files to the
// 'assets' folder where the web server reads them from
out = '../../ui/assets/'
out = '../../client/web/dist/'
}
out += OUTPUT_DIR
@ -97,7 +97,7 @@ export default config
* web root directory.
*
* The SvelteKit artifacts however are deployed into a subdirectory,
* to not conflict with any webpack artifacts, which means they are
* to not conflict with any artifacts, which means they are
* served from a different location than SvelteKit assumes.
* In theory we could use the 'paths.assets' option to configure
* this path, but at the moment it only accepts a fully-qualified

View File

@ -5,7 +5,8 @@ load("@npm//:bundlesize2/package_json.bzl", bundlesize_bin = "bin")
load("//client/shared/dev:generate_graphql_operations.bzl", "generate_graphql_operations")
load("//client/shared/dev:tools.bzl", "module_style_typings")
load("//dev:defs.bzl", "jest_test", "npm_package", "sass", "ts_project")
load("//dev:webpack.bzl", "webpack_bundle", "webpack_web_app")
load("@aspect_rules_esbuild//esbuild:defs.bzl", "esbuild")
load("//dev:esbuild.bzl", "esbuild_web_app")
load("@aspect_rules_ts//ts:defs.bzl", "ts_config")
load("//dev:eslint.bzl", "eslint_config_and_lint_root", "eslint_test_with_types")
@ -399,10 +400,10 @@ ts_project(
"src/devsettings/settings/eventLoggingDebug.tsx",
"src/devsettings/utils.ts",
"src/enterprise/EnterpriseWebApp.tsx",
"src/enterprise/app-main.tsx",
"src/enterprise/app/AppAuthCallbackPage.tsx",
"src/enterprise/app/components/index.ts",
"src/enterprise/app/components/zero-states/AppZeroStates.tsx",
"src/enterprise/app/main.tsx",
"src/enterprise/app/routes.tsx",
"src/enterprise/app/settings/AppSettingsArea.tsx",
"src/enterprise/app/settings/about/AboutPage.tsx",
@ -722,7 +723,7 @@ ts_project(
"src/enterprise/dotcom/productSubscriptions/features.ts",
"src/enterprise/embed/EmbeddedWebApp.tsx",
"src/enterprise/embed/OpenNewTabAnchorLink.tsx",
"src/enterprise/embed/main.tsx",
"src/enterprise/embed/embedMain.tsx",
"src/enterprise/executors/ExecutorsSiteAdminArea.tsx",
"src/enterprise/executors/ExecutorsUserArea.tsx",
"src/enterprise/executors/instances/ExecutorCompatibilityAlert.tsx",
@ -2078,29 +2079,17 @@ jest_test(
],
)
# webpack dev environment -------------------
WEBPACK_CONFIG_DEPS = [
# esbuild dev environment -------------------
ESBUILD_CONFIG_DEPS = [
":node_modules/@sourcegraph/build-config",
"//:node_modules/@babel/runtime",
"//:node_modules/@pmmmwh/react-refresh-webpack-plugin",
"//:node_modules/@sentry/webpack-plugin",
"//:node_modules/buffer",
"//:node_modules/css-loader",
"//:node_modules/style-loader",
"//:node_modules/css-minimizer-webpack-plugin",
"//:node_modules/lodash",
"//:node_modules/mini-css-extract-plugin",
"//:node_modules/path-browserify",
"//:node_modules/punycode",
"//:node_modules/util",
"//:node_modules/events",
"//:node_modules/webpack-manifest-plugin",
"//:node_modules/webpack-stats-plugin",
"//:node_modules/monaco-editor",
"//:node_modules/babel-loader",
"//:node_modules/compression-webpack-plugin",
"//:node_modules/webpack",
"//:node_modules/react-refresh",
"//:node_modules/esbuild",
"//client/web/dev",
"//:node_modules/react-visibility-sensor", # required for https://github.com/joshwnj/react-visibility-sensor/issues/148 workaround
"//:postcss_config_js",
@ -2117,62 +2106,51 @@ BUNDLE_DATA_DEPS = [
# TODO(bazel): this is already in the main ts_project(srcs). Why also here?
"src/enterprise/codeintel/configuration/schema.json",
":enterprise-yaml",
"//client/web/dev:esbuild-config",
]
[
webpack_bundle(
name = "bundle" if prod else "bundle-dev",
srcs = BUNDLE_DATA_DEPS + [
":web_lib",
"//:babel_config",
"//:browserslist",
"//:package_json",
],
entry_points = {
"src/enterprise/main.js": "app",
"src/enterprise/embed/main.js": "embed",
},
env = {
"NODE_ENV": "production" if prod else "development",
"INTEGRATION_TESTS": "false",
"WEBPACK_BUNDLE_ANALYZER": "false",
"WEBPACK_USE_NAMED_CHUNKS": "false",
},
output_dir = True,
visibility = ["//ui/assets:__subpackages__"],
webpack_config = "webpack.bazel.config.js",
deps = WEBPACK_CONFIG_DEPS,
)
for prod in [
True,
False,
]
]
# Used for integration tests and has bigger bundle size
# because of the inlined source-maps.
webpack_web_app(
name = "app",
esbuild(
name = "bundle",
srcs = BUNDLE_DATA_DEPS + [
"//:babel_config",
"//:browserslist",
":web_lib",
"//:package_json",
],
entry_points = {
"src/enterprise/main.js": "app",
config = "//client/web/dev:esbuild-config",
define = {
"process.env.NODE_ENV": "\"production\"",
},
env = {
"NODE_ENV": "production",
"INTEGRATION_TESTS": "true",
entry_points = [
"src/enterprise/main.js",
"src/enterprise/embed/embedMain.js",
],
minify = True,
splitting = True,
visibility = ["//client/web/dist:__subpackages__"],
deps = ESBUILD_CONFIG_DEPS,
)
# Used for integration tests.
esbuild_web_app(
name = "app",
srcs = BUNDLE_DATA_DEPS + [
":web_lib",
"//:package_json",
],
config = "//client/web/dev:esbuild-config",
define = {
"process.env.NODE_ENV": "\"production\"",
"process.env.INTEGRATION_TESTS": "true",
},
output_dir = True,
entry_points = [
"src/enterprise/main.js",
"src/enterprise/embed/embedMain.js",
],
visibility = ["//visibility:public"],
webpack_config = "webpack.bazel.config.js",
deps = WEBPACK_CONFIG_DEPS,
deps = ESBUILD_CONFIG_DEPS,
)
build_test(
name = "webpack_test",
name = "esbuild_test",
size = "enormous",
targets = [
":bundle",

View File

@ -10,7 +10,7 @@ Our local development server runs by starting both a [Caddy](https://caddyserver
Environment variables important for the web server:
1. `WEB_BUILDER_SERVE_INDEX` should be set to `true` to enable `HTMLWebpackPlugin`.
1. `WEB_BUILDER_SERVE_INDEX` should be set to `true` to enable serving of an index page.
2. `SOURCEGRAPH_API_URL` is used as a proxied API url. By default it points to the [https://k8s.sgdev.org](https://k8s.sgdev.org).
It's possible to overwrite these variables by creating `sg.config.overwrite.yaml` in the root folder and adjusting the `env` section of the relevant command.
@ -21,12 +21,6 @@ It's possible to overwrite these variables by creating `sg.config.overwrite.yaml
sg start web-standalone
```
For open-source version:
```sh
sg start oss-web-standalone
```
#### Public API
To use a public API that doesn't require authentication for most of the functionality:
@ -35,32 +29,16 @@ To use a public API that doesn't require authentication for most of the function
SOURCEGRAPH_API_URL=https://sourcegraph.com sg start web-standalone
```
For open-source version:
```sh
SOURCEGRAPH_API_URL=https://sourcegraph.com sg start oss-web-standalone
```
### Production server
```sh
sg start web-standalone-prod
```
For open-source version:
```sh
sg start oss-web-standalone-prod
```
Web app should be available at `https://${SOURCEGRAPH_HTTPS_DOMAIN}:${SOURCEGRAPH_HTTPS_PORT}`. Build artifacts will be served from `<rootRepoPath>/ui/assets`.
Web app should be available at `https://${SOURCEGRAPH_HTTPS_DOMAIN}:${SOURCEGRAPH_HTTPS_PORT}`. Build artifacts will be served from `<rootRepoPath>/client/web/dist`.
Note: If you are unable to use the above commands (e.g. you can't install Caddy), you can use `sg run web-standalone-http` instead. This will start a development server using only Node, and will be available at `http://localhost:${SOURCEGRAPH_HTTP_PORT}`.
### API proxy
In both environments, server proxies API requests to `SOURCEGRAPH_API_URL` provided as the `.env` variable.
### esbuild (experimental)
See https://docs.sourcegraph.com/dev/background-information/web/build#esbuild.

View File

@ -1,8 +0,0 @@
// @ts-check
/** @type {import('@babel/core').TransformOptions} */
const config = {
extends: '../../babel.config.js',
}
module.exports = config

View File

@ -1,13 +1,6 @@
const path = require('path')
function relativeAssets(base) {
if (process.env.NODE_ENV !== undefined && process.env.NODE_ENV === 'development') {
return path.join(base, '../../ui/assets')
}
return path.join(base, '../../ui/assets/enterprise')
}
const STATIC_ASSETS_PATH = process.env.WEB_BUNDLE_PATH || relativeAssets(__dirname)
const STATIC_ASSETS_PATH = process.env.WEB_BUNDLE_PATH || path.join(__dirname, 'dist')
const config = {
files: [
@ -15,75 +8,39 @@ const config = {
* Our main entry JavaScript bundles, contains core logic that is loaded on every page.
*/
{
path: path.join(STATIC_ASSETS_PATH, 'scripts/app.*.bundle.js.br'),
path: path.join(STATIC_ASSETS_PATH, 'main*.js'),
/**
* Note: Temporary increase from 400kb.
* Primary cause is due to multiple ongoing migrations that mean we are duplicating similar dependencies.
* Issue to track: https://github.com/sourcegraph/sourcegraph/issues/37845
*/
maxSize: '500kb',
compression: 'none',
},
{
path: path.join(STATIC_ASSETS_PATH, 'scripts/embed.*.bundle.js.br'),
path: path.join(STATIC_ASSETS_PATH, 'embedMain*.js'),
maxSize: '155kb',
compression: 'none',
},
{
path: path.join(STATIC_ASSETS_PATH, 'scripts/react.*.bundle.js.br'),
maxSize: '45kb',
compression: 'none',
},
{
path: path.join(STATIC_ASSETS_PATH, 'scripts/opentelemetry.*.bundle.js.br'),
maxSize: '40kb',
compression: 'none',
},
/**
* Our generated application chunks. Matches the deterministic id generated by Webpack.
*
* Note: The vast majority of our chunks are under 200kb, this threshold is bloated as we treat the Monaco editor as a normal chunk.
* We should consider not doing this, as it is much larger than other chunks and we would likely benefit from caching this differently.
* Issue to improve this: https://github.com/sourcegraph/sourcegraph/issues/26573
*/
{
path: path.join(STATIC_ASSETS_PATH, 'scripts/!(sg_)*.chunk.js.br'),
maxSize: '500kb',
compression: 'none',
},
{
path: path.join(STATIC_ASSETS_PATH, 'scripts/sg_*.chunk.js.br'),
maxSize: '200kb',
compression: 'none',
},
/**
* Our generated worker files.
*/
{
path: path.join(STATIC_ASSETS_PATH, '*.worker.js.br'),
maxSize: '250kb',
compression: 'none',
path: path.join(STATIC_ASSETS_PATH, 'chunks/chunk-*.js'),
maxSize: '600kb', // 2 monaco chunks are very big
},
/**
* Our main entry CSS bundle, contains core styles that are loaded on every page.
*/
{
path: path.join(STATIC_ASSETS_PATH, 'styles/app.*.css.br'),
maxSize: '50kb',
compression: 'none',
path: path.join(STATIC_ASSETS_PATH, 'main*.css'),
maxSize: '350kb',
},
/**
* Notebook embed main entry CSS bundle.
*/
{
path: path.join(STATIC_ASSETS_PATH, 'styles/embed.*.css.br'),
maxSize: '35kb',
compression: 'none',
path: path.join(STATIC_ASSETS_PATH, 'embedMain*.css'),
maxSize: '350kb',
},
{
path: path.join(STATIC_ASSETS_PATH, 'styles/!(app|embed).*.css.br'),
maxSize: '25kb',
compression: 'none',
path: path.join(STATIC_ASSETS_PATH, 'chunks/chunk-*.css'),
maxSize: '45kb',
},
],
}

View File

@ -1,3 +1,4 @@
load("@aspect_rules_js//js:defs.bzl", "js_library")
load("@aspect_rules_ts//ts:defs.bzl", "ts_config")
load("//dev:defs.bzl", "ts_project")
@ -5,9 +6,6 @@ load("//dev:defs.bzl", "ts_project")
# gazelle:js_files **/*.{ts,tsx}
# gazelle:js_test_files there-are-no-tests
# TODO(bazel): wip - currently broken
# gazelle:exclude **/server/development.server.ts
ts_config(
name = "tsconfig",
src = "tsconfig.json",
@ -20,8 +18,13 @@ ts_config(
ts_project(
name = "dev",
srcs = [
"esbuild/build.ts",
"esbuild/manifest.test.ts",
"esbuild/manifest.ts",
"esbuild/manifestPlugin.ts",
"esbuild/server.ts",
"mocks/mockEventLogger.ts",
"server/bazel.server.ts",
"server/development.server.ts",
"server/production.server.ts",
"utils/constants.ts",
"utils/create-js-context.ts",
@ -37,6 +40,7 @@ ts_project(
tsconfig = ":tsconfig",
visibility = ["//client/web:__subpackages__"],
deps = [
"//:node_modules/@sentry/esbuild-plugin",
"//:node_modules/@types/compression",
"//:node_modules/@types/connect-history-api-fallback",
"//:node_modules/@types/express",
@ -47,15 +51,25 @@ ts_project(
"//:node_modules/chalk",
"//:node_modules/compression",
"//:node_modules/connect-history-api-fallback",
"//:node_modules/esbuild",
"//:node_modules/express",
"//:node_modules/express-static-gzip",
"//:node_modules/http-proxy-middleware",
"//:node_modules/jsonc-parser",
"//:node_modules/lodash",
"//:node_modules/signale",
"//:node_modules/webpack",
"//:node_modules/webpack-dev-server",
"//client/web:node_modules/@sourcegraph/build-config",
"//client/web:web_lib",
],
)
js_library(
name = "esbuild-config",
srcs = ["esbuild/esbuild.bazel.js"],
visibility = ["//client:__subpackages__"],
deps = [
"//client/build-config:build-config_lib",
"//client/web:node_modules/@sourcegraph/build-config",
"//client/web/dev",
],
)

View File

@ -1,4 +0,0 @@
# TODO(bazel): esbuild dev tools currently disabled in bazel
# See client/build-config/src/esbuild/BUILD.bazel
# gazelle:js disabled

View File

@ -4,18 +4,16 @@ import path from 'path'
import { sentryEsbuildPlugin } from '@sentry/esbuild-plugin'
import * as esbuild from 'esbuild'
import { ROOT_PATH, STATIC_ASSETS_PATH } from '@sourcegraph/build-config'
import {
MONACO_LANGUAGES_AND_FEATURES,
ROOT_PATH,
STATIC_ASSETS_PATH,
stylePlugin,
packageResolutionPlugin,
monacoPlugin,
RXJS_RESOLUTIONS,
buildMonaco,
buildTimerPlugin,
} from '@sourcegraph/build-config'
import { isDefined } from '@sourcegraph/common'
} from '@sourcegraph/build-config/src/esbuild/plugins'
import { MONACO_LANGUAGES_AND_FEATURES } from '@sourcegraph/build-config/src/monaco-editor'
import { ENVIRONMENT_CONFIG, IS_DEVELOPMENT, IS_PRODUCTION } from '../utils'
@ -25,20 +23,22 @@ const isCodyApp = ENVIRONMENT_CONFIG.CODY_APP
const omitSlowDeps = ENVIRONMENT_CONFIG.DEV_WEB_BUILDER_OMIT_SLOW_DEPS
export const BUILD_OPTIONS: esbuild.BuildOptions = {
entryPoints: {
'scripts/app': isCodyApp
? path.join(ROOT_PATH, 'client/web/src/enterprise/app-main.tsx')
: path.join(ROOT_PATH, 'client/web/src/enterprise/main.tsx'),
},
entryPoints: isCodyApp
? [path.join(ROOT_PATH, 'client/web/src/enterprise/app/main.tsx')]
: [
path.join(ROOT_PATH, 'client/web/src/enterprise/main.tsx'),
path.join(ROOT_PATH, 'client/web/src/enterprise/embed/embedMain.tsx'),
],
bundle: true,
minify: IS_PRODUCTION,
format: 'esm',
logLevel: 'error',
jsx: 'automatic',
jsxDev: IS_DEVELOPMENT,
splitting: true,
chunkNames: 'chunks/chunk-[name]-[hash]',
entryNames: IS_PRODUCTION ? 'scripts/[name]-[hash]' : undefined,
entryNames: '[name]-[hash]',
outdir: STATIC_ASSETS_PATH,
plugins: [
stylePlugin,
@ -74,10 +74,10 @@ export const BUILD_OPTIONS: esbuild.BuildOptions = {
authToken: ENVIRONMENT_CONFIG.SENTRY_DOT_COM_AUTH_TOKEN,
silent: true,
release: { name: `frontend@${ENVIRONMENT_CONFIG.VERSION}` },
sourcemaps: { assets: path.join(ENVIRONMENT_CONFIG.STATIC_ASSETS_PATH, 'scripts', '*.map') },
sourcemaps: { assets: [path.join('dist', '*.map'), path.join('dist', 'chunks', '*.map')] },
})
: null,
].filter(isDefined),
].filter((plugin): plugin is esbuild.Plugin => plugin !== null),
define: {
...Object.fromEntries(
Object.entries({ ...ENVIRONMENT_CONFIG, SOURCEGRAPH_API_URL: undefined }).map(([key, value]) => [
@ -98,20 +98,30 @@ export const BUILD_OPTIONS: esbuild.BuildOptions = {
}
export const build = async (): Promise<void> => {
if (!BUILD_OPTIONS.outdir) {
throw new Error('no outdir')
}
const metafile = process.env.ESBUILD_METAFILE
const result = await esbuild.build({
const options: esbuild.BuildOptions = {
...BUILD_OPTIONS,
outdir: STATIC_ASSETS_PATH,
metafile: Boolean(metafile),
})
}
const result = await esbuild.build(options)
if (metafile) {
writeFileSync(metafile, JSON.stringify(result.metafile), 'utf-8')
}
if (!omitSlowDeps) {
const ctx = await buildMonaco(STATIC_ASSETS_PATH)
const ctx = await buildMonaco(BUILD_OPTIONS.outdir)
await ctx.rebuild()
await ctx.dispose()
}
if (process.env.WATCH) {
const ctx = await esbuild.context(options)
await ctx.watch()
await new Promise(() => {}) // wait forever
}
}
if (require.main === module) {

View File

@ -0,0 +1,18 @@
// This file is only used by Bazel builds.
const { BUILD_OPTIONS } = require('./build.js')
module.exports = {
// TODO(sqs): does not support Cody app build
...BUILD_OPTIONS,
// Unset configuration properties that are provided by Bazel.
entryPoints: undefined,
bundle: undefined,
outdir: undefined,
sourcemap: undefined,
splitting: undefined,
// Bazel's esbuild rule can't set process.env.NODE_ENV, so we need to override this here.
jsxDev: false,
}

View File

@ -0,0 +1,64 @@
import { type WebBuildManifest, createManifestFromBuildResult } from './manifest'
describe('createManifestFromBuildResult', () => {
test('non-bazel', () =>
expect(
createManifestFromBuildResult(
{ entryPoints: ['src/enterprise/main.tsx', 'src/enterprise/embed/embedMain.tsx'], outdir: 'dist' },
{
'dist/main-AAA.js': {
entryPoint: 'src/enterprise/main.tsx',
cssBundle: 'dist/main-BBB.css',
imports: [],
exports: [],
inputs: {},
bytes: 123,
},
'dist/embedMain-CCC.js': {
entryPoint: 'src/enterprise/embed/embedMain.tsx',
cssBundle: 'dist/embedMain-DDD.css',
imports: [],
exports: [],
inputs: {},
bytes: 123,
},
}
)
).toEqual<WebBuildManifest>({
'main.js': 'main-AAA.js',
'main.css': 'main-BBB.css',
'embed.js': 'embedMain-CCC.js',
'embed.css': 'embedMain-DDD.css',
}))
test('bazel', () =>
expect(
createManifestFromBuildResult(
{ entryPoints: ['src/enterprise/main.js', 'src/enterprise/embed/embedMain.js'], outdir: 'dist' },
{
'client/web/bundle/main-AAA.js': {
entryPoint: 'src/enterprise/main.js',
cssBundle: 'client/web/bundle/main-BBB.css',
imports: [],
exports: [],
inputs: {},
bytes: 123,
},
'client/web/bundle/embedMain-CCC.js': {
entryPoint: 'src/enterprise/embed/embedMain.js',
cssBundle: 'client/web/bundle/embedMain-DDD.css',
imports: [],
exports: [],
inputs: {},
bytes: 123,
},
},
true
)
).toEqual<WebBuildManifest>({
'main.js': 'main-AAA.js',
'main.css': 'main-BBB.css',
'embed.js': 'embedMain-CCC.js',
'embed.css': 'embedMain-DDD.css',
}))
})

View File

@ -0,0 +1,90 @@
import path from 'path'
import type * as esbuild from 'esbuild'
export interface WebBuildManifest {
/** Main JS bundle. */
'main.js': string
/** Main CSS bundle. */
'main.css': string
/** Embed JS bundle. */
'embed.js': string
/** Embed CSS bundle. */
'embed.css': string
}
export const assetPathPrefix = '/.assets'
export const WEB_BUILD_MANIFEST_FILENAME = 'web.manifest.json'
/**
* Create a web manifest from esbuild build results.
*
*
* @param buildOptions The esbuild options
* @param outputs The esbuild metafile outputs
* @param inBazel Whether the build is running in Bazel
*/
export function createManifestFromBuildResult(
buildOptions: { entryPoints: string[]; outdir: string },
outputs: esbuild.Metafile['outputs'],
inBazel = Boolean(process.env.BAZEL_BINDIR)
): WebBuildManifest {
const outdir = path.relative(process.cwd(), buildOptions.outdir)
const assetPath = (filePath: string): string => {
if (inBazel) {
const BAZEL_PATH_PREFIX = /^client\/web\/(app_)?bundle\//
if (!BAZEL_PATH_PREFIX.test(filePath)) {
throw new Error(`expected filePath to match ${BAZEL_PATH_PREFIX}, got ${filePath}`)
}
return filePath.replace(BAZEL_PATH_PREFIX, '')
}
return path.relative(outdir, filePath)
}
if (buildOptions.entryPoints.length !== 2) {
throw new Error('expected 2 entryPoints (main and embed)')
}
const [mainEntrypoint, embedEntrypoint] = buildOptions.entryPoints.map(filePath =>
path.relative(process.cwd(), filePath)
)
const manifest: Partial<WebBuildManifest> = {}
// Find the entrypoint in the output files
for (const [asset, output] of Object.entries(outputs)) {
if (!output.entryPoint) {
continue
}
if (output.entryPoint.endsWith(mainEntrypoint)) {
manifest['main.js'] = assetPath(asset)
if (output.cssBundle) {
manifest['main.css'] = assetPath(output.cssBundle)
}
} else if (output.entryPoint.endsWith(embedEntrypoint)) {
manifest['embed.js'] = assetPath(asset)
if (output.cssBundle) {
manifest['embed.css'] = assetPath(output.cssBundle)
}
}
}
if (!manifest['main.js']) {
throw new Error('no main.js found in outputs')
}
if (!manifest['main.css']) {
throw new Error('no main.css found in outputs')
}
if (!manifest['embed.js']) {
throw new Error('no embed.js found in outputs')
}
if (!manifest['embed.css']) {
throw new Error('no embed.css found in outputs')
}
return manifest as WebBuildManifest
}

View File

@ -3,66 +3,48 @@ import path from 'path'
import type * as esbuild from 'esbuild'
import { STATIC_ASSETS_PATH } from '@sourcegraph/build-config'
import { type WebBuildManifest, WEB_BUILD_MANIFEST_PATH } from '../utils'
export const assetPathPrefix = '/.assets'
export const getManifest = (jsEntrypoint?: string, cssEntrypoint?: string): WebBuildManifest => ({
'app.js': path.join(assetPathPrefix, jsEntrypoint ?? 'scripts/app.js'),
'app.css': path.join(assetPathPrefix, cssEntrypoint ?? 'scripts/app.css'),
isModule: true,
})
const writeManifest = async (manifest: WebBuildManifest): Promise<void> => {
await fs.promises.writeFile(WEB_BUILD_MANIFEST_PATH, JSON.stringify(manifest, null, 2))
}
const ENTRYPOINT_NAME = 'scripts/app'
import { WEB_BUILD_MANIFEST_FILENAME, createManifestFromBuildResult } from './manifest'
/**
* An esbuild plugin to write a web.manifest.json file (just as Webpack does), for compatibility
* with our current Webpack build.
* An esbuild plugin to write a web.manifest.json file.
*/
export const manifestPlugin: esbuild.Plugin = {
name: 'manifest',
setup: build => {
const origMetafile = build.initialOptions.metafile
build.initialOptions.metafile = true
build.onEnd(async result => {
console.log(process.cwd())
const { entryPoints } = build.initialOptions
const outputs = result?.metafile?.outputs
if (!entryPoints) {
console.error('[manifestPlugin] No entrypoints found')
return
if (!origMetafile) {
// If we were the only consumers of the metafile, then delete it from the result to
// avoid unexpected behavior from other downstream consumers relying on the metafile
// despite not actually enabling it in the config.
delete result.metafile
}
const absoluteEntrypoint: string | undefined = (entryPoints as any)[ENTRYPOINT_NAME]
if (!absoluteEntrypoint) {
console.error('[manifestPlugin] No entrypoint found with the name scripts/app')
return
if (!checkEntryPoints(entryPoints)) {
throw new Error('[manifestPlugin] Unexpected entryPoints format')
}
const { outdir } = build.initialOptions
if (!outdir) {
throw new Error('[manifestPlugin] No outdir found')
}
const relativeEntrypoint = path.relative(process.cwd(), absoluteEntrypoint)
if (!outputs) {
return
}
let jsEntrypoint: string | undefined
let cssEntrypoint: string | undefined
// Find the entrypoint in the output files
for (const [asset, output] of Object.entries(outputs)) {
if (output.entryPoint === relativeEntrypoint) {
jsEntrypoint = path.relative(STATIC_ASSETS_PATH, asset)
if (output.cssBundle) {
cssEntrypoint = path.relative(STATIC_ASSETS_PATH, output.cssBundle)
}
}
throw new Error('[manifestPlugin] No outputs found')
}
await writeManifest(getManifest(jsEntrypoint, cssEntrypoint))
const manifest = createManifestFromBuildResult({ entryPoints, outdir }, outputs)
const manifestPath = path.join(outdir, WEB_BUILD_MANIFEST_FILENAME)
await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2))
})
},
}
function checkEntryPoints(entryPoints: esbuild.BuildOptions['entryPoints']): entryPoints is string[] {
return Array.isArray(entryPoints) && typeof entryPoints[0] === 'string'
}

View File

@ -5,12 +5,13 @@ import express from 'express'
import { createProxyMiddleware } from 'http-proxy-middleware'
import signale from 'signale'
import { STATIC_ASSETS_PATH, buildMonaco } from '@sourcegraph/build-config'
import { STATIC_ASSETS_PATH } from '@sourcegraph/build-config'
import { buildMonaco } from '@sourcegraph/build-config/src/esbuild/monacoPlugin'
import { ENVIRONMENT_CONFIG, HTTPS_WEB_SERVER_URL, printSuccessBanner } from '../utils'
import { BUILD_OPTIONS } from './build'
import { assetPathPrefix } from './manifestPlugin'
import { assetPathPrefix } from './manifest'
export const esbuildDevelopmentServer = async (
listenAddress: { host: string; port: number },
@ -44,7 +45,7 @@ export const esbuildDevelopmentServer = async (
createProxyMiddleware({
target: { protocol: 'http:', host: esbuildHost, port: esbuildPort },
pathRewrite: { [`^${assetPathPrefix}`]: '' },
onProxyRes: (proxyResponse, request, response) => {
onProxyRes: (_proxyResponse, request, response) => {
// Cache chunks because their filename includes a hash of the content.
const isCacheableChunk = path.basename(request.url).startsWith('chunk-')
if (isCacheableChunk) {
@ -58,7 +59,7 @@ export const esbuildDevelopmentServer = async (
const proxyServer = proxyApp.listen(listenAddress)
// eslint-disable-next-line @typescript-eslint/return-await
return await new Promise<void>((resolve, reject) => {
return await new Promise<void>((_resolve, reject) => {
proxyServer.once('listening', () => {
signale.success(`esbuild server is ready after ${Math.round(performance.now() - start)}ms`)
printSuccessBanner(['✱ Sourcegraph is really ready now!', `Click here: ${HTTPS_WEB_SERVER_URL}`])

View File

@ -0,0 +1,12 @@
// @ts-check
const config = require('../../../jest.config.base')
/** @type {import('@jest/types').Config.InitialOptions} */
const exportedConfig = {
...config,
displayName: 'web-dev',
rootDir: __dirname,
roots: ['<rootDir>'],
}
module.exports = exportedConfig

View File

@ -1,69 +0,0 @@
import compression from 'compression'
import type WebpackDevServer from 'webpack-dev-server'
import { STATIC_ASSETS_PATH } from '@sourcegraph/build-config'
import {
ENVIRONMENT_CONFIG,
getAPIProxySettings,
getIndexHTML,
getWebBuildManifest,
shouldCompressResponse,
STATIC_ASSETS_URL,
} from '../utils'
const { SOURCEGRAPH_API_URL, SOURCEGRAPH_HTTPS_PORT, SOURCEGRAPH_HTTP_PORT } = ENVIRONMENT_CONFIG
export function createDevelopmentServerConfig(): WebpackDevServer.Configuration {
console.log(ENVIRONMENT_CONFIG)
// if (!SOURCEGRAPH_API_URL) {
// throw new Error('bazel.server.ts only supports *web-standalone* usage')
// }
const apiURL = SOURCEGRAPH_API_URL!
const { proxyRoutes, ...proxyConfig } = getAPIProxySettings({
apiURL,
getLocalIndexHTML(jsContextScript) {
const manifestFile = getWebBuildManifest()
return getIndexHTML({ manifestFile, jsContextScript })
},
})
return {
// react-refresh plugin triggers page reload if needed.
liveReload: false,
allowedHosts: 'all',
hot: true,
historyApiFallback: {
disableDotRule: true,
},
port: SOURCEGRAPH_HTTP_PORT,
client: {
overlay: false,
webSocketTransport: 'ws',
logging: 'verbose',
webSocketURL: {
port: SOURCEGRAPH_HTTPS_PORT,
protocol: 'wss',
},
},
static: {
directory: STATIC_ASSETS_PATH,
publicPath: [STATIC_ASSETS_URL, '/'],
},
proxy: [
{
context: proxyRoutes,
...proxyConfig,
},
],
// Disable default DevServer compression. We need more fine grained compression to support streaming search.
compress: false,
setupMiddlewares: (middlewares, developmentServer) => {
// Re-enable gzip compression using our own `compression` filter.
developmentServer.app!.use(compression({ filter: shouldCompressResponse }))
return middlewares
},
}
}

View File

@ -1,140 +1,39 @@
import compression from 'compression'
import { createProxyMiddleware } from 'http-proxy-middleware'
import { once } from 'lodash'
import signale from 'signale'
import createWebpackCompiler, { type Configuration } from 'webpack'
import WebpackDevServer from 'webpack-dev-server'
import { STATIC_ASSETS_PATH } from '@sourcegraph/build-config'
import { getManifest } from '../esbuild/manifestPlugin'
import { esbuildDevelopmentServer } from '../esbuild/server'
import {
ENVIRONMENT_CONFIG,
getAPIProxySettings,
getIndexHTML,
getWebBuildManifest,
HTTP_WEB_SERVER_URL,
HTTPS_WEB_SERVER_URL,
printSuccessBanner,
shouldCompressResponse,
STATIC_ASSETS_URL,
} from '../utils'
import { ENVIRONMENT_CONFIG, getAPIProxySettings, getIndexHTML, getWebBuildManifest } from '../utils'
// TODO: migrate webpack.config.js to TS to use `import` in this file.
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const webpackConfig = require('../../webpack.config') as Configuration
const { SOURCEGRAPH_API_URL, SOURCEGRAPH_HTTPS_PORT, SOURCEGRAPH_HTTP_PORT } = ENVIRONMENT_CONFIG
const { SOURCEGRAPH_API_URL, SOURCEGRAPH_HTTP_PORT } = ENVIRONMENT_CONFIG
interface DevelopmentServerInit {
apiURL: string
}
async function startDevelopmentServer(): Promise<void> {
signale.start(`Starting ${ENVIRONMENT_CONFIG.DEV_WEB_BUILDER} dev server.`, ENVIRONMENT_CONFIG)
signale.start('Starting dev server.', ENVIRONMENT_CONFIG)
if (!SOURCEGRAPH_API_URL) {
throw new Error('development.server.ts only supports *web-standalone* usage')
}
const init: DevelopmentServerInit = {
await startEsbuildDevelopmentServer({
apiURL: SOURCEGRAPH_API_URL,
}
switch (ENVIRONMENT_CONFIG.DEV_WEB_BUILDER) {
case 'webpack':
await startWebpackDevelopmentServer(init)
break
case 'esbuild':
await startEsbuildDevelopmentServer(init)
break
}
}
async function startWebpackDevelopmentServer({ apiURL }: DevelopmentServerInit): Promise<void> {
const compiler = createWebpackCompiler(webpackConfig)
const { proxyRoutes, ...proxyConfig } = getAPIProxySettings({
apiURL,
getLocalIndexHTML(jsContextScript) {
const manifestFile = getWebBuildManifest()
return getIndexHTML({ manifestFile, jsContextScript })
},
})
const developmentServerConfig: WebpackDevServer.Configuration = {
// react-refresh plugin triggers page reload if needed.
liveReload: false,
allowedHosts: 'all',
hot: true,
historyApiFallback: {
disableDotRule: true,
},
port: SOURCEGRAPH_HTTP_PORT,
client: {
overlay: false,
webSocketTransport: 'ws',
logging: 'verbose',
webSocketURL: {
port: SOURCEGRAPH_HTTPS_PORT,
protocol: 'wss',
},
},
static: {
directory: STATIC_ASSETS_PATH,
publicPath: [STATIC_ASSETS_URL, '/'],
},
proxy: [
{
context: proxyRoutes,
...proxyConfig,
},
],
// Disable default DevServer compression. We need more fine grained compression to support streaming search.
compress: false,
setupMiddlewares: (middlewares, developmentServer) => {
// Re-enable gzip compression using our own `compression` filter.
developmentServer.app!.use(compression({ filter: shouldCompressResponse }))
return middlewares
},
}
const server = new WebpackDevServer(developmentServerConfig, compiler)
compiler.hooks.done.tap(
'development-server-logger',
once(() => {
printSuccessBanner(
[
'Webpack build is ready!',
`Development HTTP server is ready at ${HTTP_WEB_SERVER_URL}`,
`Development HTTPS server is ready at ${HTTPS_WEB_SERVER_URL}`,
],
signale.log.bind(signale)
)
})
)
await server.start()
}
async function startEsbuildDevelopmentServer({ apiURL }: DevelopmentServerInit): Promise<void> {
const manifestFile = getManifest()
const htmlPage = getIndexHTML({ manifestFile })
const { proxyRoutes, ...proxyMiddlewareOptions } = getAPIProxySettings({
apiURL,
getLocalIndexHTML(jsContextScript) {
return getIndexHTML({ manifestFile, jsContextScript })
return getIndexHTML({ manifestFile: getWebBuildManifest(), jsContextScript })
},
})
await esbuildDevelopmentServer({ host: '0.0.0.0', port: SOURCEGRAPH_HTTP_PORT }, app => {
app.use(createProxyMiddleware(proxyRoutes, proxyMiddlewareOptions))
app.get(/.*/, (_request, response) => {
response.send(htmlPage)
response.send(getIndexHTML({ manifestFile: getWebBuildManifest() }))
})
})
}

View File

@ -2,15 +2,6 @@ import path from 'path'
import { ROOT_PATH } from '@sourcegraph/build-config'
export const STATIC_ASSETS_URL = '/.assets/'
export const DEV_SERVER_LISTEN_ADDR = { host: '127.0.0.1', port: 3080 } as const
export const DEV_SERVER_PROXY_TARGET_ADDR = { host: '127.0.0.1', port: 3081 } as const
export const DEFAULT_SITE_CONFIG_PATH = path.resolve(ROOT_PATH, '../dev-private/enterprise/dev/site-config.json')
export const WEBPACK_STATS_OPTIONS = {
all: false,
timings: true,
errors: true,
warnings: true,
colors: true,
} as const

View File

@ -8,9 +8,7 @@ import { getEnvironmentBoolean, STATIC_ASSETS_PATH } from '@sourcegraph/build-co
import { DEFAULT_SITE_CONFIG_PATH } from './constants'
type WEB_BUILDER = 'esbuild' | 'webpack'
const NODE_ENV = process.env.NODE_ENV || 'development'
const NODE_ENV = process.env.NODE_ENV || (process.env.BAZEL_BIN ? 'production' : 'development')
const NODE_DEBUG = process.env.NODE_DEBUG
@ -31,21 +29,8 @@ export const ENVIRONMENT_CONFIG = {
// Can be used to expose global variables to integration tests (e.g., CodeMirror API).
// Enabled in the dev environment to allow debugging integration tests with the dev server.
INTEGRATION_TESTS: getEnvironmentBoolean('INTEGRATION_TESTS') || IS_DEVELOPMENT,
// Enables `embed` entrypoint.
EMBED_DEVELOPMENT: getEnvironmentBoolean('EMBED_DEVELOPMENT'),
// Should the web builder serve `index.html` with `HTMLWebpackPlugin`.
WEB_BUILDER_SERVE_INDEX: getEnvironmentBoolean('WEB_BUILDER_SERVE_INDEX'),
// Enables `StatoscopeWebpackPlugin` that allows to analyze application bundle.
WEBPACK_BUNDLE_ANALYZER: getEnvironmentBoolean('WEBPACK_BUNDLE_ANALYZER'),
// The name used to generate Statoscope JSON stats and HTML report in the `/ui/assets` folder.
WEBPACK_STATS_NAME: process.env.WEBPACK_STATS_NAME,
// Allow overriding default Webpack naming behavior for debugging.
WEBPACK_USE_NAMED_CHUNKS: getEnvironmentBoolean('WEBPACK_USE_NAMED_CHUNKS'),
// Enables the plugin that write Webpack stats to disk.
WEBPACK_EXPORT_STATS: process.env.WEBPACK_EXPORT_STATS,
// Allow to adjust https://webpack.js.org/configuration/devtool/ in the dev environment.
WEBPACK_DEVELOPMENT_DEVTOOL: process.env.WEBPACK_DEVELOPMENT_DEVTOOL || 'eval-cheap-module-source-map',
STATIC_ASSETS_PATH: process.env.STATIC_ASSETS_PATH || STATIC_ASSETS_PATH,
// The commit SHA the client bundle was built with.
@ -61,10 +46,6 @@ export const ENVIRONMENT_CONFIG = {
// Sentry project
SENTRY_PROJECT: process.env.SENTRY_PROJECT,
// Webpack is the default web build tool, and esbuild is an experimental option (see
// https://docs.sourcegraph.com/dev/background-information/web/build#esbuild).
DEV_WEB_BUILDER: (process.env.DEV_WEB_BUILDER === 'esbuild' ? 'esbuild' : 'webpack') as WEB_BUILDER,
/**
* Omit slow deps (such as Monaco and GraphiQL) in the build to get a ~40% reduction in esbuild
* rebuild time. The web app will show placeholders if features needing these deps are used.

View File

@ -1,37 +1,19 @@
import { readFileSync, writeFileSync } from 'fs'
import { readFileSync } from 'fs'
import path from 'path'
import type { WebpackPluginFunction } from 'webpack'
import type { SourcegraphContext } from '../../src/jscontext'
import { assetPathPrefix, WEB_BUILD_MANIFEST_FILENAME, type WebBuildManifest } from '../esbuild/manifest'
import { createJsContext, ENVIRONMENT_CONFIG, HTTPS_WEB_SERVER_URL, STATIC_INDEX_PATH } from '.'
import { createJsContext, ENVIRONMENT_CONFIG, HTTPS_WEB_SERVER_URL } from '.'
const { NODE_ENV, STATIC_ASSETS_PATH } = ENVIRONMENT_CONFIG
const { STATIC_ASSETS_PATH } = ENVIRONMENT_CONFIG
export const WEB_BUILD_MANIFEST_PATH = path.resolve(STATIC_ASSETS_PATH, 'web.manifest.json')
const WEB_BUILD_MANIFEST_PATH = path.resolve(STATIC_ASSETS_PATH, WEB_BUILD_MANIFEST_FILENAME)
export const HTML_INDEX_PATH = path.resolve(STATIC_ASSETS_PATH, 'index.html')
export const getWebBuildManifest = (): WebBuildManifest =>
JSON.parse(readFileSync(WEB_BUILD_MANIFEST_PATH, 'utf-8')) as WebBuildManifest
export interface WebBuildManifest {
/** Main app entry JS bundle */
'app.js': string
/** Main app entry CSS bundle, only used in production mode */
'app.css'?: string
/** Runtime bundle, only used in development mode */
'runtime.js'?: string
/** React entry bundle, only used in production mode */
'react.js'?: string
/** Opentelemetry entry bundle, only used in production mode */
'opentelemetry.js'?: string
/** If script files should be treated as JS modules. Required for esbuild bundle. */
isModule?: boolean
/** The node env value `production | development` */
environment?: 'development' | 'production'
}
interface GetHTMLPageOptions {
manifestFile: WebBuildManifest
/**
@ -54,14 +36,7 @@ interface GetHTMLPageOptions {
export function getIndexHTML(options: GetHTMLPageOptions): string {
const { manifestFile, jsContext, jsContextScript } = options
const {
'app.js': appBundle,
'app.css': cssBundle,
'runtime.js': runtimeBundle,
'react.js': reactBundle,
'opentelemetry.js': oTelBundle,
isModule,
} = manifestFile
const { 'main.js': mainJS, 'main.css': mainCSS } = manifestFile
return `
<!DOCTYPE html>
@ -72,7 +47,7 @@ export function getIndexHTML(options: GetHTMLPageOptions): string {
<meta name="viewport" content="width=device-width, viewport-fit=cover" />
<meta name="referrer" content="origin-when-cross-origin"/>
<meta name="color-scheme" content="light dark"/>
${cssBundle ? `<link rel="stylesheet" href="${cssBundle}">` : ''}
<link rel="stylesheet" href="${assetPathPrefix}/${mainCSS}">
${
ENVIRONMENT_CONFIG.SOURCEGRAPHDOTCOM_MODE
? '<script src="https://js.sentry-cdn.com/ae2f74442b154faf90b5ff0f7cd1c618.min.js" crossorigin="anonymous"></script>'
@ -82,9 +57,6 @@ export function getIndexHTML(options: GetHTMLPageOptions): string {
<body>
<div id="root"></div>
<script>
// Optional value useful for checking if index.html is created by HtmlWebpackPlugin with the right NODE_ENV.
window.webpackBuildEnvironment = '${NODE_ENV}'
${
jsContextScript ||
`
@ -96,17 +68,8 @@ export function getIndexHTML(options: GetHTMLPageOptions): string {
}
</script>
${runtimeBundle ? `<script src="${runtimeBundle}"></script>` : ''}
${reactBundle ? `<script src="${reactBundle}" ${isModule ? 'type="module"' : ''}></script>` : ''}
${oTelBundle ? `<script src="${oTelBundle}" ${isModule ? 'type="module"' : ''}></script>` : ''}
<script src="${appBundle}" ${isModule ? 'type="module"' : ''}></script>
<script src="${assetPathPrefix}/${mainJS}" type="module"></script>
</body>
</html>
`
}
export const writeIndexHTMLPlugin: WebpackPluginFunction = compiler => {
compiler.hooks.done.tap('WriteIndexHTMLPlugin', () => {
writeFileSync(STATIC_INDEX_PATH, getIndexHTML({ manifestFile: getWebBuildManifest() }), 'utf-8')
})
}

View File

@ -2,10 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
go_library(
name = "enterprise",
name = "dist",
srcs = ["assets.go"],
embedsrcs = ["copy_bundle"], # keep
importpath = "github.com/sourcegraph/sourcegraph/ui/assets/enterprise",
importpath = "github.com/sourcegraph/sourcegraph/client/web/dist",
visibility = ["//visibility:public"],
deps = [
"//lib/errors",
@ -20,13 +20,13 @@ copy_to_directory(
"//client/browser:integration-assets",
"//client/web:bundle",
"//client/web-sveltekit",
"//ui/assets/img",
"//client/web/dist/img",
],
out = "dist",
replace_prefixes = {
"client/web/bundle": "",
"client/web-sveltekit/build": "",
"ui/assets/img": "img",
"client/web/dist/img": "img",
"client/browser/integration-assets": "",
},
)

View File

@ -1,4 +1,4 @@
package enterprise
package dist
import (
"embed"

View File

@ -8,6 +8,6 @@ filegroup(
copy_to_bin(
name = "copy",
srcs = ["//ui/assets/img"],
srcs = ["//client/web/dist/img"],
visibility = ["//client/web-sveltekit:__pkg__"],
)

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 539 KiB

After

Width:  |  Height:  |  Size: 539 KiB

View File

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1010 B

After

Width:  |  Height:  |  Size: 1010 B

View File

Before

Width:  |  Height:  |  Size: 1007 B

After

Width:  |  Height:  |  Size: 1007 B

View File

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 840 B

After

Width:  |  Height:  |  Size: 840 B

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Some files were not shown because too many files have changed in this diff Show More