webpack.config.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. const path = require('path');
  4. const fs = require('fs-extra');
  5. const Handlebars = require('handlebars');
  6. const Build = require('@jupyterlab/builder').Build;
  7. const webpack = require('webpack');
  8. const merge = require('webpack-merge').default;
  9. const baseConfig = require('@jupyterlab/builder/lib/webpack.config.base');
  10. const { ModuleFederationPlugin } = webpack.container;
  11. const packageData = require('./package.json');
  12. const jlab = packageData.jupyterlab;
  13. // Create a list of application extensions and mime extensions from
  14. // jlab.extensions
  15. const extensions = {};
  16. const mimeExtensions = {};
  17. for (const key of jlab.extensions) {
  18. const {
  19. jupyterlab: { extension, mimeExtension }
  20. } = require(`${key}/package.json`);
  21. if (extension !== undefined) {
  22. extensions[key] = extension === true ? '' : extension;
  23. }
  24. if (mimeExtension !== undefined) {
  25. mimeExtensions[key] = mimeExtension === true ? '' : mimeExtension;
  26. }
  27. }
  28. // buildDir is a temporary directory where files are copied before the build.
  29. const buildDir = path.resolve(jlab.buildDir);
  30. fs.emptyDirSync(buildDir);
  31. // outputDir is where the final built assets go
  32. const outputDir = path.resolve(jlab.outputDir);
  33. fs.emptyDirSync(outputDir);
  34. // <schemaDir>/schemas is where the settings schemas live
  35. const schemaDir = path.resolve(jlab.schemaDir || outputDir);
  36. // ensureAssets puts schemas in the schemas subdirectory
  37. fs.emptyDirSync(path.join(schemaDir, 'schemas'));
  38. // <themeDir>/themes is where theme assets live
  39. const themeDir = path.resolve(jlab.themeDir || outputDir);
  40. // ensureAssets puts themes in the themes subdirectory
  41. fs.emptyDirSync(path.join(themeDir, 'themes'));
  42. // Configuration to handle extension assets
  43. const extensionAssetConfig = Build.ensureAssets({
  44. packageNames: jlab.extensions,
  45. output: buildDir,
  46. schemaOutput: schemaDir,
  47. themeOutput: themeDir
  48. });
  49. // Create the entry point and other assets in build directory.
  50. const template = Handlebars.compile(
  51. fs.readFileSync('index.template.js').toString()
  52. );
  53. fs.writeFileSync(
  54. path.join(buildDir, 'index.js'),
  55. template({ extensions, mimeExtensions })
  56. );
  57. // Create the bootstrap file that loads federated extensions and calls the
  58. // initialization logic in index.js
  59. const entryPoint = path.join(buildDir, 'bootstrap.js');
  60. fs.copySync('./bootstrap.js', entryPoint);
  61. /**
  62. * Create the webpack ``shared`` configuration
  63. */
  64. function createShared(packageData) {
  65. // Set up module federation sharing config
  66. const shared = {};
  67. const extensionPackages = packageData.jupyterlab.extensions;
  68. // Make sure any resolutions are shared
  69. for (let [pkg, requiredVersion] of Object.entries(packageData.resolutions)) {
  70. shared[pkg] = { requiredVersion };
  71. }
  72. // Add any extension packages that are not in resolutions (i.e., installed from npm)
  73. for (let pkg of extensionPackages) {
  74. if (!shared[pkg]) {
  75. shared[pkg] = {
  76. requiredVersion: require(`${pkg}/package.json`).version
  77. };
  78. }
  79. }
  80. // Add dependencies and sharedPackage config from extension packages if they
  81. // are not already in the shared config. This means that if there is a
  82. // conflict, the resolutions package version is the one that is shared.
  83. const extraShared = [];
  84. for (let pkg of extensionPackages) {
  85. let pkgShared = {};
  86. let {
  87. dependencies = {},
  88. jupyterlab: { sharedPackages = {} } = {}
  89. } = require(`${pkg}/package.json`);
  90. for (let [dep, requiredVersion] of Object.entries(dependencies)) {
  91. if (!shared[dep]) {
  92. pkgShared[dep] = { requiredVersion };
  93. }
  94. }
  95. // Overwrite automatic dependency sharing with custom sharing config
  96. for (let [dep, config] of Object.entries(sharedPackages)) {
  97. if (config === false) {
  98. delete pkgShared[dep];
  99. } else {
  100. if ('bundled' in config) {
  101. config.import = config.bundled;
  102. delete config.bundled;
  103. }
  104. pkgShared[dep] = config;
  105. }
  106. }
  107. extraShared.push(pkgShared);
  108. }
  109. // Now merge the extra shared config
  110. const mergedShare = {};
  111. for (let sharedConfig of extraShared) {
  112. for (let [pkg, config] of Object.entries(sharedConfig)) {
  113. // Do not override the basic share config from resolutions
  114. if (shared[pkg]) {
  115. continue;
  116. }
  117. // Add if we haven't seen the config before
  118. if (!mergedShare[pkg]) {
  119. mergedShare[pkg] = config;
  120. continue;
  121. }
  122. // Choose between the existing config and this new config. We do not try
  123. // to merge configs, which may yield a config no one wants
  124. let oldConfig = mergedShare[pkg];
  125. // if the old one has import: false, use the new one
  126. if (oldConfig.import === false) {
  127. mergedShare[pkg] = config;
  128. }
  129. }
  130. }
  131. Object.assign(shared, mergedShare);
  132. // Transform any file:// requiredVersion to the version number from the
  133. // imported package. This assumes (for simplicity) that the version we get
  134. // importing was installed from the file.
  135. for (let [pkg, { requiredVersion }] of Object.entries(shared)) {
  136. if (requiredVersion && requiredVersion.startsWith('file:')) {
  137. shared[pkg].requiredVersion = require(`${pkg}/package.json`).version;
  138. }
  139. }
  140. // Add singleton package information
  141. for (let pkg of packageData.jupyterlab.singletonPackages) {
  142. shared[pkg].singleton = true;
  143. }
  144. return shared;
  145. }
  146. const plugins = [
  147. new ModuleFederationPlugin({
  148. library: {
  149. type: 'var',
  150. name: ['_JUPYTERLAB', 'CORE_LIBRARY_FEDERATION']
  151. },
  152. name: 'CORE_FEDERATION',
  153. shared: createShared(packageData)
  154. })
  155. ];
  156. module.exports = [
  157. merge(baseConfig, {
  158. mode: 'development',
  159. devtool: 'source-map',
  160. entry: ['./publicpath.js', entryPoint],
  161. output: {
  162. path: path.resolve(outputDir),
  163. library: {
  164. type: 'var',
  165. name: ['_JUPYTERLAB', 'CORE_OUTPUT']
  166. },
  167. filename: 'bundle.js'
  168. },
  169. optimization: {
  170. splitChunks: {
  171. chunks: 'all',
  172. cacheGroups: {
  173. jlab_core: {
  174. test: /[\\/](node_modules[\\/]@(jupyterlab|lumino)|packages)[\\/]/,
  175. name: 'jlab_core'
  176. }
  177. }
  178. }
  179. },
  180. plugins
  181. })
  182. ].concat(extensionAssetConfig);
  183. // For debugging, write the config out
  184. fs.writeFileSync(
  185. path.join(buildDir, 'webpack.config-log.json'),
  186. JSON.stringify(module.exports, null, ' ')
  187. );