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.

781 lines
20 KiB

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