airbnb/caravel

View on GitHub
superset-frontend/packages/superset-ui-core/src/dynamic-plugins/shared-modules.ts

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/** The type of an imported module. Don't fully understand this, yet. */
export type Module = any;

/**
 * This is where packages are stored. We use window, because it plays well with Webpack.
 * To avoid
 * Have to amend the type of window, because window's usual type doesn't describe these fields.
 */
interface ModuleReferencer {
  [packageKey: string]: Promise<Module>;
}

declare const window: Window & typeof globalThis & ModuleReferencer;

const modulePromises: { [key: string]: Promise<Module> } = {};

const withNamespace = (name: string) => `__superset__/${name}`;

/**
 * Dependency management using global variables, because for the life of me
 * I can't figure out how to hook into UMD from a dynamically imported package.
 *
 * This defines a dynamically imported js module that can be used to import from
 * multiple different plugins.
 *
 * When importing a common module (such as react or lodash or superset-ui)
 * from a plugin, the plugin's build config will be able to
 * reference these globals instead of rebuilding them.
 *
 * @param name the module's name (should match name in package.json)
 * @param promise the promise resulting from a call to `import(name)`
 */
export async function defineSharedModule(
  name: string,
  fetchModule: () => Promise<Module>,
) {
  // this field on window is used by dynamic plugins to reference the module
  const moduleKey = withNamespace(name);

  if (!window[moduleKey] && !modulePromises[name]) {
    // if the module has not been loaded, load it
    const modulePromise = fetchModule();
    modulePromises[name] = modulePromise;
    // wait for the module to load, and attach the result to window
    // so that it can be referenced by plugins
    window[moduleKey] = await modulePromise;
  }

  // we always return a reference to the promise.
  // Multiple consumers can `.then()` or `await` the same promise,
  // even long after it has completed,
  // and it will always call back with the same reference.
  return modulePromises[name];
}

/**
 * Define multiple shared modules at once, using a map of name -> `import(name)`
 *
 * @see defineSharedModule
 * @param moduleMap
 */
export async function defineSharedModules(moduleMap: {
  [key: string]: () => Promise<Module>;
}) {
  return Promise.all(
    Object.entries(moduleMap).map(([name, fetchModule]) =>
      defineSharedModule(name, fetchModule),
    ),
  );
}

// only exposed for tests
export function reset() {
  Object.keys(modulePromises).forEach(key => {
    delete window[withNamespace(key)];
    delete modulePromises[key];
  });
}