選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

615 行
16 KiB

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