From 947756f4318965c6715156918cd1f58f061b54da Mon Sep 17 00:00:00 2001 From: vivek1729 <1658576+vivek1729@users.noreply.github.com> Date: Tue, 13 Oct 2020 12:56:03 -0700 Subject: [PATCH 01/11] vp - Add support for loading 3rd party widgets from CDN --- .../__tests__/manager/manager.spec.ts | 75 ++++++++++++++++-- .../__tests__/manager/widgetLoader.spec.ts | 65 +++++++++++++++ .../src/manager/widget-comms.ts | 5 +- .../src/manager/widget-manager.ts | 6 +- .../src/manager/widgetLoader.ts | 79 +++++++++++++++++++ 5 files changed, 220 insertions(+), 10 deletions(-) create mode 100644 packages/jupyter-widgets/__tests__/manager/widgetLoader.spec.ts create mode 100644 packages/jupyter-widgets/src/manager/widgetLoader.ts diff --git a/packages/jupyter-widgets/__tests__/manager/manager.spec.ts b/packages/jupyter-widgets/__tests__/manager/manager.spec.ts index bf79e21..1c74a53 100644 --- a/packages/jupyter-widgets/__tests__/manager/manager.spec.ts +++ b/packages/jupyter-widgets/__tests__/manager/manager.spec.ts @@ -1,13 +1,40 @@ import { IntSliderView } from "@jupyter-widgets/controls"; import { Map } from "immutable"; +import { ManagerActions } from "../../src/manager/index"; import { WidgetManager } from "../../src/manager/widget-manager"; +import * as customWidgetLoader from "../../src/manager/widgetLoader"; + +// A mock valid module representing a custom widget +const mockFooModule = { + "foo" : "bar" +}; +// Mock implementation of the core require API +const mockRequireJS = jest.fn((modules, ready, errCB) => ready(mockFooModule)); +(window as any).requirejs = mockRequireJS; +(window as any).requirejs.config = jest.fn(); + +// Manager actions passed as the third arg when instantiating the WidgetManager class +const mockManagerActions: ManagerActions["actions"] = { + appendOutput: jest.fn(), + clearOutput: jest.fn(), + updateCellStatus: jest.fn(), + promptInputRequest: jest.fn() +}; + describe("WidgetManager", () => { describe("loadClass", () => { + beforeAll(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it("returns a class if it exists", () => { const modelById = (id: string) => undefined; - const manager = new WidgetManager(null, modelById); + const manager = new WidgetManager(null, modelById, mockManagerActions); const view = manager.loadClass( "IntSliderView", "@jupyter-widgets/controls", @@ -15,11 +42,49 @@ describe("WidgetManager", () => { ); expect(view).not.toBe(null); }); + + it("Returns a valid module class successfully from CDN for custom widgets", () => { + const modelById = (id: string) => undefined; + const manager = new WidgetManager(null, modelById, mockManagerActions); + const requireLoaderSpy = jest.spyOn(customWidgetLoader, "requireLoader"); + + return manager.loadClass( + "foo", + "fooModule", + "1.1.0" + ).then(view => { + expect(requireLoaderSpy).toHaveBeenCalledTimes(1); + // Get the second arg to Monaco.editor.create call + const mockLoaderArgs = requireLoaderSpy.mock.calls[0]; + expect(mockLoaderArgs).not.toBe(null); + expect(mockLoaderArgs.length).toBe(4); + expect(mockLoaderArgs[0]).toBe("fooModule"); + expect(mockLoaderArgs[1]).toBe("1.1.0"); + expect(view).not.toBe(null); + expect(view).toBe(mockFooModule["foo"]); + }); + }); + + it("Returns an error if the class does not exist on the module", () => { + const modelById = (id: string) => undefined; + const manager = new WidgetManager(null, modelById, mockManagerActions); + const requireLoaderSpy = jest.spyOn(customWidgetLoader, "requireLoader"); + + return manager.loadClass( + "INVALID_CLASS", + "fooModule", + "1.1.0" + ).catch(error => { + expect(requireLoaderSpy).toHaveBeenCalledTimes(1); + expect(error).toBe("Class INVALID_CLASS not found in module fooModule@1.1.0"); + }); + }); }); + describe("create_view", () => { it("returns a widget mounted on the provided element", async () => { const modelById = (id: string) => undefined; - const manager = new WidgetManager(null, modelById); + const manager = new WidgetManager(null, modelById, mockManagerActions); const model = { _dom_classes: [], _model_module: "@jupyter-widgets/controls", @@ -99,7 +164,7 @@ describe("WidgetManager", () => { const model = id === "layout_id" ? layoutModel : styleModel; return Promise.resolve(Map({ state: Map(model) })); }; - const manager = new WidgetManager(null, modelById); + const manager = new WidgetManager(null, modelById, mockManagerActions); const widget = await manager.new_widget_from_state_and_id( model, "test_model_id" @@ -120,10 +185,10 @@ describe("WidgetManager", () => { }); it("can update class properties via method", () => { const modelById = (id: string) => undefined; - const manager = new WidgetManager(null, modelById); + const manager = new WidgetManager(null, modelById, mockManagerActions); expect(manager.kernel).toBeNull(); const newKernel = { channels: { next: jest.fn() } }; - manager.update(newKernel, modelById, {}); + manager.update(newKernel, modelById, mockManagerActions); expect(manager.kernel).toBe(newKernel); }); }); diff --git a/packages/jupyter-widgets/__tests__/manager/widgetLoader.spec.ts b/packages/jupyter-widgets/__tests__/manager/widgetLoader.spec.ts new file mode 100644 index 0000000..fd49517 --- /dev/null +++ b/packages/jupyter-widgets/__tests__/manager/widgetLoader.spec.ts @@ -0,0 +1,65 @@ +import { requireLoader } from "../../src/manager/widgetLoader"; + +// A mock valid module representing a custom widget +const mockModule = { + "foo" : "bar" +}; +// Info representing an invalid module for testing the failure case +const invalidModule = { + name: "invalid_module", + version: "1.0", + url: "https://unpkg.com/invalid_module@1.0/dist/index.js" +}; +// Mock implementation of the core require API +const mockRequireJS = jest.fn((modules, ready, errCB) => { + if(modules.length > 0 && modules[0] == invalidModule.url){ + errCB(new Error('Whoops!')); + } + else { + ready(mockModule); + } +}); + +// Callbak bindind +const mockSuccessCB = jest.fn(); +const mockErrorCB = jest.fn(); +(window as any).requirejs = mockRequireJS; +(window as any).requirejs.config = jest.fn(); + +describe("requireLoader", () => { + beforeAll(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it("Returns a module if linked to a valid CDN URL", () => { + requireLoader("foo", "1.0.0", mockSuccessCB, mockErrorCB); + expect(mockRequireJS).toHaveBeenCalledTimes(1); + + const moduleURLs = mockRequireJS.mock.calls[0][0]; + expect(moduleURLs).not.toBe(null); + expect(moduleURLs.length).toBe(1); + expect(moduleURLs[0]).toBe("https://unpkg.com/foo@1.0.0/dist/index.js"); + expect(mockSuccessCB).toHaveBeenCalledWith(mockModule); + }); + + it("Returns a module even when module version is missing", () => { + requireLoader("foo", undefined, mockSuccessCB, mockErrorCB); + expect(mockRequireJS).toHaveBeenCalledTimes(1); + + const moduleURLs = mockRequireJS.mock.calls[0][0]; + expect(moduleURLs).not.toBe(null); + expect(moduleURLs.length).toBe(1); + expect(moduleURLs[0]).toBe("https://unpkg.com/foo/dist/index.js"); + expect(mockSuccessCB).toHaveBeenCalledWith(mockModule); + }); + + it("Calls the error callback if an error is encountered during the module loading", () => { + const {name, version} = invalidModule; + requireLoader(name, version, mockSuccessCB, mockErrorCB); + expect(mockRequireJS).toHaveBeenCalledTimes(1); + expect(mockSuccessCB).toBeCalledTimes(0); + expect(mockErrorCB).toBeCalledTimes(1); + }); +}); diff --git a/packages/jupyter-widgets/src/manager/widget-comms.ts b/packages/jupyter-widgets/src/manager/widget-comms.ts index 2dea0f5..006c2cc 100644 --- a/packages/jupyter-widgets/src/manager/widget-comms.ts +++ b/packages/jupyter-widgets/src/manager/widget-comms.ts @@ -205,13 +205,14 @@ export function request_state(kernel: any, comm_id: string): Promise { .pipe(childOf(message)) .subscribe((reply: any) => { // if we get a comm message back, it is the state we requested - if (reply.msg_type === "comm_msg") { + if (reply.header && reply.header.msg_type === "comm_msg") { replySubscription.unsubscribe(); return resolve(reply); } // otherwise, if we havent gotten a comm message and it goes idle, it wasn't found else if ( - reply.msg_type === "status" && + reply.header && + reply.header.msg_type === "status" && reply.content.execution_state === "idle" ) { replySubscription.unsubscribe(); diff --git a/packages/jupyter-widgets/src/manager/widget-manager.ts b/packages/jupyter-widgets/src/manager/widget-manager.ts index d91409f..22174b9 100644 --- a/packages/jupyter-widgets/src/manager/widget-manager.ts +++ b/packages/jupyter-widgets/src/manager/widget-manager.ts @@ -16,6 +16,7 @@ import { } from "@nteract/core"; import { JupyterMessage } from "@nteract/messaging"; import { ManagerActions } from "../manager/index"; +import { initRequireDeps, requireLoader } from "./widgetLoader"; interface IDomWidgetModel extends DOMWidgetModel { _model_name: string; @@ -52,6 +53,7 @@ export class WidgetManager extends base.ManagerBase { this.stateModelById = stateModelById; this.actions = actions; this.widgetsBeingCreated = {}; + initRequireDeps(); } update( @@ -74,9 +76,7 @@ export class WidgetManager extends base.ManagerBase { } else if (moduleName === "@jupyter-widgets/base") { resolve(base); } else { - return Promise.reject( - `Module ${moduleName}@${moduleVersion} not found` - ); + requireLoader(moduleName, moduleVersion, resolve, reject); } }).then(function(module: any) { if (module[className]) { diff --git a/packages/jupyter-widgets/src/manager/widgetLoader.ts b/packages/jupyter-widgets/src/manager/widgetLoader.ts new file mode 100644 index 0000000..2d0283c --- /dev/null +++ b/packages/jupyter-widgets/src/manager/widgetLoader.ts @@ -0,0 +1,79 @@ +import * as base from "@jupyter-widgets/base"; +import * as controls from "@jupyter-widgets/controls"; + +let cdn = 'https://unpkg.com/'; + +/** + * Constructs a well formed module URL for requireJS + * mapping the modulename and version from the base CDN URL + * @param moduleName Name of the module corresponding to the widget package + * @param moduleVersion Module version returned from kernel + */ +function moduleNameToCDNUrl(moduleName: string, moduleVersion: string): string { + let packageName = moduleName; + let fileName = 'index.js'; // default filename + // if a '/' is present, like 'foo/bar', packageName is changed to 'foo', and path to 'bar' + // We first find the first '/' + let index = moduleName.indexOf('/'); + if (index !== -1 && moduleName[0] === '@') { + // if we have a namespace, it's a different story + // @foo/bar/baz should translate to @foo/bar and baz + // so we find the 2nd '/' + index = moduleName.indexOf('/', index + 1); + } + if (index !== -1) { + fileName = moduleName.substr(index + 1); + packageName = moduleName.substr(0, index); + } + let moduleNameString = moduleName; + if(moduleVersion){ + moduleNameString = `${moduleName}@${moduleVersion}`; + } + return `${cdn}${moduleNameString}/dist/${fileName}`; +} + +/** + * Initialize dependencies that need to be preconfigured for requireJS module loading + * Here, we define the jupyter-base, controls package that most 3rd party widgets depend on + * We also override the cdn end point (if applicable) + */ +export function initRequireDeps(){ + // Export the following for `requirejs`. + // tslint:disable-next-line: no-any no-function-expression no-empty + const define = (window as any).define || function () {}; + define('@jupyter-widgets/controls', () => controls); + define('@jupyter-widgets/base', () => base); + + // find the data-cdn for any script tag, assuming it is only used for embed-amd.js + const scripts = document.getElementsByTagName('script'); + Array.prototype.forEach.call(scripts, (script: HTMLScriptElement) => { + cdn = script.getAttribute('data-jupyter-widgets-cdn') || cdn; + }); +} + +/** + * Load an amd module locally and fall back to specified CDN if unavailable. + * + * @param moduleName The name of the module to load.. + * @param moduleVersion The semver range for the module, if loaded from a CDN. + * @param succssCB Callback when the module is loaded successfully by requireJS + * @param errorCB Called to hand off any errors encountered during modul eloading + * + * By default, the CDN service used is unpkg.com. However, this default can be + * overriden by specifying another URL via the HTML attribute + * "data-jupyter-widgets-cdn" on a script tag of the page. + * + * The semver range is only used with the CDN. + */ +export function requireLoader(moduleName: string, moduleVersion: string, successCB: (value?: unknown) => void, errorCB: (reason ?: any) => void): any { + const require = (window as any).requirejs; + if (require === undefined) { + console.error('Requirejs is needed, please ensure it is loaded on the page.'); + } + const conf: { paths: { [key: string]: string } } = { paths: {} }; + const moduleCDN = moduleNameToCDNUrl(moduleName, moduleVersion); + console.log("module CDN Url "+moduleCDN); + conf.paths[moduleName] = moduleCDN; + require.config(conf); + return require([`${moduleCDN}`], successCB, errorCB); +} From a6b63c20e1e7fa5ba3f994b90e7cd17e59947821 Mon Sep 17 00:00:00 2001 From: vivek1729 <1658576+vivek1729@users.noreply.github.com> Date: Thu, 5 Nov 2020 10:39:17 -0800 Subject: [PATCH 02/11] vp - Add extensibility point for providing custom loader for 3rd party widgets, minor cleanup --- packages/jupyter-widgets/src/index.tsx | 2 ++ packages/jupyter-widgets/src/manager/index.tsx | 4 +++- .../jupyter-widgets/src/manager/widget-manager.ts | 14 ++++++++++---- .../jupyter-widgets/src/manager/widgetLoader.ts | 12 +++++++++--- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/jupyter-widgets/src/index.tsx b/packages/jupyter-widgets/src/index.tsx index 13c252d..2a65b8c 100644 --- a/packages/jupyter-widgets/src/index.tsx +++ b/packages/jupyter-widgets/src/index.tsx @@ -30,6 +30,7 @@ interface Props { | null; id: CellId; contentRef: ContentRef; + customWidgetLoader?: (mName: string, mVer: string, sucCB: any, errCB: any) => any; } interface State { @@ -72,6 +73,7 @@ export class WidgetDisplay extends React.Component { contentRef={this.props.contentRef} modelById={this.props.modelById} kernel={this.props.kernel} + customWidgetLoader={this.props.customWidgetLoader} /> ); } else { diff --git a/packages/jupyter-widgets/src/manager/index.tsx b/packages/jupyter-widgets/src/manager/index.tsx index b7b4561..f0cad9a 100644 --- a/packages/jupyter-widgets/src/manager/index.tsx +++ b/packages/jupyter-widgets/src/manager/index.tsx @@ -37,6 +37,7 @@ interface OwnProps { model_id: string; id: CellId; contentRef: ContentRef; + customWidgetLoader?: (mName: string, mVer: string, sucCB: any, errCB: any) => any; } type Props = ConnectedProps & OwnProps & ManagerActions; @@ -68,7 +69,8 @@ class Manager extends React.Component { Manager.manager = new WidgetManager( this.props.kernel, this.props.modelById, - this.props.actions + this.props.actions, + this.props.customWidgetLoader ); } else { Manager.manager.update( diff --git a/packages/jupyter-widgets/src/manager/widget-manager.ts b/packages/jupyter-widgets/src/manager/widget-manager.ts index 22174b9..bf587ea 100644 --- a/packages/jupyter-widgets/src/manager/widget-manager.ts +++ b/packages/jupyter-widgets/src/manager/widget-manager.ts @@ -16,7 +16,7 @@ import { } from "@nteract/core"; import { JupyterMessage } from "@nteract/messaging"; import { ManagerActions } from "../manager/index"; -import { initRequireDeps, requireLoader } from "./widgetLoader"; +import { initRequireDeps, overrideCDNBaseURL, requireLoader } from "./widgetLoader"; interface IDomWidgetModel extends DOMWidgetModel { _model_name: string; @@ -42,18 +42,23 @@ export class WidgetManager extends base.ManagerBase { | null; actions: ManagerActions["actions"]; widgetsBeingCreated: { [model_id: string]: Promise }; + customWidgetLoader?: (mName: string, mVer: string, sucCB: any, errCB: any) => any; constructor( kernel: any, stateModelById: (id: string) => any, - actions: ManagerActions["actions"] + actions: ManagerActions["actions"], + customWidgetLoader?: (mName: string, mVer: string, sucCB: any, errCB: any) => any ) { super(); this.kernel = kernel; this.stateModelById = stateModelById; this.actions = actions; this.widgetsBeingCreated = {}; - initRequireDeps(); + this.customWidgetLoader = customWidgetLoader; + // Setup for supporting 3rd party widgets + initRequireDeps(); // define jupyter-widgets base package for requirejs + overrideCDNBaseURL(); // Override default CDN URL for fetching widgets } update( @@ -70,13 +75,14 @@ export class WidgetManager extends base.ManagerBase { * Load a class and return a promise to the loaded object. */ loadClass(className: string, moduleName: string, moduleVersion: string): any { + const cwLoader = this.customWidgetLoader ? this.customWidgetLoader : requireLoader; return new Promise(function(resolve, reject) { if (moduleName === "@jupyter-widgets/controls") { resolve(controls); } else if (moduleName === "@jupyter-widgets/base") { resolve(base); } else { - requireLoader(moduleName, moduleVersion, resolve, reject); + cwLoader(moduleName, moduleVersion, resolve, reject); } }).then(function(module: any) { if (module[className]) { diff --git a/packages/jupyter-widgets/src/manager/widgetLoader.ts b/packages/jupyter-widgets/src/manager/widgetLoader.ts index 2d0283c..7a0b786 100644 --- a/packages/jupyter-widgets/src/manager/widgetLoader.ts +++ b/packages/jupyter-widgets/src/manager/widgetLoader.ts @@ -25,9 +25,9 @@ function moduleNameToCDNUrl(moduleName: string, moduleVersion: string): string { fileName = moduleName.substr(index + 1); packageName = moduleName.substr(0, index); } - let moduleNameString = moduleName; + let moduleNameString = packageName; if(moduleVersion){ - moduleNameString = `${moduleName}@${moduleVersion}`; + moduleNameString = `${packageName}@${moduleVersion}`; } return `${cdn}${moduleNameString}/dist/${fileName}`; } @@ -43,7 +43,14 @@ export function initRequireDeps(){ const define = (window as any).define || function () {}; define('@jupyter-widgets/controls', () => controls); define('@jupyter-widgets/base', () => base); +} +/** + * Overrides the default CDN base URL by querying the DOM for script tags + * We follow the same pattern as defined in HTML manager class of ipywidgets + * https://github.com/jupyter-widgets/ipywidgets/blob/master/packages/html-manager/src/libembed-amd.ts + */ +export function overrideCDNBaseURL(){ // find the data-cdn for any script tag, assuming it is only used for embed-amd.js const scripts = document.getElementsByTagName('script'); Array.prototype.forEach.call(scripts, (script: HTMLScriptElement) => { @@ -72,7 +79,6 @@ export function requireLoader(moduleName: string, moduleVersion: string, success } const conf: { paths: { [key: string]: string } } = { paths: {} }; const moduleCDN = moduleNameToCDNUrl(moduleName, moduleVersion); - console.log("module CDN Url "+moduleCDN); conf.paths[moduleName] = moduleCDN; require.config(conf); return require([`${moduleCDN}`], successCB, errorCB); From 2ea82ee1195c21a10972164d014c59d2b11ce496 Mon Sep 17 00:00:00 2001 From: vivek1729 <1658576+vivek1729@users.noreply.github.com> Date: Mon, 9 Nov 2020 08:21:29 -0800 Subject: [PATCH 03/11] vp - Fix typos, minor formating, URL sanitization --- .../__tests__/manager/widgetLoader.spec.ts | 2 +- .../src/manager/widget-manager.ts | 2 ++ .../src/manager/widgetLoader.ts | 32 +++++++++++-------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/jupyter-widgets/__tests__/manager/widgetLoader.spec.ts b/packages/jupyter-widgets/__tests__/manager/widgetLoader.spec.ts index fd49517..24784fa 100644 --- a/packages/jupyter-widgets/__tests__/manager/widgetLoader.spec.ts +++ b/packages/jupyter-widgets/__tests__/manager/widgetLoader.spec.ts @@ -20,7 +20,7 @@ const mockRequireJS = jest.fn((modules, ready, errCB) => { } }); -// Callbak bindind +// Callback binding const mockSuccessCB = jest.fn(); const mockErrorCB = jest.fn(); (window as any).requirejs = mockRequireJS; diff --git a/packages/jupyter-widgets/src/manager/widget-manager.ts b/packages/jupyter-widgets/src/manager/widget-manager.ts index bf587ea..ee6ede0 100644 --- a/packages/jupyter-widgets/src/manager/widget-manager.ts +++ b/packages/jupyter-widgets/src/manager/widget-manager.ts @@ -92,6 +92,8 @@ export class WidgetManager extends base.ManagerBase { `Class ${className} not found in module ${moduleName}@${moduleVersion}` ); } + }).catch(function(err: Error) { + console.warn(err.message); }); } diff --git a/packages/jupyter-widgets/src/manager/widgetLoader.ts b/packages/jupyter-widgets/src/manager/widgetLoader.ts index 7a0b786..3e164ae 100644 --- a/packages/jupyter-widgets/src/manager/widgetLoader.ts +++ b/packages/jupyter-widgets/src/manager/widgetLoader.ts @@ -1,7 +1,7 @@ import * as base from "@jupyter-widgets/base"; import * as controls from "@jupyter-widgets/controls"; -let cdn = 'https://unpkg.com/'; +let cdn = "https://unpkg.com"; /** * Constructs a well formed module URL for requireJS @@ -11,7 +11,7 @@ let cdn = 'https://unpkg.com/'; */ function moduleNameToCDNUrl(moduleName: string, moduleVersion: string): string { let packageName = moduleName; - let fileName = 'index.js'; // default filename + let fileName = "index.js"; // default filename // if a '/' is present, like 'foo/bar', packageName is changed to 'foo', and path to 'bar' // We first find the first '/' let index = moduleName.indexOf('/'); @@ -29,7 +29,7 @@ function moduleNameToCDNUrl(moduleName: string, moduleVersion: string): string { if(moduleVersion){ moduleNameString = `${packageName}@${moduleVersion}`; } - return `${cdn}${moduleNameString}/dist/${fileName}`; + return `${cdn}/${moduleNameString}/dist/${fileName}`; } /** @@ -41,8 +41,8 @@ export function initRequireDeps(){ // Export the following for `requirejs`. // tslint:disable-next-line: no-any no-function-expression no-empty const define = (window as any).define || function () {}; - define('@jupyter-widgets/controls', () => controls); - define('@jupyter-widgets/base', () => base); + define("@jupyter-widgets/controls", () => controls); + define("@jupyter-widgets/base", () => base); } /** @@ -52,10 +52,12 @@ export function initRequireDeps(){ */ export function overrideCDNBaseURL(){ // find the data-cdn for any script tag, assuming it is only used for embed-amd.js - const scripts = document.getElementsByTagName('script'); + const scripts = document.getElementsByTagName("script"); Array.prototype.forEach.call(scripts, (script: HTMLScriptElement) => { - cdn = script.getAttribute('data-jupyter-widgets-cdn') || cdn; + cdn = script.getAttribute("data-jupyter-widgets-cdn") || cdn; }); + // Remove Single/consecutive trailing slashes from the URL to sanitize it + cdn = cdn.replace(/\/+$/, ""); } /** @@ -64,7 +66,7 @@ export function overrideCDNBaseURL(){ * @param moduleName The name of the module to load.. * @param moduleVersion The semver range for the module, if loaded from a CDN. * @param succssCB Callback when the module is loaded successfully by requireJS - * @param errorCB Called to hand off any errors encountered during modul eloading + * @param errorCB Called to hand off any errors encountered during module loading * * By default, the CDN service used is unpkg.com. However, this default can be * overriden by specifying another URL via the HTML attribute @@ -75,11 +77,13 @@ export function overrideCDNBaseURL(){ export function requireLoader(moduleName: string, moduleVersion: string, successCB: (value?: unknown) => void, errorCB: (reason ?: any) => void): any { const require = (window as any).requirejs; if (require === undefined) { - console.error('Requirejs is needed, please ensure it is loaded on the page.'); + return errorCB(new Error("Requirejs is needed, please ensure it is loaded on the page.")); + } + else{ + const conf: { paths: { [key: string]: string } } = { paths: {} }; + const moduleCDN = moduleNameToCDNUrl(moduleName, moduleVersion); + conf.paths[moduleName] = moduleCDN; + require.config(conf); + return require([`${moduleCDN}`], successCB, errorCB); } - const conf: { paths: { [key: string]: string } } = { paths: {} }; - const moduleCDN = moduleNameToCDNUrl(moduleName, moduleVersion); - conf.paths[moduleName] = moduleCDN; - require.config(conf); - return require([`${moduleCDN}`], successCB, errorCB); } From aadbe7cc8f833943fcfcd191385918d4992dc2a0 Mon Sep 17 00:00:00 2001 From: vivek1729 <1658576+vivek1729@users.noreply.github.com> Date: Wed, 11 Nov 2020 06:29:49 -0800 Subject: [PATCH 04/11] vp - Rename files to match nteract pattern --- packages/jupyter-widgets/__tests__/manager/manager.spec.ts | 2 +- .../manager/{widgetLoader.spec.ts => widget-loader.spec.ts} | 2 +- .../src/manager/{widgetLoader.ts => widget-loader.ts} | 0 packages/jupyter-widgets/src/manager/widget-manager.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/jupyter-widgets/__tests__/manager/{widgetLoader.spec.ts => widget-loader.spec.ts} (97%) rename packages/jupyter-widgets/src/manager/{widgetLoader.ts => widget-loader.ts} (100%) diff --git a/packages/jupyter-widgets/__tests__/manager/manager.spec.ts b/packages/jupyter-widgets/__tests__/manager/manager.spec.ts index 1c74a53..58c81fe 100644 --- a/packages/jupyter-widgets/__tests__/manager/manager.spec.ts +++ b/packages/jupyter-widgets/__tests__/manager/manager.spec.ts @@ -3,7 +3,7 @@ import { Map } from "immutable"; import { ManagerActions } from "../../src/manager/index"; import { WidgetManager } from "../../src/manager/widget-manager"; -import * as customWidgetLoader from "../../src/manager/widgetLoader"; +import * as customWidgetLoader from "../../src/manager/widget-loader"; // A mock valid module representing a custom widget const mockFooModule = { diff --git a/packages/jupyter-widgets/__tests__/manager/widgetLoader.spec.ts b/packages/jupyter-widgets/__tests__/manager/widget-loader.spec.ts similarity index 97% rename from packages/jupyter-widgets/__tests__/manager/widgetLoader.spec.ts rename to packages/jupyter-widgets/__tests__/manager/widget-loader.spec.ts index 24784fa..192e9a6 100644 --- a/packages/jupyter-widgets/__tests__/manager/widgetLoader.spec.ts +++ b/packages/jupyter-widgets/__tests__/manager/widget-loader.spec.ts @@ -1,4 +1,4 @@ -import { requireLoader } from "../../src/manager/widgetLoader"; +import { requireLoader } from "../../src/manager/widget-loader"; // A mock valid module representing a custom widget const mockModule = { diff --git a/packages/jupyter-widgets/src/manager/widgetLoader.ts b/packages/jupyter-widgets/src/manager/widget-loader.ts similarity index 100% rename from packages/jupyter-widgets/src/manager/widgetLoader.ts rename to packages/jupyter-widgets/src/manager/widget-loader.ts diff --git a/packages/jupyter-widgets/src/manager/widget-manager.ts b/packages/jupyter-widgets/src/manager/widget-manager.ts index ee6ede0..0551aaf 100644 --- a/packages/jupyter-widgets/src/manager/widget-manager.ts +++ b/packages/jupyter-widgets/src/manager/widget-manager.ts @@ -16,7 +16,7 @@ import { } from "@nteract/core"; import { JupyterMessage } from "@nteract/messaging"; import { ManagerActions } from "../manager/index"; -import { initRequireDeps, overrideCDNBaseURL, requireLoader } from "./widgetLoader"; +import { initRequireDeps, overrideCDNBaseURL, requireLoader } from "./widget-loader"; interface IDomWidgetModel extends DOMWidgetModel { _model_name: string; From af29994c2248a5397f85e555e5c60e537ff780e8 Mon Sep 17 00:00:00 2001 From: vivek1729 <1658576+vivek1729@users.noreply.github.com> Date: Wed, 11 Nov 2020 06:36:25 -0800 Subject: [PATCH 05/11] vp - Address review comments to update tests --- .../__tests__/manager/manager.spec.ts | 19 ++++++++----------- .../__tests__/manager/widget-loader.spec.ts | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/jupyter-widgets/__tests__/manager/manager.spec.ts b/packages/jupyter-widgets/__tests__/manager/manager.spec.ts index 58c81fe..b59c975 100644 --- a/packages/jupyter-widgets/__tests__/manager/manager.spec.ts +++ b/packages/jupyter-widgets/__tests__/manager/manager.spec.ts @@ -22,6 +22,8 @@ const mockManagerActions: ManagerActions["actions"] = { promptInputRequest: jest.fn() }; +// Default modelById stub +const mockModelById = (id: string) => undefined; describe("WidgetManager", () => { describe("loadClass", () => { @@ -33,8 +35,7 @@ describe("WidgetManager", () => { }); it("returns a class if it exists", () => { - const modelById = (id: string) => undefined; - const manager = new WidgetManager(null, modelById, mockManagerActions); + const manager = new WidgetManager(null, mockModelById, mockManagerActions); const view = manager.loadClass( "IntSliderView", "@jupyter-widgets/controls", @@ -44,8 +45,7 @@ describe("WidgetManager", () => { }); it("Returns a valid module class successfully from CDN for custom widgets", () => { - const modelById = (id: string) => undefined; - const manager = new WidgetManager(null, modelById, mockManagerActions); + const manager = new WidgetManager(null, mockModelById, mockManagerActions); const requireLoaderSpy = jest.spyOn(customWidgetLoader, "requireLoader"); return manager.loadClass( @@ -66,8 +66,7 @@ describe("WidgetManager", () => { }); it("Returns an error if the class does not exist on the module", () => { - const modelById = (id: string) => undefined; - const manager = new WidgetManager(null, modelById, mockManagerActions); + const manager = new WidgetManager(null, mockModelById, mockManagerActions); const requireLoaderSpy = jest.spyOn(customWidgetLoader, "requireLoader"); return manager.loadClass( @@ -83,8 +82,7 @@ describe("WidgetManager", () => { describe("create_view", () => { it("returns a widget mounted on the provided element", async () => { - const modelById = (id: string) => undefined; - const manager = new WidgetManager(null, modelById, mockManagerActions); + const manager = new WidgetManager(null, mockModelById, mockManagerActions); const model = { _dom_classes: [], _model_module: "@jupyter-widgets/controls", @@ -184,11 +182,10 @@ describe("WidgetManager", () => { }); }); it("can update class properties via method", () => { - const modelById = (id: string) => undefined; - const manager = new WidgetManager(null, modelById, mockManagerActions); + const manager = new WidgetManager(null, mockModelById, mockManagerActions); expect(manager.kernel).toBeNull(); const newKernel = { channels: { next: jest.fn() } }; - manager.update(newKernel, modelById, mockManagerActions); + manager.update(newKernel, mockModelById, mockManagerActions); expect(manager.kernel).toBe(newKernel); }); }); diff --git a/packages/jupyter-widgets/__tests__/manager/widget-loader.spec.ts b/packages/jupyter-widgets/__tests__/manager/widget-loader.spec.ts index 192e9a6..b272c0d 100644 --- a/packages/jupyter-widgets/__tests__/manager/widget-loader.spec.ts +++ b/packages/jupyter-widgets/__tests__/manager/widget-loader.spec.ts @@ -12,7 +12,7 @@ const invalidModule = { }; // Mock implementation of the core require API const mockRequireJS = jest.fn((modules, ready, errCB) => { - if(modules.length > 0 && modules[0] == invalidModule.url){ + if(modules.length > 0 && modules[0] === invalidModule.url){ errCB(new Error('Whoops!')); } else { From 658b54f32baa9ef326b8a8cfb58883b59d348781 Mon Sep 17 00:00:00 2001 From: vivek1729 <1658576+vivek1729@users.noreply.github.com> Date: Wed, 11 Nov 2020 10:44:07 -0800 Subject: [PATCH 06/11] vp - Simplify requireLoader to return a promise, address more review comments --- .../__tests__/manager/manager.spec.ts | 2 +- .../__tests__/manager/widget-loader.spec.ts | 44 +++++++++---------- packages/jupyter-widgets/src/index.tsx | 2 +- .../jupyter-widgets/src/manager/index.tsx | 2 +- .../src/manager/widget-comms.ts | 5 +-- .../src/manager/widget-loader.ts | 40 ++++++++++++----- .../src/manager/widget-manager.ts | 34 +++++++------- 7 files changed, 74 insertions(+), 55 deletions(-) diff --git a/packages/jupyter-widgets/__tests__/manager/manager.spec.ts b/packages/jupyter-widgets/__tests__/manager/manager.spec.ts index b59c975..5fcff42 100644 --- a/packages/jupyter-widgets/__tests__/manager/manager.spec.ts +++ b/packages/jupyter-widgets/__tests__/manager/manager.spec.ts @@ -57,7 +57,7 @@ describe("WidgetManager", () => { // Get the second arg to Monaco.editor.create call const mockLoaderArgs = requireLoaderSpy.mock.calls[0]; expect(mockLoaderArgs).not.toBe(null); - expect(mockLoaderArgs.length).toBe(4); + expect(mockLoaderArgs.length).toBe(2); expect(mockLoaderArgs[0]).toBe("fooModule"); expect(mockLoaderArgs[1]).toBe("1.1.0"); expect(view).not.toBe(null); diff --git a/packages/jupyter-widgets/__tests__/manager/widget-loader.spec.ts b/packages/jupyter-widgets/__tests__/manager/widget-loader.spec.ts index b272c0d..60ba6c6 100644 --- a/packages/jupyter-widgets/__tests__/manager/widget-loader.spec.ts +++ b/packages/jupyter-widgets/__tests__/manager/widget-loader.spec.ts @@ -13,7 +13,7 @@ const invalidModule = { // Mock implementation of the core require API const mockRequireJS = jest.fn((modules, ready, errCB) => { if(modules.length > 0 && modules[0] === invalidModule.url){ - errCB(new Error('Whoops!')); + errCB(new Error("Whoops!")); } else { ready(mockModule); @@ -21,8 +21,6 @@ const mockRequireJS = jest.fn((modules, ready, errCB) => { }); // Callback binding -const mockSuccessCB = jest.fn(); -const mockErrorCB = jest.fn(); (window as any).requirejs = mockRequireJS; (window as any).requirejs.config = jest.fn(); @@ -34,32 +32,32 @@ describe("requireLoader", () => { jest.clearAllMocks(); }); it("Returns a module if linked to a valid CDN URL", () => { - requireLoader("foo", "1.0.0", mockSuccessCB, mockErrorCB); - expect(mockRequireJS).toHaveBeenCalledTimes(1); - - const moduleURLs = mockRequireJS.mock.calls[0][0]; - expect(moduleURLs).not.toBe(null); - expect(moduleURLs.length).toBe(1); - expect(moduleURLs[0]).toBe("https://unpkg.com/foo@1.0.0/dist/index.js"); - expect(mockSuccessCB).toHaveBeenCalledWith(mockModule); + return requireLoader("foo", "1.0.0").then(mod => { + expect(mockRequireJS).toHaveBeenCalledTimes(1); + const moduleURLs = mockRequireJS.mock.calls[0][0]; + expect(moduleURLs).not.toBe(null); + expect(moduleURLs.length).toBe(1); + expect(moduleURLs[0]).toBe("https://unpkg.com/foo@1.0.0/dist/index.js"); + expect(mod).toEqual(mockModule); + }); }); it("Returns a module even when module version is missing", () => { - requireLoader("foo", undefined, mockSuccessCB, mockErrorCB); - expect(mockRequireJS).toHaveBeenCalledTimes(1); - - const moduleURLs = mockRequireJS.mock.calls[0][0]; - expect(moduleURLs).not.toBe(null); - expect(moduleURLs.length).toBe(1); - expect(moduleURLs[0]).toBe("https://unpkg.com/foo/dist/index.js"); - expect(mockSuccessCB).toHaveBeenCalledWith(mockModule); + return requireLoader("foo", undefined).then(mod => { + expect(mockRequireJS).toHaveBeenCalledTimes(1); + const moduleURLs = mockRequireJS.mock.calls[0][0]; + expect(moduleURLs).not.toBe(null); + expect(moduleURLs.length).toBe(1); + expect(moduleURLs[0]).toBe("https://unpkg.com/foo/dist/index.js"); + expect(mod).toEqual(mockModule); + }); }); it("Calls the error callback if an error is encountered during the module loading", () => { const {name, version} = invalidModule; - requireLoader(name, version, mockSuccessCB, mockErrorCB); - expect(mockRequireJS).toHaveBeenCalledTimes(1); - expect(mockSuccessCB).toBeCalledTimes(0); - expect(mockErrorCB).toBeCalledTimes(1); + return requireLoader(name, version).catch((error: Error) => { + expect(mockRequireJS).toHaveBeenCalledTimes(1); + expect(error.message).toBe("Whoops!"); + }); }); }); diff --git a/packages/jupyter-widgets/src/index.tsx b/packages/jupyter-widgets/src/index.tsx index 2a65b8c..5965a9c 100644 --- a/packages/jupyter-widgets/src/index.tsx +++ b/packages/jupyter-widgets/src/index.tsx @@ -30,7 +30,7 @@ interface Props { | null; id: CellId; contentRef: ContentRef; - customWidgetLoader?: (mName: string, mVer: string, sucCB: any, errCB: any) => any; + customWidgetLoader?: (moduleName: string, moduleVersion: string) => Promise; } interface State { diff --git a/packages/jupyter-widgets/src/manager/index.tsx b/packages/jupyter-widgets/src/manager/index.tsx index f0cad9a..975a9a1 100644 --- a/packages/jupyter-widgets/src/manager/index.tsx +++ b/packages/jupyter-widgets/src/manager/index.tsx @@ -37,7 +37,7 @@ interface OwnProps { model_id: string; id: CellId; contentRef: ContentRef; - customWidgetLoader?: (mName: string, mVer: string, sucCB: any, errCB: any) => any; + customWidgetLoader?: (moduleName: string, moduleVersion: string) => Promise; } type Props = ConnectedProps & OwnProps & ManagerActions; diff --git a/packages/jupyter-widgets/src/manager/widget-comms.ts b/packages/jupyter-widgets/src/manager/widget-comms.ts index 006c2cc..9858aa9 100644 --- a/packages/jupyter-widgets/src/manager/widget-comms.ts +++ b/packages/jupyter-widgets/src/manager/widget-comms.ts @@ -205,14 +205,13 @@ export function request_state(kernel: any, comm_id: string): Promise { .pipe(childOf(message)) .subscribe((reply: any) => { // if we get a comm message back, it is the state we requested - if (reply.header && reply.header.msg_type === "comm_msg") { + if (reply.header?.msg_type === "comm_msg") { replySubscription.unsubscribe(); return resolve(reply); } // otherwise, if we havent gotten a comm message and it goes idle, it wasn't found else if ( - reply.header && - reply.header.msg_type === "status" && + reply.header?.msg_type === "status" && reply.content.execution_state === "idle" ) { replySubscription.unsubscribe(); diff --git a/packages/jupyter-widgets/src/manager/widget-loader.ts b/packages/jupyter-widgets/src/manager/widget-loader.ts index 3e164ae..f96c006 100644 --- a/packages/jupyter-widgets/src/manager/widget-loader.ts +++ b/packages/jupyter-widgets/src/manager/widget-loader.ts @@ -1,3 +1,8 @@ +/** + * Several functions in this file are based off the html-manager in jupyter-widgets project - + * https://github.com/jupyter-widgets/ipywidgets/blob/master/packages/html-manager/src/libembed-amd.ts + */ + import * as base from "@jupyter-widgets/base"; import * as controls from "@jupyter-widgets/controls"; @@ -25,13 +30,27 @@ function moduleNameToCDNUrl(moduleName: string, moduleVersion: string): string { fileName = moduleName.substr(index + 1); packageName = moduleName.substr(0, index); } - let moduleNameString = packageName; - if(moduleVersion){ - moduleNameString = `${packageName}@${moduleVersion}`; - } + const moduleNameString = moduleVersion ? `${packageName}@${moduleVersion}` : packageName; return `${cdn}/${moduleNameString}/dist/${fileName}`; } +/** + * Load a package using requirejs and return a promise + * + * @param pkg Package name or names to load + */ +function requirePromise(pkg: string | string[]): Promise { + return new Promise((resolve, reject) => { + const require = (window as any).requirejs; + if (require === undefined) { + reject('Requirejs is needed, please ensure it is loaded on the page.'); + } else { + // tslint:disable-next-line: non-literal-require + require(pkg, resolve, reject); + } + }); +}; + /** * Initialize dependencies that need to be preconfigured for requireJS module loading * Here, we define the jupyter-base, controls package that most 3rd party widgets depend on @@ -47,8 +66,9 @@ export function initRequireDeps(){ /** * Overrides the default CDN base URL by querying the DOM for script tags - * We follow the same pattern as defined in HTML manager class of ipywidgets - * https://github.com/jupyter-widgets/ipywidgets/blob/master/packages/html-manager/src/libembed-amd.ts + * By default, the CDN service used is unpkg.com. However, this default can be + * overriden by specifying another URL via the HTML attribute + * "data-jupyter-widgets-cdn" on a script tag of the page. */ export function overrideCDNBaseURL(){ // find the data-cdn for any script tag, assuming it is only used for embed-amd.js @@ -74,16 +94,16 @@ export function overrideCDNBaseURL(){ * * The semver range is only used with the CDN. */ -export function requireLoader(moduleName: string, moduleVersion: string, successCB: (value?: unknown) => void, errorCB: (reason ?: any) => void): any { +export function requireLoader(moduleName: string, moduleVersion: string): Promise { const require = (window as any).requirejs; if (require === undefined) { - return errorCB(new Error("Requirejs is needed, please ensure it is loaded on the page.")); + return Promise.reject(new Error("Requirejs is needed, please ensure it is loaded on the page.")); } - else{ + else { const conf: { paths: { [key: string]: string } } = { paths: {} }; const moduleCDN = moduleNameToCDNUrl(moduleName, moduleVersion); conf.paths[moduleName] = moduleCDN; require.config(conf); - return require([`${moduleCDN}`], successCB, errorCB); + return requirePromise([moduleCDN]); } } diff --git a/packages/jupyter-widgets/src/manager/widget-manager.ts b/packages/jupyter-widgets/src/manager/widget-manager.ts index 0551aaf..b1ba761 100644 --- a/packages/jupyter-widgets/src/manager/widget-manager.ts +++ b/packages/jupyter-widgets/src/manager/widget-manager.ts @@ -16,7 +16,7 @@ import { } from "@nteract/core"; import { JupyterMessage } from "@nteract/messaging"; import { ManagerActions } from "../manager/index"; -import { initRequireDeps, overrideCDNBaseURL, requireLoader } from "./widget-loader"; +import * as widgetLoader from "./widget-loader"; interface IDomWidgetModel extends DOMWidgetModel { _model_name: string; @@ -42,13 +42,13 @@ export class WidgetManager extends base.ManagerBase { | null; actions: ManagerActions["actions"]; widgetsBeingCreated: { [model_id: string]: Promise }; - customWidgetLoader?: (mName: string, mVer: string, sucCB: any, errCB: any) => any; + customWidgetLoader?: (moduleName: string, moduleVersion: string) => Promise; constructor( kernel: any, stateModelById: (id: string) => any, actions: ManagerActions["actions"], - customWidgetLoader?: (mName: string, mVer: string, sucCB: any, errCB: any) => any + customWidgetLoader?: (moduleName: string, moduleVersion: string) => Promise ) { super(); this.kernel = kernel; @@ -57,8 +57,8 @@ export class WidgetManager extends base.ManagerBase { this.widgetsBeingCreated = {}; this.customWidgetLoader = customWidgetLoader; // Setup for supporting 3rd party widgets - initRequireDeps(); // define jupyter-widgets base package for requirejs - overrideCDNBaseURL(); // Override default CDN URL for fetching widgets + widgetLoader.initRequireDeps(); // define jupyter-widgets base package for requirejs + widgetLoader.overrideCDNBaseURL(); // Override default CDN URL for fetching widgets } update( @@ -74,17 +74,19 @@ export class WidgetManager extends base.ManagerBase { /** * Load a class and return a promise to the loaded object. */ - loadClass(className: string, moduleName: string, moduleVersion: string): any { - const cwLoader = this.customWidgetLoader ? this.customWidgetLoader : requireLoader; - return new Promise(function(resolve, reject) { - if (moduleName === "@jupyter-widgets/controls") { - resolve(controls); - } else if (moduleName === "@jupyter-widgets/base") { - resolve(base); - } else { - cwLoader(moduleName, moduleVersion, resolve, reject); - } - }).then(function(module: any) { + loadClass(className: string, moduleName: string, moduleVersion: string): Promise { + const customWidgetLoader = this.customWidgetLoader ?? widgetLoader.requireLoader; + + let widgetPromise: Promise; + if (moduleName === "@jupyter-widgets/controls") { + widgetPromise = Promise.resolve(controls); + } else if (moduleName === "@jupyter-widgets/base") { + widgetPromise = Promise.resolve(base); + } else { + widgetPromise = customWidgetLoader(moduleName, moduleVersion); + } + + return widgetPromise.then(function(module: any) { if (module[className]) { return module[className]; } else { From 8dc6a5e3789274c3b53d74b7d8b23ef7181de5cb Mon Sep 17 00:00:00 2001 From: vivek1729 <1658576+vivek1729@users.noreply.github.com> Date: Wed, 11 Nov 2020 11:14:09 -0800 Subject: [PATCH 07/11] vp - Fix documentation --- packages/jupyter-widgets/src/manager/widget-loader.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/jupyter-widgets/src/manager/widget-loader.ts b/packages/jupyter-widgets/src/manager/widget-loader.ts index f96c006..4298133 100644 --- a/packages/jupyter-widgets/src/manager/widget-loader.ts +++ b/packages/jupyter-widgets/src/manager/widget-loader.ts @@ -54,7 +54,6 @@ function requirePromise(pkg: string | string[]): Promise { /** * Initialize dependencies that need to be preconfigured for requireJS module loading * Here, we define the jupyter-base, controls package that most 3rd party widgets depend on - * We also override the cdn end point (if applicable) */ export function initRequireDeps(){ // Export the following for `requirejs`. @@ -71,7 +70,7 @@ export function initRequireDeps(){ * "data-jupyter-widgets-cdn" on a script tag of the page. */ export function overrideCDNBaseURL(){ - // find the data-cdn for any script tag, assuming it is only used for embed-amd.js + // find the data-cdn for any script tag const scripts = document.getElementsByTagName("script"); Array.prototype.forEach.call(scripts, (script: HTMLScriptElement) => { cdn = script.getAttribute("data-jupyter-widgets-cdn") || cdn; @@ -81,12 +80,10 @@ export function overrideCDNBaseURL(){ } /** - * Load an amd module locally and fall back to specified CDN if unavailable. + * Load an amd module from a specified CDN * - * @param moduleName The name of the module to load.. + * @param moduleName The name of the module to load. * @param moduleVersion The semver range for the module, if loaded from a CDN. - * @param succssCB Callback when the module is loaded successfully by requireJS - * @param errorCB Called to hand off any errors encountered during module loading * * By default, the CDN service used is unpkg.com. However, this default can be * overriden by specifying another URL via the HTML attribute From 4e8eba620f6c8e441482a1b60a624ff61bf25cfb Mon Sep 17 00:00:00 2001 From: vivek1729 <1658576+vivek1729@users.noreply.github.com> Date: Wed, 11 Nov 2020 12:12:05 -0800 Subject: [PATCH 08/11] vp - Minor formatting fix --- packages/jupyter-widgets/src/manager/widget-loader.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/jupyter-widgets/src/manager/widget-loader.ts b/packages/jupyter-widgets/src/manager/widget-loader.ts index 4298133..c1b3e74 100644 --- a/packages/jupyter-widgets/src/manager/widget-loader.ts +++ b/packages/jupyter-widgets/src/manager/widget-loader.ts @@ -43,10 +43,11 @@ function requirePromise(pkg: string | string[]): Promise { return new Promise((resolve, reject) => { const require = (window as any).requirejs; if (require === undefined) { - reject('Requirejs is needed, please ensure it is loaded on the page.'); - } else { - // tslint:disable-next-line: non-literal-require - require(pkg, resolve, reject); + reject('Requirejs is needed, please ensure it is loaded on the page.'); + } + else { + // tslint:disable-next-line: non-literal-require + require(pkg, resolve, reject); } }); }; From f2ce58992dd2f7ffe0b6a17988061658f115b105 Mon Sep 17 00:00:00 2001 From: vivek1729 <1658576+vivek1729@users.noreply.github.com> Date: Fri, 13 Nov 2020 08:17:04 -0800 Subject: [PATCH 09/11] vp - Update README --- packages/jupyter-widgets/README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/jupyter-widgets/README.md b/packages/jupyter-widgets/README.md index 1dcf145..e574235 100644 --- a/packages/jupyter-widgets/README.md +++ b/packages/jupyter-widgets/README.md @@ -28,7 +28,27 @@ export default class MyNotebookApp extends ReactComponent { ## Documentation -We're working on adding more documentation for this component. Stay tuned by watching this repository! +The `jupyter-widgets` package supports two types of widgets: +- Standard widgets provided in the official [`jupyter-widgets/base`](https://www.npmjs.com/package/@jupyter-widgets/base) and [`jupyter-widgets/controls`](https://www.npmjs.com/package/@jupyter-widgets/controls) package +- [Custom Widgets](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Custom.html) or 3rd party widgets authored by the OSS community + +The `WidgetDisplay` component has an additional prop named `customWidgetLoader` to provide custom loaders for fetching 3rd party widgets. A reference implementation for a custom loader which serves as the default for this package can be found in `widget-loader.ts`. + +```typescript +customWidgetLoader?: (moduleName: string, moduleVersion: string) => Promise; +``` + +### Custom Widgets + +Since custom widgets are hosted on CDN, we set https://unkpg.com as our default CDN Base URL. The default base URL can be overriden by specifying another URL via the HTML attribute "data-jupyter-widgets-cdn" on any script tag of the page. + +For instance if your js bundle is loaded as `bundle.js` on your page and you wanted to set [jsdelivr](https://www.jsdelivr.com) as your default CDN url for custom widgets, you could do the following: +```html + +``` +Note: Custom widgets are fetched and loaded using the [requireJS](https://requirejs.org/) library. Please ensure that the library is loaded on your page and that the `require` and `define` APIs are available on the `window` object. We attempt to detect the presence of these APIs and emit a warning that custom widgets won't work when `requirejs` is missing. + + ## Support From 8e89ce8f66c50324221e7e38082c376af808539e Mon Sep 17 00:00:00 2001 From: vivek1729 <1658576+vivek1729@users.noreply.github.com> Date: Fri, 20 Nov 2020 12:56:37 -0800 Subject: [PATCH 10/11] Update packages/jupyter-widgets/README.md Co-authored-by: Safia Abdalla --- packages/jupyter-widgets/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jupyter-widgets/README.md b/packages/jupyter-widgets/README.md index e574235..a208554 100644 --- a/packages/jupyter-widgets/README.md +++ b/packages/jupyter-widgets/README.md @@ -42,7 +42,7 @@ customWidgetLoader?: (moduleName: string, moduleVersion: string) => Promise Since custom widgets are hosted on CDN, we set https://unkpg.com as our default CDN Base URL. The default base URL can be overriden by specifying another URL via the HTML attribute "data-jupyter-widgets-cdn" on any script tag of the page. -For instance if your js bundle is loaded as `bundle.js` on your page and you wanted to set [jsdelivr](https://www.jsdelivr.com) as your default CDN url for custom widgets, you could do the following: +For instance if your JavaScript bundle is loaded as `bundle.js` on your page and you wanted to set [jsdelivr](https://www.jsdelivr.com) as your default CDN url for custom widgets, you could do the following: ```html ``` From e693ef4ef27591c012af0583e57a9d73746c5424 Mon Sep 17 00:00:00 2001 From: vivek1729 <1658576+vivek1729@users.noreply.github.com> Date: Fri, 20 Nov 2020 13:03:27 -0800 Subject: [PATCH 11/11] vp - Add link to requireJS docs for error message --- packages/jupyter-widgets/src/manager/widget-loader.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/jupyter-widgets/src/manager/widget-loader.ts b/packages/jupyter-widgets/src/manager/widget-loader.ts index c1b3e74..4054f2a 100644 --- a/packages/jupyter-widgets/src/manager/widget-loader.ts +++ b/packages/jupyter-widgets/src/manager/widget-loader.ts @@ -6,6 +6,7 @@ import * as base from "@jupyter-widgets/base"; import * as controls from "@jupyter-widgets/controls"; +const requireJSMissingErrorMessage = "Requirejs is needed, please ensure it is loaded on the page. Docs - https://requirejs.org/docs/api.html"; let cdn = "https://unpkg.com"; /** @@ -43,7 +44,7 @@ function requirePromise(pkg: string | string[]): Promise { return new Promise((resolve, reject) => { const require = (window as any).requirejs; if (require === undefined) { - reject('Requirejs is needed, please ensure it is loaded on the page.'); + reject(requireJSMissingErrorMessage); } else { // tslint:disable-next-line: non-literal-require @@ -95,7 +96,7 @@ export function overrideCDNBaseURL(){ export function requireLoader(moduleName: string, moduleVersion: string): Promise { const require = (window as any).requirejs; if (require === undefined) { - return Promise.reject(new Error("Requirejs is needed, please ensure it is loaded on the page.")); + return Promise.reject(new Error(requireJSMissingErrorMessage)); } else { const conf: { paths: { [key: string]: string } } = { paths: {} };