浏览代码

Move module federation container initialization to bootstrap file.

Since index.out.js was directly importing compiled extensions, webpack was loading the dependencies before index.out.js was initialized. Since federated extensions were loaded in index.out.js, they did not get a chance to provide packages for compiled extensions. This was a problem if you had a compiled extension that depended on a package a federated extension would provide (for example, a custom ipywidgets extension as a compiled extension, with the ipywidget manager as a federated extension).

This loads all federated modules and initializes their containers in the bootstrap phase, making their packages available to the system, before loading the index.out.js file that actually uses them.
Jason Grout 4 年之前
父节点
当前提交
da8a4ff1c4
共有 5 个文件被更改,包括 129 次插入75 次删除
  1. 1 0
      buildutils/src/update-core-mode.ts
  2. 115 0
      dev_mode/bootstrap.js
  3. 7 71
      dev_mode/index.js
  4. 5 3
      dev_mode/webpack.config.js
  5. 1 1
      jupyterlab/commands.py

+ 1 - 0
buildutils/src/update-core-mode.ts

@@ -39,6 +39,7 @@ const notice =
 
 [
   'index.js',
+  'boostrap.js',
   'publicpath.js',
   'webpack.config.js',
   'webpack.prod.config.js',

+ 115 - 0
dev_mode/bootstrap.js

@@ -0,0 +1,115 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+// We copy some of the pageconfig parsing logic in @jupyterlab/coreutils
+// below, since this must run before any other files are loaded (including
+// @jupyterlab/coreutils).
+
+/**
+ * Get global configuration data for the Jupyter application.
+ *
+ * @param name - The name of the configuration option.
+ *
+ * @returns The config value or an empty string if not found.
+ *
+ * #### Notes
+ * All values are treated as strings. For browser based applications, it is
+ * assumed that the page HTML includes a script tag with the id
+ * `jupyter-config-data` containing the configuration as valid JSON.
+ */
+let _CONFIG_DATA = null;
+function getOption(name) {
+  if (_CONFIG_DATA === null) {
+    let configData;
+    // Use script tag if available.
+    if (typeof document !== 'undefined' && document) {
+      const el = document.getElementById('jupyter-config-data');
+
+      if (el) {
+        configData = JSON.parse(el.textContent || '{}');
+      }
+    }
+    _CONFIG_DATA = configData ?? Object.create(null);
+  }
+
+  return _CONFIG_DATA[name] || '';
+}
+
+// eslint-disable-next-line no-undef
+__webpack_public_path__ = getOption('fullStaticUrl') + '/';
+
+// Promise.allSettled polyfill, until our supported browsers implement it
+// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
+if (Promise.allSettled === undefined) {
+  Promise.allSettled = promises =>
+    Promise.all(
+      promises.map(promise =>
+        promise.then(
+          value => ({
+            status: 'fulfilled',
+            value
+          }),
+          reason => ({
+            status: 'rejected',
+            reason
+          })
+        )
+      )
+    );
+}
+
+function loadScript(url) {
+  return new Promise((resolve, reject) => {
+    const newScript = document.createElement('script');
+    newScript.onerror = reject;
+    newScript.onload = resolve;
+    newScript.async = true;
+    document.head.appendChild(newScript);
+    newScript.src = url;
+  });
+}
+
+async function loadComponent(url, scope) {
+  await loadScript(url);
+
+  // From https://webpack.js.org/concepts/module-federation/#dynamic-remote-containers
+  await __webpack_init_sharing__('default');
+  const container = window._JUPYTERLAB[scope];
+  // Initialize the container, it may provide shared modules and may need ours
+  await container.init(__webpack_share_scopes__.default);
+}
+
+(async function bootstrap() {
+  // This is all the data needed to load and activate plugins. This should be
+  // gathered by the server and put onto the initial page template.
+  const extension_data = getOption('federated_extensions');
+
+  // We first load all federated components so that the shared module
+  // deduplication can run and figure out which shared modules from all
+  // components should be actually used. We have to do this before importing
+  // and using the module that actually uses these components so that all
+  // dependencies are initialized.
+  let labExtensionUrl = getOption('fullLabextensionsUrl');
+  const extensions = await Promise.allSettled(
+    extension_data.map(async data => {
+      await loadComponent(
+        `${labExtensionUrl}/${data.name}/${data.load}`,
+        data.name
+      );
+    })
+  );
+
+  extensions.forEach(p => {
+    if (p.status === 'rejected') {
+      // There was an error loading the component
+      console.error(p.reason);
+    }
+  });
+
+  // Now that all federated containers are initialized with the main
+  // container, we can import the main function.
+  let main = (await import('./index.out.js')).main;
+  window.addEventListener('load', main);
+})();

+ 7 - 71
dev_mode/index.js

@@ -3,58 +3,13 @@
 | Distributed under the terms of the Modified BSD License.
 |----------------------------------------------------------------------------*/
 
-import {
-  PageConfig,
-  URLExt,
-} from '@jupyterlab/coreutils';
+import { PageConfig } from '@jupyterlab/coreutils';
 
-// eslint-disable-next-line no-undef
-__webpack_public_path__ = PageConfig.getOption('fullStaticUrl') + '/';
-
-// Promise.allSettled polyfill, until our supported browsers implement it
-// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
-if (Promise.allSettled === undefined) {
-  Promise.allSettled = promises =>
-    Promise.all(
-      promises.map(promise =>
-        promise
-          .then(value => ({
-            status: "fulfilled",
-            value,
-          }), reason => ({
-            status: "rejected",
-            reason,
-          }))
-      )
-    );
-}
 
 // This must be after the public path is set.
 // This cannot be extracted because the public path is dynamic.
 require('./imports.css');
 
-
-function loadScript(url) {
-  return new Promise((resolve, reject) => {
-    const newScript = document.createElement('script');
-    newScript.onerror = reject;
-    newScript.onload = resolve;
-    newScript.async = true;
-    document.head.appendChild(newScript);
-    newScript.src = url;
-  });
-}
-
-async function loadComponent(url, scope) {
-  await loadScript(url);
-
-  // From MIT-licensed https://github.com/module-federation/module-federation-examples/blob/af043acd6be1718ee195b2511adf6011fba4233c/advanced-api/dynamic-remotes/app1/src/App.js#L6-L12
-  await __webpack_init_sharing__('default');
-  const container = window._JUPYTERLAB[scope];
-  // Initialize the container, it may provide shared modules and may need ours
-  await container.init(__webpack_share_scopes__.default);
-}
-
 async function createModule(scope, module) {
   try {
     const factory = await window._JUPYTERLAB[scope].get(module);
@@ -68,42 +23,24 @@ async function createModule(scope, module) {
 /**
  * The main entry point for the application.
  */
-async function main() {
+export async function main() {
   var JupyterLab = require('@jupyterlab/application').JupyterLab;
   var disabled = [];
   var deferred = [];
   var ignorePlugins = [];
   var register = [];
 
-  // This is all the data needed to load and activate plugins. This should be
-  // gathered by the server and put onto the initial page template.
-  const extension_data = JSON.parse(
-    PageConfig.getOption('federated_extensions')
-  );
 
   const federatedExtensionPromises = [];
   const federatedMimeExtensionPromises = [];
   const federatedStylePromises = [];
 
-  // We first load all federated components so that the shared module
-  // deduplication can run and figure out which shared modules from all
-  // components should be actually used.
-  const extensions = await Promise.allSettled(extension_data.map( async data => {
-    await loadComponent(
-      `${URLExt.join(PageConfig.getOption('fullLabextensionsUrl'), data.name, data.load)}`,
-      data.name
-    );
-    return data;
-  }));
-
-  extensions.forEach(p => {
-    if (p.status === "rejected") {
-      // There was an error loading the component
-      console.error(p.reason);
-      return;
-    }
+  // Start initializing the federated extensions
+  const extensions = JSON.parse(
+    PageConfig.getOption('federated_extensions')
+  );
 
-    const data = p.value;
+  extensions.forEach(data => {
     if (data.extension) {
       federatedExtensionPromises.push(createModule(data.name, data.extension));
     }
@@ -262,4 +199,3 @@ async function main() {
 
 }
 
-window.addEventListener('load', main);

+ 5 - 3
dev_mode/webpack.config.js

@@ -66,8 +66,10 @@ const extData = {
 };
 fs.writeFileSync(plib.join(buildDir, 'index.out.js'), template(extData));
 
+// Create the bootstrap file that loads federated extensions and calls the
+// initialization logic in index.out.js
 const entryPoint = plib.join(buildDir, 'bootstrap.js');
-fs.writeFileSync(entryPoint, 'import("./index.out.js");');
+fs.copySync('./bootstrap.js', entryPoint);
 
 fs.copySync('./package.json', plib.join(buildDir, 'package.json'));
 if (outputDir !== buildDir) {
@@ -161,7 +163,7 @@ const shared = {};
 for (let [key, requiredVersion] of Object.entries(package_data.resolutions)) {
   // eager so that built-in extensions can be bundled together into just a few
   // js files to load
-  shared[key] = { requiredVersion, eager: true };
+  shared[key] = { requiredVersion };
 }
 
 // Add dependencies and sharedPackage config from extension packages if they
@@ -176,7 +178,7 @@ for (let pkg of extensionPackages) {
   } = require(`${pkg}/package.json`);
   for (let [dep, requiredVersion] of Object.entries(dependencies)) {
     if (!shared[dep]) {
-      pkgShared[dep] = { requiredVersion, eager: true };
+      pkgShared[dep] = { requiredVersion };
     }
   }
 

+ 1 - 1
jupyterlab/commands.py

@@ -1127,7 +1127,7 @@ class _AppHandler(object):
                 _rmtree(staging, self.logger)
                 os.makedirs(staging)
 
-        for fname in ['index.js', 'publicpath.js',
+        for fname in ['index.js', 'bootstrap.js', 'publicpath.js',
                       'webpack.config.js',
                       'webpack.prod.config.js',
                       'webpack.prod.minimize.config.js',