Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transpile (optional) TypeScript, support imports (package and relative) and schema #28

Merged
merged 43 commits into from
Mar 24, 2022

Conversation

krassowski
Copy link
Member

@krassowski krassowski commented Jan 2, 2022

This is a follow up to #26 based on the discussion held in there. It is also a first step towards #25 and it relevant to #5. The ultimate goal is to make all the self-contained examples from jupyterlab/extension-examples just work without modification, including:

  • menus from schema
  • examples with multiple typescript files

but excluding importing styles (ultimately we could handle those too).

In short, this PR uses TypeScript Compiler API to transpile the code into CommonJS understood by browsers (without type checking!) and TypeScript AST Transformer API with minor modifications:

  • require() calls are replaced by async require()
  • exports are defined as an object on top of the module and returned in the body of new async function which serves the purpose of module executor

The require function is provided to the executor and depending on the module it returns:

  • if the imported value comes from a known package (a hard-coded, auto-generated list), which is available at runtime, the value will be passed at runtime
  • the loaded module from another file retrieved with contents API; circular imports are not supported
  • otherwise, requirejs is used; to avoid breaking the flow an await is used (the module loading is now done in n asynchronous function to accommodate that); as before, this will only work for AMD modules - and no further transpilation nor transformation is applied to the code retrieved by requirejs. To allow retrieval without explicitly passing the CDN URL, a new setting is added with a default pointing to https://cdn.jsdelivr.net/npm.
  • the expression given by export default is returned at the end, which gives us the plugin object

The proposed changes are backwards compatible - if no export is found, the old loading mechanism will be used instead.

The code can, but does not have to, be typed. Users who scrupulously annotate variables with types will be rewarded by better completer suggestions (at least on Binder where LSP is automatically installed).

Screenshot from 2022-01-02 03-53-51

Punchlist:

Follow up items:

  • maybe randomise the name of the import function in the transpiled code to avoid users using it as if it was an API?
  • integrate SystemJS in the resolver as loader for ES6 modules (and CSS, and other resources)
  • add tests:
    • jest: transformer is easily testable with unit tests, but it might be replaced in a subsequent PR by a babel-based transformer if we integrate with SystemJS so I don't feel like writing it now
    • aspiration: run the same exact visual test as in extension-examples on those example; this is a big task and I would prefer to do this in a subsequent PR
  • support SRI: There should be a way to verify subresource integrity #30

Additionally:

  • plugins with the same ID can now be loaded multiple times
  • the addition of LSP is carried over from Add LSP to the Binder image #26
  • error handling was added to display useful error messages to users when any of the steps: loading, imports, or activation fail
  • once JupyterLab 3.3 gets released, the file editor will get a button to run the code as an extension
  • an icon to create and open a new TypeScript file is now displayed in the launcher

Screenshot from 2022-01-02 03-24-16

and when pressed, it creates and opens a pre-populated minimal hello word plugin.

Screenshot from 2022-01-02 03-24-07

@krassowski
Copy link
Member Author

Binder 👈 Launch a binder notebook on branch krassowski:transpile-tokens

@krassowski krassowski marked this pull request as ready for review January 4, 2022 03:57
@jtpio jtpio added the enhancement New feature or request label Jan 4, 2022
@jtpio
Copy link
Member

jtpio commented Jan 4, 2022

Thanks @krassowski this is looking great!

add submodule for testing extension-exmaples?

Or we pull the repo when creating the Binder, in postBuild? An alternative would be to use the released version of jupyterlab-plugin-playground on the Binder of the examples repo.

while we are left with non-pretty replacements of `require` with
`await require`, maybe those could be performed in an `after`
transformer in the future.

The advantage of this approach is in lower maintenance burden
(less to rewrite on each TypeScript major version bump) and
a working implementation of exports, over quite complex custom
transformer which was not fully functional for some edge cases.

The disadvantage is that the transpiled code looks less like
the original making debugging harder for plugin users.
@krassowski krassowski changed the title Transpile (optional) TypeScript, support tokens and package imports Transpile (optional) TypeScript, support imports (package and relative) and schema Jan 9, 2022
@krassowski
Copy link
Member Author

