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.

778 line
20 KiB

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