25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

482 lines
12 KiB

  1. //@ts-check
  2. /** @typedef {import('webpack').Configuration} WebpackConfig **/
  3. /* eslint-disable import/extensions */
  4. /* eslint-disable import/no-dynamic-require */
  5. /* eslint-disable @typescript-eslint/ban-ts-comment */
  6. /* eslint-disable @typescript-eslint/no-var-requires */
  7. /* eslint-disable @typescript-eslint/strict-boolean-expressions */
  8. /* eslint-disable @typescript-eslint/prefer-optional-chain */
  9. 'use strict';
  10. const path = require('path');
  11. const CircularDependencyPlugin = require('circular-dependency-plugin');
  12. const { CleanWebpackPlugin: CleanPlugin } = require('clean-webpack-plugin');
  13. const CopyPlugin = require('copy-webpack-plugin');
  14. const CspHtmlPlugin = require('csp-html-webpack-plugin');
  15. const esbuild = require('esbuild');
  16. const { ESBuildMinifyPlugin } = require('esbuild-loader');
  17. const ForkTsCheckerPlugin = require('fork-ts-checker-webpack-plugin');
  18. const HtmlPlugin = require('html-webpack-plugin');
  19. const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
  20. const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  21. const TerserPlugin = require('terser-webpack-plugin');
  22. const { WebpackError } = require('webpack');
  23. const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
  24. class InlineChunkHtmlPlugin {
  25. constructor(htmlPlugin, patterns) {
  26. this.htmlPlugin = htmlPlugin;
  27. this.patterns = patterns;
  28. }
  29. getInlinedTag(publicPath, assets, tag) {
  30. if (
  31. (tag.tagName !== 'script' || !(tag.attributes && tag.attributes.src)) &&
  32. (tag.tagName !== 'link' || !(tag.attributes && tag.attributes.href))
  33. ) {
  34. return tag;
  35. }
  36. let chunkName = tag.tagName === 'link' ? tag.attributes.href : tag.attributes.src;
  37. if (publicPath) {
  38. chunkName = chunkName.replace(publicPath, '');
  39. }
  40. if (!this.patterns.some(pattern => chunkName.match(pattern))) {
  41. return tag;
  42. }
  43. const asset = assets[chunkName];
  44. if (asset == null) {
  45. return tag;
  46. }
  47. return { tagName: tag.tagName === 'link' ? 'style' : tag.tagName, innerHTML: asset.source(), closeTag: true };
  48. }
  49. apply(compiler) {
  50. let publicPath = compiler.options.output.publicPath || '';
  51. if (publicPath && !publicPath.endsWith('/')) {
  52. publicPath += '/';
  53. }
  54. compiler.hooks.compilation.tap('InlineChunkHtmlPlugin', compilation => {
  55. const getInlinedTagFn = tag => this.getInlinedTag(publicPath, compilation.assets, tag);
  56. this.htmlPlugin.getHooks(compilation).alterAssetTagGroups.tap('InlineChunkHtmlPlugin', assets => {
  57. assets.headTags = assets.headTags.map(getInlinedTagFn);
  58. assets.bodyTags = assets.bodyTags.map(getInlinedTagFn);
  59. });
  60. });
  61. }
  62. }
  63. module.exports =
  64. /**
  65. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; } | undefined } env
  66. * @param {{ mode: 'production' | 'development' | 'none' | undefined; }} argv
  67. * @returns { WebpackConfig[] }
  68. */
  69. function (env, argv) {
  70. const mode = argv.mode || 'none';
  71. env = {
  72. analyzeBundle: false,
  73. analyzeDeps: false,
  74. esbuild: true,
  75. ...env,
  76. };
  77. return [getExtensionConfig(mode, env), getWebviewsConfig(mode, env)];
  78. };
  79. /**
  80. * @param { 'production' | 'development' | 'none' } mode
  81. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; }} env
  82. * @returns { WebpackConfig }
  83. */
  84. function getExtensionConfig(mode, env) {
  85. /**
  86. * @type WebpackConfig['plugins'] | any
  87. */
  88. const plugins = [
  89. new CleanPlugin({ cleanOnceBeforeBuildPatterns: ['!webviews/**'] }),
  90. new ForkTsCheckerPlugin({
  91. async: false,
  92. eslint: { enabled: true, files: 'src/**/*.ts', options: { cache: true } },
  93. formatter: 'basic',
  94. }),
  95. ];
  96. if (env.analyzeDeps) {
  97. plugins.push(
  98. new CircularDependencyPlugin({
  99. cwd: __dirname,
  100. exclude: /node_modules/,
  101. failOnError: false,
  102. onDetected: function ({ module: _webpackModuleRecord, paths, compilation }) {
  103. if (paths.some(p => p.includes('container.ts'))) return;
  104. // @ts-ignore
  105. compilation.warnings.push(new WebpackError(paths.join(' -> ')));
  106. },
  107. }),
  108. );
  109. }
  110. if (env.analyzeBundle) {
  111. plugins.push(new BundleAnalyzerPlugin());
  112. }
  113. return {
  114. name: 'extension',
  115. entry: './src/extension.ts',
  116. mode: mode,
  117. target: 'node',
  118. node: {
  119. __dirname: false,
  120. },
  121. devtool: 'source-map',
  122. output: {
  123. path: path.join(__dirname, 'dist'),
  124. libraryTarget: 'commonjs2',
  125. filename: 'gitlens.js',
  126. chunkFilename: 'feature-[name].js',
  127. },
  128. optimization: {
  129. minimizer: [
  130. env.esbuild
  131. ? new ESBuildMinifyPlugin({
  132. format: 'cjs',
  133. // @ts-ignore
  134. implementation: esbuild,
  135. minify: true,
  136. treeShaking: true,
  137. // Keep the class names otherwise @log won't provide a useful name
  138. keepNames: true,
  139. target: 'es2019',
  140. })
  141. : new TerserPlugin({
  142. extractComments: false,
  143. parallel: true,
  144. terserOptions: {
  145. ecma: 2019,
  146. // Keep the class names otherwise @log won't provide a useful name
  147. keep_classnames: true,
  148. module: true,
  149. },
  150. }),
  151. ],
  152. splitChunks: {
  153. cacheGroups: {
  154. defaultVendors: false,
  155. },
  156. },
  157. },
  158. externals: {
  159. vscode: 'commonjs vscode',
  160. },
  161. module: {
  162. rules: [
  163. {
  164. exclude: /\.d\.ts$/,
  165. include: path.join(__dirname, 'src'),
  166. test: /\.tsx?$/,
  167. use: env.esbuild
  168. ? {
  169. loader: 'esbuild-loader',
  170. options: {
  171. implementation: esbuild,
  172. loader: 'ts',
  173. target: 'es2019',
  174. tsconfigRaw: require('./tsconfig.json'),
  175. },
  176. }
  177. : {
  178. loader: 'ts-loader',
  179. options: {
  180. experimentalWatchApi: true,
  181. transpileOnly: true,
  182. },
  183. },
  184. },
  185. ],
  186. },
  187. resolve: {
  188. alias: {
  189. 'universal-user-agent': path.join(
  190. __dirname,
  191. 'node_modules',
  192. 'universal-user-agent',
  193. 'dist-node',
  194. 'index.js',
  195. ),
  196. },
  197. extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  198. symlinks: false,
  199. },
  200. plugins: plugins,
  201. stats: {
  202. preset: 'errors-warnings',
  203. assets: true,
  204. colors: true,
  205. env: true,
  206. errorsCount: true,
  207. warningsCount: true,
  208. timings: true,
  209. },
  210. };
  211. }
  212. /**
  213. * @param { 'production' | 'development' | 'none' } mode
  214. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; }} env
  215. * @returns { WebpackConfig }
  216. */
  217. function getWebviewsConfig(mode, env) {
  218. const basePath = path.join(__dirname, 'src', 'webviews', 'apps');
  219. const cspHtmlPlugin = new CspHtmlPlugin(
  220. {
  221. 'default-src': "'none'",
  222. 'img-src': ['#{cspSource}', 'https:', 'data:'],
  223. 'script-src':
  224. mode !== 'production'
  225. ? ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-eval'"]
  226. : ['#{cspSource}', "'nonce-#{cspNonce}'"],
  227. 'style-src': ['#{cspSource}', "'nonce-#{cspNonce}'"],
  228. 'font-src': ['#{cspSource}'],
  229. },
  230. {
  231. enabled: true,
  232. hashingMethod: 'sha256',
  233. hashEnabled: {
  234. 'script-src': true,
  235. 'style-src': true,
  236. },
  237. nonceEnabled: {
  238. 'script-src': true,
  239. 'style-src': true,
  240. },
  241. },
  242. );
  243. // Override the nonce creation so we can dynamically generate them at runtime
  244. // @ts-ignore
  245. cspHtmlPlugin.createNonce = () => '#{cspNonce}';
  246. /**
  247. * @type WebpackConfig['plugins'] | any
  248. */
  249. const plugins = [
  250. new CleanPlugin(
  251. mode === 'production'
  252. ? {
  253. cleanOnceBeforeBuildPatterns: [
  254. path.posix.join(__dirname.replace(/\\/g, '/'), 'images', 'settings', '**'),
  255. ],
  256. dangerouslyAllowCleanPatternsOutsideProject: true,
  257. dry: false,
  258. }
  259. : undefined,
  260. ),
  261. new ForkTsCheckerPlugin({
  262. async: false,
  263. eslint: {
  264. enabled: true,
  265. files: path.join(basePath, '**', '*.ts'),
  266. options: { cache: true },
  267. },
  268. formatter: 'basic',
  269. typescript: {
  270. configFile: path.join(basePath, 'tsconfig.json'),
  271. },
  272. }),
  273. new MiniCssExtractPlugin({
  274. filename: '[name].css',
  275. }),
  276. new HtmlPlugin({
  277. template: 'rebase/rebase.html',
  278. chunks: ['rebase'],
  279. filename: path.join(__dirname, 'dist', 'webviews', 'rebase.html'),
  280. inject: true,
  281. inlineSource: mode === 'production' ? '.css$' : undefined,
  282. minify:
  283. mode === 'production'
  284. ? {
  285. removeComments: true,
  286. collapseWhitespace: true,
  287. removeRedundantAttributes: false,
  288. useShortDoctype: true,
  289. removeEmptyAttributes: true,
  290. removeStyleLinkTypeAttributes: true,
  291. keepClosingSlash: true,
  292. minifyCSS: true,
  293. }
  294. : false,
  295. }),
  296. new HtmlPlugin({
  297. template: 'settings/settings.html',
  298. chunks: ['settings'],
  299. filename: path.join(__dirname, 'dist', 'webviews', 'settings.html'),
  300. inject: true,
  301. inlineSource: mode === 'production' ? '.css$' : undefined,
  302. minify:
  303. mode === 'production'
  304. ? {
  305. removeComments: true,
  306. collapseWhitespace: true,
  307. removeRedundantAttributes: false,
  308. useShortDoctype: true,
  309. removeEmptyAttributes: true,
  310. removeStyleLinkTypeAttributes: true,
  311. keepClosingSlash: true,
  312. minifyCSS: true,
  313. }
  314. : false,
  315. }),
  316. new HtmlPlugin({
  317. template: 'welcome/welcome.html',
  318. chunks: ['welcome'],
  319. filename: path.join(__dirname, 'dist', 'webviews', 'welcome.html'),
  320. inject: true,
  321. inlineSource: mode === 'production' ? '.css$' : undefined,
  322. minify:
  323. mode === 'production'
  324. ? {
  325. removeComments: true,
  326. collapseWhitespace: true,
  327. removeRedundantAttributes: false,
  328. useShortDoctype: true,
  329. removeEmptyAttributes: true,
  330. removeStyleLinkTypeAttributes: true,
  331. keepClosingSlash: true,
  332. minifyCSS: true,
  333. }
  334. : false,
  335. }),
  336. cspHtmlPlugin,
  337. new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []),
  338. new CopyPlugin({
  339. patterns: [
  340. {
  341. from: path.posix.join(basePath.replace(/\\/g, '/'), 'images', 'settings', '*.png'),
  342. to: __dirname.replace(/\\/g, '/'),
  343. },
  344. {
  345. from: path.posix.join(
  346. __dirname.replace(/\\/g, '/'),
  347. 'node_modules',
  348. '@vscode',
  349. 'codicons',
  350. 'dist',
  351. 'codicon.ttf',
  352. ),
  353. to: path.posix.join(__dirname.replace(/\\/g, '/'), 'dist', 'webviews'),
  354. },
  355. ],
  356. }),
  357. new ImageMinimizerPlugin({
  358. test: /\.(png)$/i,
  359. filename: '[path][name].webp',
  360. loader: false,
  361. deleteOriginalAssets: true,
  362. minimizerOptions: {
  363. plugins: [
  364. [
  365. 'imagemin-webp',
  366. {
  367. lossless: true,
  368. nearLossless: 0,
  369. quality: 100,
  370. method: mode === 'production' ? 4 : 0,
  371. },
  372. ],
  373. ],
  374. },
  375. }),
  376. ];
  377. return {
  378. name: 'webviews',
  379. context: basePath,
  380. entry: {
  381. rebase: './rebase/rebase.ts',
  382. settings: './settings/settings.ts',
  383. welcome: './welcome/welcome.ts',
  384. },
  385. mode: mode,
  386. target: 'web',
  387. devtool: 'source-map',
  388. output: {
  389. filename: '[name].js',
  390. path: path.join(__dirname, 'dist', 'webviews'),
  391. publicPath: '#{root}/dist/webviews/',
  392. },
  393. module: {
  394. rules: [
  395. {
  396. exclude: /\.d\.ts$/,
  397. include: path.join(__dirname, 'src'),
  398. test: /\.tsx?$/,
  399. use: env.esbuild
  400. ? {
  401. loader: 'esbuild-loader',
  402. options: {
  403. implementation: esbuild,
  404. loader: 'ts',
  405. target: 'es2019',
  406. tsconfigRaw: require(path.join(basePath, 'tsconfig.json')),
  407. },
  408. }
  409. : {
  410. loader: 'ts-loader',
  411. options: {
  412. configFile: path.join(basePath, 'tsconfig.json'),
  413. experimentalWatchApi: true,
  414. transpileOnly: true,
  415. },
  416. },
  417. },
  418. {
  419. test: /\.scss$/,
  420. use: [
  421. {
  422. loader: MiniCssExtractPlugin.loader,
  423. },
  424. {
  425. loader: 'css-loader',
  426. options: {
  427. sourceMap: true,
  428. url: false,
  429. },
  430. },
  431. {
  432. loader: 'sass-loader',
  433. options: {
  434. sourceMap: true,
  435. },
  436. },
  437. ],
  438. exclude: /node_modules/,
  439. },
  440. ],
  441. },
  442. resolve: {
  443. extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  444. modules: [basePath, 'node_modules'],
  445. symlinks: false,
  446. },
  447. plugins: plugins,
  448. stats: {
  449. preset: 'errors-warnings',
  450. assets: true,
  451. colors: true,
  452. env: true,
  453. errorsCount: true,
  454. warningsCount: true,
  455. timings: true,
  456. },
  457. infrastructureLogging: {
  458. level: 'log', // enables logging required for problem matchers
  459. },
  460. };
  461. }