extensionConfig.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import * as path from 'path';
  4. import * as webpack from 'webpack';
  5. import { Build } from './build';
  6. import { merge } from 'webpack-merge';
  7. import * as fs from 'fs-extra';
  8. import * as glob from 'glob';
  9. import Ajv from 'ajv';
  10. import { readJSONFile, writeJSONFile } from '@jupyterlab/buildutils';
  11. const baseConfig = require('./webpack.config.base');
  12. const { ModuleFederationPlugin } = webpack.container;
  13. export interface IOptions {
  14. packagePath?: string;
  15. corePath?: string;
  16. staticUrl?: string;
  17. mode?: 'development' | 'production';
  18. devtool?: string;
  19. watchMode?: boolean;
  20. }
  21. function generateConfig({
  22. packagePath = '',
  23. corePath = '',
  24. staticUrl = '',
  25. mode = 'production',
  26. devtool = mode === 'development' ? 'source-map' : undefined,
  27. watchMode = false
  28. }: IOptions = {}): webpack.Configuration[] {
  29. const data = require(path.join(packagePath, 'package.json'));
  30. const ajv = new Ajv({ useDefaults: true });
  31. const validate = ajv.compile(require('../metadata_schema.json'));
  32. let valid = validate(data.jupyterlab ?? {});
  33. if (!valid) {
  34. console.error(validate.errors);
  35. process.exit(1);
  36. }
  37. let outputPath = data.jupyterlab['outputDir'];
  38. outputPath = path.join(packagePath, outputPath);
  39. // Handle the extension entry point and the lib entry point, if different
  40. const index = require.resolve(packagePath);
  41. const exposes: { [id: string]: string } = {
  42. './index': index
  43. };
  44. if (data.jupyterlab.extension === true) {
  45. exposes['./extension'] = index;
  46. } else if (typeof data.jupyterlab.extension === 'string') {
  47. exposes['./extension'] = path.join(packagePath, data.jupyterlab.extension);
  48. }
  49. if (data.jupyterlab.mimeExtension === true) {
  50. exposes['./mimeExtension'] = index;
  51. } else if (typeof data.jupyterlab.mimeExtension === 'string') {
  52. exposes['./mimeExtension'] = path.join(
  53. packagePath,
  54. data.jupyterlab.mimeExtension
  55. );
  56. }
  57. if (data.style) {
  58. exposes['./style'] = path.join(packagePath, data.style);
  59. }
  60. const coreData = require(path.join(corePath, 'package.json'));
  61. let shared: any = {};
  62. // Start with core package versions.
  63. const coreDeps: any = {
  64. ...coreData.dependencies,
  65. ...(coreData.resolutions ?? {})
  66. };
  67. // Alow extensions to match a wider range than the core dependency
  68. // To ensure forward compatibility.
  69. Object.keys(coreDeps).forEach(element => {
  70. shared[element] = {
  71. requiredVersion: coreDeps[element].replace('~', '^'),
  72. import: false
  73. };
  74. });
  75. // Add package dependencies.
  76. Object.keys(data.dependencies).forEach(element => {
  77. // TODO: make sure that the core dependency semver range is a subset of our
  78. // data.depencies version range for any packages in the core deps.
  79. if (!shared[element]) {
  80. shared[element] = {};
  81. }
  82. });
  83. // Set core packages as singletons that are not bundled.
  84. coreData.jupyterlab.singletonPackages.forEach((element: string) => {
  85. if (!shared[element]) {
  86. shared[element] = {};
  87. }
  88. shared[element].import = false;
  89. shared[element].singleton = true;
  90. });
  91. // Now we merge in the sharedPackages configuration provided by the extension.
  92. const sharedPackages = data.jupyterlab.sharedPackages ?? {};
  93. // Delete any modules that are explicitly not shared
  94. Object.keys(sharedPackages).forEach(pkg => {
  95. if (sharedPackages[pkg] === false) {
  96. delete shared[pkg];
  97. delete sharedPackages[pkg];
  98. }
  99. });
  100. // Transform the sharedPackages information into valid webpack config
  101. Object.keys(sharedPackages).forEach(pkg => {
  102. // Convert `bundled` to `import`
  103. if (sharedPackages[pkg].bundled === false) {
  104. sharedPackages[pkg].import = false;
  105. } else if (
  106. sharedPackages[pkg].bundled === true &&
  107. shared[pkg]?.import === false
  108. ) {
  109. // We can't delete a key in the merge, so we have to delete it in the source
  110. delete shared[pkg].import;
  111. }
  112. delete sharedPackages[pkg].bundled;
  113. });
  114. shared = merge(shared, sharedPackages);
  115. // add the root module itself to shared
  116. if (shared[data.name]) {
  117. console.error(
  118. `The root package itself '${data.name}' may not specified as a shared dependency.`
  119. );
  120. }
  121. shared[data.name] = {
  122. version: data.version,
  123. singleton: true,
  124. import: index
  125. };
  126. // Ensure a clean output directory - remove files but not the directory
  127. // in case it is a symlink
  128. fs.emptyDirSync(outputPath);
  129. const extras = Build.ensureAssets({
  130. packageNames: [],
  131. packagePaths: [packagePath],
  132. output: outputPath
  133. });
  134. fs.copyFileSync(
  135. path.join(packagePath, 'package.json'),
  136. path.join(outputPath, 'package.json')
  137. );
  138. class CleanupPlugin {
  139. apply(compiler: any) {
  140. compiler.hooks.done.tap('Cleanup', () => {
  141. // Find the remoteEntry file and add it to the package.json metadata
  142. const files = glob.sync(path.join(outputPath, 'remoteEntry.*.js'));
  143. let newestTime = -1;
  144. let newestRemote = '';
  145. files.forEach(fpath => {
  146. const mtime = fs.statSync(fpath).mtime.getTime();
  147. if (mtime > newestTime) {
  148. newestRemote = fpath;
  149. newestTime = mtime;
  150. }
  151. });
  152. const data = readJSONFile(path.join(outputPath, 'package.json'));
  153. const _build: any = {
  154. load: path.basename(newestRemote)
  155. };
  156. if (exposes['./extension'] !== undefined) {
  157. _build.extension = './extension';
  158. }
  159. if (exposes['./mimeExtension'] !== undefined) {
  160. _build.mimeExtension = './mimeExtension';
  161. }
  162. if (exposes['./style'] !== undefined) {
  163. _build.style = './style';
  164. }
  165. data.jupyterlab._build = _build;
  166. writeJSONFile(path.join(outputPath, 'package.json'), data);
  167. });
  168. }
  169. }
  170. const config = [
  171. merge(baseConfig, {
  172. mode,
  173. devtool,
  174. entry: {},
  175. output: {
  176. filename: '[name].[contenthash].js',
  177. path: outputPath,
  178. publicPath: staticUrl || 'auto'
  179. },
  180. module: {
  181. rules: [{ test: /\.html$/, use: 'file-loader' }]
  182. },
  183. plugins: [
  184. new ModuleFederationPlugin({
  185. name: data.name,
  186. library: {
  187. type: 'var',
  188. name: ['_JUPYTERLAB', data.name]
  189. },
  190. filename: 'remoteEntry.[contenthash].js',
  191. exposes,
  192. shared
  193. }),
  194. new CleanupPlugin()
  195. ]
  196. })
  197. ].concat(extras);
  198. if (mode === 'development') {
  199. const logPath = path.join(outputPath, 'build_log.json');
  200. fs.writeFileSync(logPath, JSON.stringify(config, null, ' '));
  201. }
  202. return config;
  203. }
  204. export default generateConfig;