krassowski commented Jan 9, 2022

This is now ready for review (and hopefully a merge).

There was a major change in 0599fbc (followed up by cleanup in c1b011b): transpilation module target is now CommonJS as it turned out to be much easier to work with for full exports support (which is required for support of multiple files) - we just need to add await in front of require, define exports object and return it, instead of manually extracting this information. The net change is -321 LoC, and it this should be much more maintainable now.

Also, with newly added multi-file and schema support most examples in extension-examples now work out of the box. There are two three loaders which are missing:

  • CSS styles
  • SVG strings
  • react (tsx files)

the first can be easily added in a follow up PR; the tsc compiler also supports react but it might require some more changes (I don't know yet).

Ideally we would require a `.d.ts`, but for a dynamic
playground I think it is fine to accept svg based on extension
@krassowski
Copy link
Member Author

I added support for React/tsx as as it was just a simple switch in tsc config. I don't plan to work on adding any more features. I tested most of the extensions and they just work :)

There is a corner case of some extension examples using default import for React. It works fine if React is imported using:

import * as React from 'react';

but fails when it is imported using a default import as in:

import React from 'react';

Some extension examples do the former (e.g. kernel-messaging) and some the latter (e.g. custom-log-console). This comes down to allowSyntheticDefaultImports/esModuleInterop and can be easily fixed by exposing the namespace as default when default is not present (so re-implementing synthetic default).

@krassowski
Copy link
Member Author

There is a corner case of some extension examples using default import for React

Fixed in 580c723.

@meeseeksmachine
Copy link

This pull request has been mentioned on Jupyter Community Forum. There might be relevant details there:

https://discourse.jupyter.org/t/what-is-the-2022-way-to-display-javascript-in-a-python-notebook-in-jupyter-lab/12318/5

Copy link
Member

@echarles echarles left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @krassowski . This PR works for me (tested also with a few examples from https://github.com/jupyterlab/extension-examples). I have left a few comments.

let result;
try {
result = await pluginLoader.load(code, path);
} catch (error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to show a dialog in case of success.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. Ideally this would not be a dialog (which could be disruptive) but a notification. I guess we could defer until the notifications are in core.

await app.activatePlugin(plugin.id);
// Unregister plugin if already registered.
if (this.app.hasPlugin(plugin.id)) {
delete (this.app as any)._pluginMap[plugin.id];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The leaf/dependent plugins are not unloaded (see jupyterlab/lumino#278 (comment)). I guess this is ok for now

@@ -0,0 +1,107 @@
import * as jupyter_widgets_base from '@jupyter-widgets/base';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this list of predefined modules be dynamic (maybe a feature for later?)

} catch (e) {
showDialog({
title: `Plugin autostart failed: ${(e as Error).message}`,
body: formatErrorWithResult(e, result)
Copy link
Member

@echarles echarles Jan 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My VS Code complains with Argument of type 'unknown' is not assignable to parameter of type 'Error' . However, no issue when building from the CLI.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe VS Code is using a different or more version of typescript?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may have useUnknownInCatchVariables enabled (or strict). It is not enabled in main JupyterLab repo, but we could enable it here.

Co-authored-by: Eric Charles <[email protected]>
@jtpio
Copy link
Member

jtpio commented Feb 16, 2022

Looks like there is a small conflict to fix in yarn.lock, and a couple of inline comments.

Otherwise it works great on Binder, and we should consider getting this in soon!

@krassowski
Copy link
Member Author

I would say ready to be merged.

Copy link
Member

@jtpio jtpio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice, thanks!

@jtpio
Copy link
Member

jtpio commented Mar 24, 2022

Let's get this in an iterate 🚀

@jtpio jtpio merged commit d292779 into jupyterlab:master Mar 24, 2022
@jtpio
Copy link
Member

jtpio commented Mar 24, 2022

Starting a new release with this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants