You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

562 line
14 KiB

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