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.

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