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.

745 lines
19 KiB

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