From 3011c5bf3d5bb130e14cdb0f59ed2252389c247d Mon Sep 17 00:00:00 2001 From: Mario Buikhuizen Date: Fri, 22 Dec 2023 17:12:01 +0100 Subject: [PATCH] refactor: add linting (#51) --- .eslintignore | 5 - .eslintrc.js | 28 ----- .github/workflows/build.yml | 3 +- .github/workflows/code-quality.yml | 24 ++++ .github/workflows/deploy.yml | 4 +- .pre-commit-config.yaml | 13 ++ .prettierrc | 3 - README.md | 73 ++++++----- babel.config.js | 6 +- docs/environment.yml | 1 - docs/source/_static/helper.js | 2 +- docs/source/conf.py | 100 +++++++-------- examples/my_component.tsx | 4 +- examples/styles_orange.css | 12 +- ipyreact/__init__.py | 26 ++-- ipyreact/basic.tsx | 21 ++-- ipyreact/cellmagic.py | 51 ++++++-- ipyreact/nbextension/extension.js | 51 ++++---- ipyreact/widget.py | 29 ++--- jest.config.js | 16 +-- lab/jupyter-lite.json | 2 +- package.json | 2 - pyproject.toml | 18 +++ src/__tests__/index.spec.ts | 18 +-- src/__tests__/utils.ts | 28 ++--- src/components.tsx | 53 ++++---- src/extension.ts | 6 +- src/index.ts | 4 +- src/plugin.ts | 14 +-- src/utils.ts | 56 +++++---- src/version.ts | 2 +- src/widget.tsx | 195 ++++++++++++++++------------- tests/ui/event_test.py | 3 +- tests/ui/jupyter_test.py | 8 +- tests/ui/library_test.py | 10 +- tests/unit/create_test.py | 4 +- tsconfig.eslint.json | 5 - tsconfig.json | 7 +- webpack.config.js | 88 ++++++------- 39 files changed, 541 insertions(+), 454 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc.js create mode 100644 .github/workflows/code-quality.yml create mode 100644 .pre-commit-config.yaml delete mode 100644 .prettierrc delete mode 100644 tsconfig.eslint.json diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index e8a2221..0000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules -dist -coverage -**/*.d.ts -tests \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 9fb27ea..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = { - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended' - ], - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'tsconfig.eslint.json', - sourceType: 'module' - }, - plugins: ['@typescript-eslint'], - rules: { - '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-namespace': 'off', - '@typescript-eslint/no-use-before-define': 'off', - '@typescript-eslint/quotes': [ - 'error', - 'single', - { avoidEscape: true, allowTemplateLiterals: false } - ], - curly: ['error', 'all'], - eqeqeq: 'error', - 'prefer-arrow-callback': 'error' - } -}; \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 222a836..5ecefe7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ on: branches: - master tags: - - 'v*' + - "v*" pull_request: workflow_dispatch: @@ -52,7 +52,6 @@ jobs: ./dist ./*.tgz - test: needs: [build] runs-on: ubuntu-20.04 diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..aaa0203 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,24 @@ +name: code-quality + +on: + pull_request: + push: + branches: [main] + +jobs: + pre-commit: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.7 + - name: Install dependencies + run: | + pip install ".[dev]" + pre-commit install + - name: run pre-commit + run: | + pre-commit run --all-files diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4f24ab6..4a73e47 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,7 +6,7 @@ on: - master pull_request: branches: - - '*' + - "*" jobs: build: @@ -17,7 +17,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: "3.10" - name: Install the dependencies run: | python -m pip install -r .jupyterlite/requirements.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c6b58af --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,13 @@ +repos: + - repo: "https://github.com/pre-commit/mirrors-prettier" + rev: "v3.1.0" + hooks: + - id: prettier + stages: [commit] + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: "v0.1.9" + hooks: + - id: ruff + stages: [commit] + - id: ruff-format + stages: [commit] diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index dc2fb82..0000000 --- a/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "singleQuote": true -} \ No newline at end of file diff --git a/README.md b/README.md index df5cf90..d775e01 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,17 @@ - # ipyreact - - React for ipywidgets that just works. No webpack, no npm, no hassle. Just write jsx, tsx and python. Build on top of [AnyWidget](https://anywidget.dev/). ## Tutorial - This tutorial will walk you through the steps of building a complete ipywidget with react. +This tutorial will walk you through the steps of building a complete ipywidget with react. [![JupyterLight](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://widgetti.github.io/ipyreact/lab/?path=full_tutorial.ipynb) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/widgetti/ipyreact/HEAD?labpath=examples%2Ffull_tutorial.ipynb) - - - Just click the JupyterLite or Binder link to start the interactive walkthrough. +Just click the JupyterLite or Binder link to start the interactive walkthrough. ## Goal @@ -45,7 +40,6 @@ ConfettiWidget() ![initial-30-fps-compressed](https://user-images.githubusercontent.com/1765949/233469170-c659b670-07f5-4666-a201-80dea01ebabe.gif) - ### Hot reloading Create a tsx file: @@ -55,14 +49,17 @@ Create a tsx file: import confetti from "canvas-confetti"; import * as React from "react"; -export default function({value, set_value, debug}) { - return -}; + ); +} ``` And use it in your python code: + ```python import ipyreact import pathlib @@ -81,11 +78,13 @@ Now edit, save, and see the changes in your browser/notebook. ### IPython magic First load the ipyreact extension: + ```python %load_ext ipyreact ``` Then use the `%%react` magic to directly write jsx/tsx in your notebook: + ```tsx %%react import confetti from "canvas-confetti"; @@ -102,8 +101,6 @@ Access the underlying widget with the name `_last_react_widget` (e.g. `_last_rea ![magic-optimized](https://user-images.githubusercontent.com/1765949/233471041-62e807d6-c16d-4fc5-af5d-13c0acb2c677.gif) - - ## Installation You can install using `pip`: @@ -113,15 +110,16 @@ pip install ipyreact ``` ## Usage + ## Facts - * The ReactWidget has an `value` trait, which is a `traitlets.Any` trait. Use this to pass data to your react component, or to get data back from your react component. - * All traits are added as props to your react component (e.g. `{value, ...}` in th example above. - * For every trait `ipyreact` automatically provides a `set_` callback, which you can use to set the trait value from your react component (e.g. `set_value` in the example above). (*Note: we used `on_value` before, this is now deprecated*) - * Your code gets transpiled using [sucrase](https://github.com/alangpierce/sucrase) in the frontend, no bundler needed. - * Your code should be written in ES modules. - * Set `debug=True` to get more debug information in the browser console (also accessible in the props). - * Make sure you export a default function from your module (e.g. `export default function MyComponent() { ... }`). This is the component that will be rendered. +- The ReactWidget has an `value` trait, which is a `traitlets.Any` trait. Use this to pass data to your react component, or to get data back from your react component. +- All traits are added as props to your react component (e.g. `{value, ...}` in th example above. +- For every trait `ipyreact` automatically provides a `set_` callback, which you can use to set the trait value from your react component (e.g. `set_value` in the example above). (_Note: we used `on_value` before, this is now deprecated_) +- Your code gets transpiled using [sucrase](https://github.com/alangpierce/sucrase) in the frontend, no bundler needed. +- Your code should be written in ES modules. +- Set `debug=True` to get more debug information in the browser console (also accessible in the props). +- Make sure you export a default function from your module (e.g. `export default function MyComponent() { ... }`). This is the component that will be rendered. ### Import maps @@ -141,38 +139,45 @@ _import_map = { } ``` -Which means we can copy paste *most* of the examples from [mui](https://mui.com/) +Which means we can copy paste _most_ of the examples from [mui](https://mui.com/) ```tsx %%react -n my_widget -d -import Button from '@mui/material/Button'; +import Button from "@mui/material/Button"; import confetti from "canvas-confetti"; import * as React from "react"; -export default function({value, set_value, debug}) { - if(debug) { - console.log("value=", value, set_value); - } - return -}; + ); +} ``` We add the https://github.com/guybedford/es-module-shims shim to the browser page for the import maps functionality. - ## Development Installation Create a dev environment: + ```bash conda create -n ipyreact-dev -c conda-forge nodejs yarn python jupyterlab conda activate ipyreact-dev ``` Install the python. This will also build the TS package. + ```bash -pip install -e ".[test, examples]" +pip install -e ".[test, examples, dev]" +pre-commit install ``` When developing your extensions, you need to manually enable your extensions with the @@ -196,7 +201,9 @@ you might also need another flag instead of `--sys-prefix`, but we won't cover t of those flags here. ### How to see your changes + #### Typescript: + If you use JupyterLab to develop then you can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the widget. @@ -210,5 +217,5 @@ jupyter lab After a change wait for the build to finish and then refresh your browser and the changes should take effect. #### Python: -If you make a change to the python code then you will need to restart the notebook kernel to have it take effect. +If you make a change to the python code then you will need to restart the notebook kernel to have it take effect. diff --git a/babel.config.js b/babel.config.js index bbc789a..c900931 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,11 +1,11 @@ module.exports = { - sourceMap: 'inline', + sourceMap: "inline", presets: [ [ - '@babel/preset-env', + "@babel/preset-env", { targets: { - node: 'current', + node: "current", }, }, ], diff --git a/docs/environment.yml b/docs/environment.yml index b2ee135..92f3757 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -1,4 +1,3 @@ - name: ipyreact_docs channels: - conda-forge diff --git a/docs/source/_static/helper.js b/docs/source/_static/helper.js index cb2e2a8..b05fd1c 100644 --- a/docs/source/_static/helper.js +++ b/docs/source/_static/helper.js @@ -1,5 +1,5 @@ var cache_require = window.require; -window.addEventListener('load', function() { +window.addEventListener("load", function () { window.require = cache_require; }); diff --git a/docs/source/conf.py b/docs/source/conf.py index f3fb985..6148b0c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -23,14 +23,14 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.intersphinx', - 'sphinx.ext.napoleon', - 'sphinx.ext.todo', - 'nbsphinx', - 'jupyter_sphinx', - 'nbsphinx_link', + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx.ext.todo", + "nbsphinx", + "jupyter_sphinx", + "nbsphinx_link", ] # Set the nbsphinx JS path to empty to avoid showing twice of the widgets @@ -39,28 +39,30 @@ # Ensure our extension is available: import sys -from os.path import dirname, join as pjoin +from os.path import dirname +from os.path import join as pjoin + docs = dirname(dirname(__file__)) root = dirname(docs) sys.path.insert(0, root) -sys.path.insert(0, pjoin(docs, 'sphinxext')) +sys.path.insert(0, pjoin(docs, "sphinxext")) # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'ipyreact' -copyright = '2023, Maarten A. Breddels' -author = 'Maarten A. Breddels' +project = "ipyreact" +copyright = "2023, Maarten A. Breddels" +author = "Maarten A. Breddels" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -71,17 +73,18 @@ # get version from python package: import os + here = os.path.dirname(__file__) -repo = os.path.join(here, '..', '..') -_version_py = os.path.join(repo, 'ipyreact', '_version.py') +repo = os.path.join(here, "..", "..") +_version_py = os.path.join(repo, "ipyreact", "_version.py") version_ns = {} with open(_version_py) as f: exec(f.read(), version_ns) # The short X.Y version. -version = '%i.%i' % version_ns['version_info'][:2] +version = "%i.%i" % version_ns["version_info"][:2] # The full version, including alpha/beta/rc tags. -release = version_ns['__version__'] +release = version_ns["__version__"] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -93,10 +96,10 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['**.ipynb_checkpoints'] +exclude_patterns = ["**.ipynb_checkpoints"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -114,13 +117,13 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'ipyreactdoc' +htmlhelp_basename = "ipyreactdoc" # -- Options for LaTeX output --------------------------------------------- @@ -129,15 +132,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -147,8 +147,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'ipyreact.tex', 'ipyreact Documentation', - 'Maarten A. Breddels', 'manual'), + (master_doc, "ipyreact.tex", "ipyreact Documentation", "Maarten A. Breddels", "manual"), ] @@ -156,12 +155,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, - 'ipyreact', - 'ipyreact Documentation', - [author], 1) -] +man_pages = [(master_doc, "ipyreact", "ipyreact Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -170,27 +164,30 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, - 'ipyreact', - 'ipyreact Documentation', - author, - 'ipyreact', - 'React for ipywidgets that just works', - 'Miscellaneous'), + ( + master_doc, + "ipyreact", + "ipyreact Documentation", + author, + "ipyreact", + "React for ipywidgets that just works", + "Miscellaneous", + ), ] # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} +intersphinx_mapping = {"https://docs.python.org/": None} # Read The Docs # on_rtd is whether we are on readthedocs.org, this line of code grabbed from # docs.readthedocs.org -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +on_rtd = os.environ.get("READTHEDOCS", None) == "True" if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' + + html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # otherwise, readthedocs.org uses their theme by default, so no need to specify it @@ -199,15 +196,18 @@ # Uncomment this line if you have know exceptions in your included notebooks # that nbsphinx complains about: # -nbsphinx_allow_errors = True # exception ipstruct.py ipython_genutils +nbsphinx_allow_errors = True # exception ipstruct.py ipython_genutils from sphinx.util import logging + logger = logging.getLogger(__name__) + def setup(app): def add_scripts(app): - for fname in ['helper.js', 'embed-bundle.js']: - if not os.path.exists(os.path.join(here, '_static', fname)): - logger.warning('missing javascript file: %s' % fname) + for fname in ["helper.js", "embed-bundle.js"]: + if not os.path.exists(os.path.join(here, "_static", fname)): + logger.warning("missing javascript file: %s" % fname) app.add_js_file(fname) - app.connect('builder-inited', add_scripts) + + app.connect("builder-inited", add_scripts) diff --git a/examples/my_component.tsx b/examples/my_component.tsx index 19c2103..cf45f16 100644 --- a/examples/my_component.tsx +++ b/examples/my_component.tsx @@ -1,5 +1,5 @@ import * as React from "react"; export default function MyButton() { - return -}; \ No newline at end of file + return ; +} diff --git a/examples/styles_orange.css b/examples/styles_orange.css index b63ec90..d31bc51 100644 --- a/examples/styles_orange.css +++ b/examples/styles_orange.css @@ -1,7 +1,7 @@ button { - color: orange; - border-color: orange; - font-weight: bold; - border-width: 2.5px; - user-select: none; -} \ No newline at end of file + color: orange; + border-color: orange; + font-weight: bold; + border-width: 2.5px; + user-select: none; +} diff --git a/ipyreact/__init__.py b/ipyreact/__init__.py index bb51a38..e18be06 100644 --- a/ipyreact/__init__.py +++ b/ipyreact/__init__.py @@ -20,10 +20,12 @@ def _jupyter_labextension_paths(): from `src` directory into /labextensions/ directory during widget installation """ - return [{ - 'src': 'labextension', - 'dest': 'jupyter-react', - }] + return [ + { + "src": "labextension", + "dest": "jupyter-react", + } + ] def _jupyter_nbextension_paths(): @@ -42,13 +44,17 @@ def _jupyter_nbextension_paths(): require: Path to importable AMD Javascript module inside the /nbextensions/ directory """ - return [{ - 'section': 'notebook', - 'src': 'nbextension', - 'dest': 'jupyter-react', - 'require': 'jupyter-react/extension' - }] + return [ + { + "section": "notebook", + "src": "nbextension", + "dest": "jupyter-react", + "require": "jupyter-react/extension", + } + ] + def load_ipython_extension(ipython): from .cellmagic import load_ipython_extension + load_ipython_extension(ipython) diff --git a/ipyreact/basic.tsx b/ipyreact/basic.tsx index 0180479..dbcdd38 100644 --- a/ipyreact/basic.tsx +++ b/ipyreact/basic.tsx @@ -1,10 +1,17 @@ -import Button from '@mui/material/Button'; +import Button from "@mui/material/Button"; import confetti from "canvas-confetti"; import * as React from "react"; -export default function({value, on_value, debug}) { - if(debug) { - console.log("value=", value, on_value); - } - return -}; +export default function ({ value, on_value, debug }) { + if (debug) { + console.log("value=", value, on_value); + } + return ( + + ); +} diff --git a/ipyreact/cellmagic.py b/ipyreact/cellmagic.py index 9cc56ce..42a66c8 100644 --- a/ipyreact/cellmagic.py +++ b/ipyreact/cellmagic.py @@ -14,9 +14,30 @@ class ReactMagics(Magics): @needs_local_scope @magic_arguments() - @argument('-n', '--name', type=str, default="_last_react_widget", help='Name of the widget variable injected into the local namespace (default = _last_react_widget).') - @argument('-d', '--debug', action='store_true', default=False, help='Show debug information in the JS console.') - @argument('-c', '--cleanup', action='store_true', default=False, help='Destroy the previous widget before creating a new one.') + @argument( + "-n", + "--name", + type=str, + default="_last_react_widget", + help=( + "Name of the widget variable injected into the local namespace", + " (default = _last_react_widget).", + ), + ) + @argument( + "-d", + "--debug", + action="store_true", + default=False, + help="Show debug information in the JS console.", + ) + @argument( + "-c", + "--cleanup", + action="store_true", + default=False, + help="Destroy the previous widget before creating a new one.", + ) @cell_magic def react(self, line, cell, local_ns): """Excute react code in a cell. @@ -25,29 +46,37 @@ def react(self, line, cell, local_ns): %%react -n my_widget -d import Button from '@mui/material/Button'; import confetti from "canvas-confetti"; - import * as React from "react"; + import * as React from "react"; export default function({value, on_value, debug}) { - if(debug) { - console.log("value=", value, on_value); - } - return - }; + ); + } """ args = parse_argstring(ReactMagics.react, line) if args.cleanup and args.name in local_ns: local_ns[args.name].close() code = self.shell.transform_cell(cell) + class Widget(widget.ReactWidget): _esm = code + react_widget = Widget(_esm=code, debug=args.debug) local_ns[args.name] = react_widget display(react_widget) + def load_ipython_extension(ipython): """ Use `%load_ext ipyreact` """ - ipython.register_magics(ReactMagics) \ No newline at end of file + ipython.register_magics(ReactMagics) diff --git a/ipyreact/nbextension/extension.js b/ipyreact/nbextension/extension.js index d248212..7977f92 100644 --- a/ipyreact/nbextension/extension.js +++ b/ipyreact/nbextension/extension.js @@ -1,26 +1,31 @@ // Entry point for the notebook bundle containing custom model definitions. // -define(function() { - "use strict"; +define(function () { + "use strict"; - window['requirejs'].config({ - map: { - '*': { - '@widgetti/jupyter-react': 'nbextensions/jupyter-react/index', - // 'jupyter-react16': 'nbextensions/ipyreact/index16', - }, - } - }); - // Export the required load_ipython_extension function - return { - load_ipython_extension : function() { - require(['notebook/js/codecell'], function(codecell) { - codecell.CodeCell.options_default.highlight_modes['text/typescript-jsx'] = {'reg':[/^%%(ipy)?react/]} ; - IPython.notebook.events.one('kernel_ready.Kernel', function(){ - IPython.notebook.get_cells().map(function(cell){ - if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ; - }); - }); - } - }; -}); \ No newline at end of file + window["requirejs"].config({ + map: { + "*": { + "@widgetti/jupyter-react": "nbextensions/jupyter-react/index", + // 'jupyter-react16': 'nbextensions/ipyreact/index16', + }, + }, + }); + // Export the required load_ipython_extension function + return { + load_ipython_extension: function () { + require(["notebook/js/codecell"], function (codecell) { + codecell.CodeCell.options_default.highlight_modes[ + "text/typescript-jsx" + ] = { reg: [/^%%(ipy)?react/] }; + IPython.notebook.events.one("kernel_ready.Kernel", function () { + IPython.notebook.get_cells().map(function (cell) { + if (cell.cell_type == "code") { + cell.auto_highlight(); + } + }); + }); + }); + }, + }; +}); diff --git a/ipyreact/widget.py b/ipyreact/widget.py index 7c5b4f5..1359f6c 100644 --- a/ipyreact/widget.py +++ b/ipyreact/widget.py @@ -11,7 +11,6 @@ from pathlib import Path import anywidget -from ipywidgets import DOMWidget from traitlets import Any, Bool, Dict, Int, List, Unicode from ._frontend import module_name, module_version @@ -20,12 +19,12 @@ class ReactWidget(anywidget.AnyWidget): - """TODO: Add docstring here - """ - _model_name = Unicode('ReactModel').tag(sync=True) + """TODO: Add docstring here""" + + _model_name = Unicode("ReactModel").tag(sync=True) _model_module = Unicode(module_name).tag(sync=True) _model_module_version = Unicode(module_version).tag(sync=True) - _view_name = Unicode('ReactView').tag(sync=True) + _view_name = Unicode("ReactView").tag(sync=True) _view_module = Unicode(module_name).tag(sync=True) _view_module_version = Unicode(module_version).tag(sync=True) value = Any(None, allow_none=True).tag(sync=True) @@ -41,30 +40,26 @@ class ReactWidget(anywidget.AnyWidget): "@mui/icons-material/": "https://esm.sh/@mui/icons-material/", "canvas-confetti": "https://esm.sh/canvas-confetti@1.6.0", }, - "scopes": { - }, + "scopes": {}, } _esm = HERE / Path("basic.tsx") def __init__(self, **kwargs) -> None: _import_map = kwargs.pop("_import_map", {}) _import_map = { - "imports": { - **self._import_map_default["imports"], - **_import_map.get("imports", {}) - }, - "scopes": { - **self._import_map_default["scopes"], - **_import_map.get("scopes", {}) - }, + "imports": {**self._import_map_default["imports"], **_import_map.get("imports", {})}, + "scopes": {**self._import_map_default["scopes"], **_import_map.get("scopes", {})}, } kwargs["_import_map"] = _import_map _ignore = ["on_msg", "on_displayed", "on_trait_change", "on_widget_constructed"] - _event_names = [method_name[3:] for method_name in dir(self) if method_name.startswith("on_") and method_name not in _ignore] + _event_names = [ + method_name[3:] + for method_name in dir(self) + if method_name.startswith("on_") and method_name not in _ignore + ] super().__init__(**{"_event_names": _event_names, **kwargs}) self.on_msg(self._handle_event) - def _handle_event(self, _, content, buffers): if "event_name" in content.keys(): event_name = content.get("event_name", "") diff --git a/jest.config.js b/jest.config.js index 79453a3..daa613c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,16 +1,16 @@ module.exports = { automock: false, moduleNameMapper: { - '\\.(css|less|sass|scss)$': 'identity-obj-proxy', + "\\.(css|less|sass|scss)$": "identity-obj-proxy", }, - preset: 'ts-jest/presets/js-with-babel', - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - testPathIgnorePatterns: ['/lib/', '/node_modules/'], - testRegex: '/__tests__/.*.spec.ts[x]?$', - transformIgnorePatterns: ['/node_modules/(?!(@jupyter(lab|-widgets)/.*)/)'], + preset: "ts-jest/presets/js-with-babel", + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + testPathIgnorePatterns: ["/lib/", "/node_modules/"], + testRegex: "/__tests__/.*.spec.ts[x]?$", + transformIgnorePatterns: ["/node_modules/(?!(@jupyter(lab|-widgets)/.*)/)"], globals: { - 'ts-jest': { - tsconfig: '/tsconfig.json', + "ts-jest": { + tsconfig: "/tsconfig.json", }, }, }; diff --git a/lab/jupyter-lite.json b/lab/jupyter-lite.json index 2a097f8..bbf743d 100644 --- a/lab/jupyter-lite.json +++ b/lab/jupyter-lite.json @@ -4,4 +4,4 @@ "settingsStorageDrivers": ["memoryStorageDriver"], "contentsStorageDrivers": ["memoryStorageDriver"] } -} \ No newline at end of file +} diff --git a/package.json b/package.json index c72bbf7..a726890 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,6 @@ "clean:lib": "rimraf lib", "clean:labextension": "rimraf ipyreact/labextension", "clean:nbextension": "rimraf ipyreact/nbextension/static/index.js", - "lint": "eslint . --ext .ts,.tsx --fix", - "lint:check": "eslint . --ext .ts,.tsx", "prepack": "yarn run build:lib", "test": "jest", "watch": "npm-run-all -p watch:*", diff --git a/pyproject.toml b/pyproject.toml index 8fc29e6..b06bcdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,9 @@ ui-test = [ "solara[pytest]", "pytest>=6.0", ] +dev = [ + "pre-commit", +] [project.urls] Homepage = "https://github.com/widgetti/ipyreact" @@ -116,3 +119,18 @@ regex = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)((?Pa|b|rc| [tool.tbump.git] message_template = "Bump to {new_version}" tag_template = "v{new_version}" + +[tool.ruff] +fix = true +exclude = [ + '.git', + 'dist', + '.eggs', +] +line-length = 100 +select = ["E", "W", "F", "Q", "I"] + +[tool.ruff.per-file-ignores] +"__init__.py" = ["F401"] +"docs/source/conf.py" = ["E402"] + diff --git a/src/__tests__/index.spec.ts b/src/__tests__/index.spec.ts index 407f930..2800ad9 100644 --- a/src/__tests__/index.spec.ts +++ b/src/__tests__/index.spec.ts @@ -4,23 +4,23 @@ // Add any needed widget imports here (or from controls) // import {} from '@jupyter-widgets/base'; -import { createTestModel } from './utils'; +import { createTestModel } from "./utils"; -import { ExampleModel } from '..'; +import { ExampleModel } from ".."; -describe('Example', () => { - describe('ExampleModel', () => { - it('should be createable', () => { +describe("Example", () => { + describe("ExampleModel", () => { + it("should be createable", () => { const model = createTestModel(ExampleModel); expect(model).toBeInstanceOf(ExampleModel); - expect(model.get('value')).toEqual('Hello World'); + expect(model.get("value")).toEqual("Hello World"); }); - it('should be createable with a value', () => { - const state = { value: 'Foo Bar!' }; + it("should be createable with a value", () => { + const state = { value: "Foo Bar!" }; const model = createTestModel(ExampleModel, state); expect(model).toBeInstanceOf(ExampleModel); - expect(model.get('value')).toEqual('Foo Bar!'); + expect(model.get("value")).toEqual("Foo Bar!"); }); }); }); diff --git a/src/__tests__/utils.ts b/src/__tests__/utils.ts index 0a61d63..eb372c2 100644 --- a/src/__tests__/utils.ts +++ b/src/__tests__/utils.ts @@ -1,9 +1,9 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -import * as widgets from '@jupyter-widgets/base'; -import * as baseManager from '@jupyter-widgets/base-manager'; -import * as services from '@jupyterlab/services'; +import * as widgets from "@jupyter-widgets/base"; +import * as baseManager from "@jupyter-widgets/base-manager"; +import * as services from "@jupyterlab/services"; let numComms = 0; @@ -29,18 +29,18 @@ export class MockComm implements widgets.IClassicComm { if (this._on_close) { this._on_close(); } - return 'dummy'; + return "dummy"; } send(): string { - return 'dummy'; + return "dummy"; } open(): string { - return 'dummy'; + return "dummy"; } comm_id: string; - target_name = 'dummy'; + target_name = "dummy"; _on_msg: ((x?: any) => void) | null = null; _on_close: ((x?: any) => void) | null = null; } @@ -48,19 +48,19 @@ export class MockComm implements widgets.IClassicComm { export class DummyManager extends baseManager.ManagerBase { constructor() { super(); - this.el = window.document.createElement('div'); + this.el = window.document.createElement("div"); } display_view( msg: services.KernelMessage.IMessage, view: widgets.DOMWidgetView, - options: any + options: any, ) { // TODO: make this a spy // TODO: return an html element return Promise.resolve(view).then((view) => { this.el.appendChild(view.el); - view.on('remove', () => console.log('view removed', view)); + view.on("remove", () => console.log("view removed", view)); return view.el; }); } @@ -68,15 +68,15 @@ export class DummyManager extends baseManager.ManagerBase { protected loadClass( className: string, moduleName: string, - moduleVersion: string + moduleVersion: string, ): Promise { - if (moduleName === '@jupyter-widgets/base') { + if (moduleName === "@jupyter-widgets/base") { if ((widgets as any)[className]) { return Promise.resolve((widgets as any)[className]); } else { return Promise.reject(`Cannot find class ${className}`); } - } else if (moduleName === 'jupyter-datawidgets') { + } else if (moduleName === "jupyter-datawidgets") { if (this.testClasses[className]) { return Promise.resolve(this.testClasses[className]); } else { @@ -106,7 +106,7 @@ export interface Constructor { export function createTestModel( constructor: Constructor, - attributes?: any + attributes?: any, ): T { const id = widgets.uuid(); const widget_manager = new DummyManager(); diff --git a/src/components.tsx b/src/components.tsx index 0a2ee89..49bf610 100644 --- a/src/components.tsx +++ b/src/components.tsx @@ -1,31 +1,32 @@ import React from "react"; -export -class ErrorBoundary extends React.Component { - constructor(props : any) { - super(props); - this.state = { hasError: false, error: null }; - } - - static getDerivedStateFromError(error: any) { - return { hasError: true, error }; - } - - componentDidCatch(error: any, errorInfo: any) { - } - - render() { +export class ErrorBoundary extends React.Component { + constructor(props: any) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: any) { + return { hasError: true, error }; + } + + componentDidCatch(error: any, errorInfo: any) {} + + render() { + // @ts-ignore + if (this.state.hasError) { // @ts-ignore - if (this.state.hasError) { - // @ts-ignore - const error = this.state.error.message; - return
-

Error

; -
{error}
- + const error = this.state.error.message; + return ( +
+

Error

;
{error}
+
- } - // @ts-ignore - return this.props.children; + ); } - } \ No newline at end of file + // @ts-ignore + return this.props.children; + } +} diff --git a/src/extension.ts b/src/extension.ts index c63ac21..d7ea45a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,7 +10,7 @@ // dynamically. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion (window as any).__webpack_public_path__ = - document.querySelector('body')!.getAttribute('data-base-url') + - 'nbextensions/jupyter-react'; + document.querySelector("body")!.getAttribute("data-base-url") + + "nbextensions/jupyter-react"; -export * from './index'; +export * from "./index"; diff --git a/src/index.ts b/src/index.ts index ab763c1..55465d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ // Copyright (c) Maarten A. Breddels // Distributed under the terms of the Modified BSD License. -export * from './version'; -export * from './widget'; +export * from "./version"; +export * from "./widget"; diff --git a/src/plugin.ts b/src/plugin.ts index d83a8da..6c1e759 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,17 +1,17 @@ // Copyright (c) Maarten A. Breddels // Distributed under the terms of the Modified BSD License. -import { Application, IPlugin } from '@lumino/application'; +import { Application, IPlugin } from "@lumino/application"; -import { Widget } from '@lumino/widgets'; +import { Widget } from "@lumino/widgets"; -import { IJupyterWidgetRegistry } from '@jupyter-widgets/base'; +import { IJupyterWidgetRegistry } from "@jupyter-widgets/base"; -import * as widgetExports from './widget'; +import * as widgetExports from "./widget"; -import { MODULE_NAME, MODULE_VERSION } from './version'; +import { MODULE_NAME, MODULE_VERSION } from "./version"; -const EXTENSION_ID = '@widgetti/jupyter-react:plugin'; +const EXTENSION_ID = "@widgetti/jupyter-react:plugin"; /** * The example plugin. @@ -32,7 +32,7 @@ export default examplePlugin; */ function activateWidgetExtension( app: Application, - registry: IJupyterWidgetRegistry + registry: IJupyterWidgetRegistry, ): void { registry.registerWidget({ name: MODULE_NAME, diff --git a/src/utils.ts b/src/utils.ts index 1915419..e88c013 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { transform } from 'sucrase'; +import { transform } from "sucrase"; const muiStyleFix = ` // This is a specific 'fix' for the notebook only, since its fontsize is non-16 @@ -36,44 +36,48 @@ function styleWrapper(element) { return element; } } -` +`; export async function setUpMuiFixModule() { - const code = transform(muiStyleFix, {transforms: ["jsx", "typescript"], filePath: "muifix.tsx"}).code; - let url = URL.createObjectURL(new Blob([code], { type: 'text/javascript' })); + const code = transform(muiStyleFix, { + transforms: ["jsx", "typescript"], + filePath: "muifix.tsx", + }).code; + let url = URL.createObjectURL(new Blob([code], { type: "text/javascript" })); // @ts-ignore return await importShim(url); } export function expose(module: any) { - const id = "_ipyreact_" + (Math.random()).toString(36); - // @ts-ignore - window[id] = module; - const names = Object.keys(module).filter(n => n !== "default").join(", ") - return toModuleUrl(` + const id = "_ipyreact_" + Math.random().toString(36); + // @ts-ignore + window[id] = module; + const names = Object.keys(module) + .filter((n) => n !== "default") + .join(", "); + return toModuleUrl(` const { ${names} } = window["${id}"]; export default window["${id}"].default; delete window["${id}"]; - export { ${names} };`) + export { ${names} };`); } export function toModuleUrl(code: string) { - return URL.createObjectURL(new Blob([code], { type: "text/javascript" })); + return URL.createObjectURL(new Blob([code], { type: "text/javascript" })); } export async function loadScript(type: string, src: string) { - const script = document.createElement("script") - script.type = type - script.src = src - script.defer = true - document.head.appendChild(script) - return new Promise((resolve, reject) => { - script.onload = () => { - resolve() - } - script.onerror = () => { - reject() - } - }) - } - + const script = document.createElement("script"); + script.type = type; + script.src = src; + script.defer = true; + document.head.appendChild(script); + return new Promise((resolve, reject) => { + script.onload = () => { + resolve(); + }; + script.onerror = () => { + reject(); + }; + }); +} diff --git a/src/version.ts b/src/version.ts index 1e73b24..d1b1153 100644 --- a/src/version.ts +++ b/src/version.ts @@ -4,7 +4,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-var-requires -const data = require('../package.json'); +const data = require("../package.json"); /** * The _model_module_version/_view_module_version this package implements. diff --git a/src/widget.tsx b/src/widget.tsx index 3738c50..78c5805 100644 --- a/src/widget.tsx +++ b/src/widget.tsx @@ -4,54 +4,66 @@ import { DOMWidgetModel, DOMWidgetView, - ISerializers -} from '@jupyter-widgets/base'; + ISerializers, +} from "@jupyter-widgets/base"; + +import * as React from "react"; +import { useEffect, useState } from "react"; +import * as ReactJsxRuntime from "react/jsx-runtime"; +import * as ReactReconcilerContants from "react-reconciler/constants"; +import * as ReactReconciler from "react-reconciler"; +import * as ReactDOM from "react-dom"; -import * as React from 'react'; -import { useEffect, useState } from 'react'; -import * as ReactJsxRuntime from 'react/jsx-runtime'; -import * as ReactReconcilerContants from "react-reconciler/constants"; -import * as ReactReconciler from "react-reconciler"; -import * as ReactDOM from 'react-dom'; // @ts-ignore -import * as ReactDOMClient from 'react-dom/client'; +import * as ReactDOMClient from "react-dom/client"; // @ts-ignore -import '../css/widget.css'; -import { expose, loadScript, setUpMuiFixModule } from './utils'; -import { MODULE_NAME, MODULE_VERSION } from './version'; +import "../css/widget.css"; +import { expose, loadScript, setUpMuiFixModule } from "./utils"; +import { MODULE_NAME, MODULE_VERSION } from "./version"; // import * as Babel from '@babel/standalone'; // TODO: find a way to ship es-module-shims with the widget // @ts-ignore // import 'es-module-shims'; -import { transform } from 'sucrase'; -import { ErrorBoundary } from './components'; +import { transform } from "sucrase"; +import { ErrorBoundary } from "./components"; import { Root } from "react-dom/client"; - // @ts-ignore // const react16Code = require('!!raw-loader!./react16.js'); // import react16Code from 'raw-loader!./react16.mjs'; // console.log(react16Code) - // this will do for now -let importShimLoaded : any = null; +let importShimLoaded: any = null; async function ensureImportShimLoaded() { - if(importShimLoaded == null) { - importShimLoaded = loadScript("module", "https://ga.jspm.io/npm:es-module-shims@1.7.0/dist/es-module-shims.js") + if (importShimLoaded == null) { + importShimLoaded = loadScript( + "module", + "https://ga.jspm.io/npm:es-module-shims@1.7.0/dist/es-module-shims.js", + ); } return await importShimLoaded; } -function autoExternalReactResolve(id: string, parentUrl: string, resolve: (arg0: any, arg1: any) => any) { - const shipsWith = (id == "react") || (id == "react-dom") || (id == "react/jsx-runtime") || (id == "react-dom/client") || (id == "react-reconciler") || (id == "react-reconciler/constants"); +function autoExternalReactResolve( + id: string, + parentUrl: string, + resolve: (arg0: any, arg1: any) => any, +) { + const shipsWith = + id == "react" || + id == "react-dom" || + id == "react/jsx-runtime" || + id == "react-dom/client" || + id == "react-reconciler" || + id == "react-reconciler/constants"; const alreadyPatched = parentUrl.includes("?external=react,react-dom"); const parentIsEsmSh = parentUrl.startsWith("https://esm.sh/"); const isBlob = id.startsWith("blob:"); - if(!shipsWith && !id.includes("://") && !parentIsEsmSh) { + if (!shipsWith && !id.includes("://") && !parentIsEsmSh) { id = "https://esm.sh/" + id; } - if(!shipsWith && !alreadyPatched && !isBlob) { + if (!shipsWith && !alreadyPatched && !isBlob) { id = id + "?external=react,react-dom"; } // console.log("resolve", id, parentUrl, resolve) @@ -59,18 +71,23 @@ function autoExternalReactResolve(id: string, parentUrl: string, resolve: (arg0: } // @ts-ignore -window.esmsInitOptions = { shimMode: true, - resolve: (id: string, parentUrl: string, resolve: (id: string, parentUrl: string) => any) => autoExternalReactResolve(id, parentUrl, resolve) -} +window.esmsInitOptions = { + shimMode: true, + resolve: ( + id: string, + parentUrl: string, + resolve: (id: string, parentUrl: string) => any, + ) => autoExternalReactResolve(id, parentUrl, resolve), +}; -let react18ESMUrls : any = null; -let react16ESMUrls : any = null; +let react18ESMUrls: any = null; +let react16ESMUrls: any = null; function ensureReactSetup(version: number) { - if(version == 18) { - if(react18ESMUrls == null) { + if (version == 18) { + if (react18ESMUrls == null) { react18ESMUrls = { - "react": expose(React), + react: expose(React), "react-dom": expose(ReactDOM), "react/jsx-runtime": expose(ReactJsxRuntime), "react-dom/client": expose(ReactDOMClient), @@ -79,16 +96,14 @@ function ensureReactSetup(version: number) { }; } return react18ESMUrls; - } else if(version == 16) { - if(react16ESMUrls == null) { + } else if (version == 16) { + if (react16ESMUrls == null) { // react16ESMUrls = {urlReact: expose(React16), urlReactDom: expose(ReactDOM16)}; } return react16ESMUrls; } } - - export class ReactModel extends DOMWidgetModel { defaults() { return { @@ -109,21 +124,20 @@ export class ReactModel extends DOMWidgetModel { ...DOMWidgetModel.serializers, }; - static model_name = 'ReactModel'; + static model_name = "ReactModel"; static model_module = MODULE_NAME; static model_module_version = MODULE_VERSION; - static view_name = 'ReactView'; // Set to null if no view + static view_name = "ReactView"; // Set to null if no view static view_module = MODULE_NAME; // Set to null if no view static view_module_version = MODULE_VERSION; } - export class ReactView extends DOMWidgetView { private root: Root | null = null; render() { - this.el.classList.add('jupyter-react-widget'); - // using babel is a bit of an art, so leaving this code for if we + this.el.classList.add("jupyter-react-widget"); + // using babel is a bit of an art, so leaving this code for if we // want to switch back to babel. However, babel is very large compared // to sucrase // Babel.registerPreset("my-preset", { @@ -140,54 +154,55 @@ export class ReactView extends DOMWidgetView { // 18: React18, // }[this.model.get("react_version")]; - - const Component = () => { // @ts-ignore // @ts-ignore const [_, setCounter] = useState(0); const forceRerender = () => { setCounter((x) => x + 1); - } + }; useEffect(() => { - this.listenTo(this.model, 'change', forceRerender); + this.listenTo(this.model, "change", forceRerender); }, []); - const compiledCode : string | Error = React.useMemo(() => { - const code = this.model.get('_esm'); - if(this.model.get("debug")) { + const compiledCode: string | Error = React.useMemo(() => { + const code = this.model.get("_esm"); + if (this.model.get("debug")) { console.log("original code:\n", code); } try { // using babel: // return Babel.transform(code, { presets: ["react", "es2017"], plugins: ["importmap"] }).code; // using sucrase: - let compiledCode = transform(code, {transforms: ["jsx", "typescript"], filePath: "test.tsx"}).code; - if(this.model.get("debug")) { + let compiledCode = transform(code, { + transforms: ["jsx", "typescript"], + filePath: "test.tsx", + }).code; + if (this.model.get("debug")) { console.log("compiledCode:\n", compiledCode); } return compiledCode; } catch (e) { return e; } - }, [this.model.get('_esm')]) - const props : any = {} + }, [this.model.get("_esm")]); + const props: any = {}; for (const event_name of this.model.attributes["_event_names"]) { - const handler = (value : any, buffers : any) => { + const handler = (value: any, buffers: any) => { if (buffers) { - const validBuffers = buffers instanceof Array && - buffers[0] instanceof ArrayBuffer; - if (!validBuffers) { - console.warn('second argument is not an BufferArray[View] array') - buffers = undefined; - } + const validBuffers = + buffers instanceof Array && buffers[0] instanceof ArrayBuffer; + if (!validBuffers) { + console.warn("second argument is not an BufferArray[View] array"); + buffers = undefined; + } } this.model.send( - {event_name, data: value}, + { event_name, data: value }, this.model.callbacks(this), buffers, ); - } + }; props["on_" + event_name] = handler; } for (const key of Object.keys(this.model.attributes)) { @@ -206,92 +221,96 @@ export class ReactView extends DOMWidgetView { const [muiFix, setMuiFix] = React.useState(null as any | Error); React.useEffect(() => { - let url : string | null = null; + let url: string | null = null; (async () => { if (compiledCode instanceof Error) { setScope(compiledCode); return; } - const reactImportMap = ensureReactSetup(this.model.get("react_version")); + const reactImportMap = ensureReactSetup( + this.model.get("react_version"), + ); await ensureImportShimLoaded(); let finalCode = compiledCode; // @ts-ignore const importMapWidget = this.model.get("_import_map"); const importMap = { - "imports": { + imports: { ...reactImportMap, ...importMapWidget["imports"], }, - "scopes": { - ...importMapWidget["scopes"] - } + scopes: { + ...importMapWidget["scopes"], + }, }; // @ts-ignore importShim.addImportMap(importMap); const needsMuiFix = compiledCode.indexOf("@mui") !== -1; - if(needsMuiFix) { + if (needsMuiFix) { setMuiFix(await setUpMuiFixModule()); } url = URL.createObjectURL( new Blob([finalCode], { type: "text/javascript" }), ); - try{ + try { // @ts-ignore let module = await importShim(url); let name = this.model.get("name"); - if(name && name.length > 0) { + if (name && name.length > 0) { // @ts-ignore - importShim.addImportMap({"imports": {[name]: url}}); + importShim.addImportMap({ imports: { [name]: url } }); } setScope(module); - } catch (e) { + } catch (e) { setScope(e); } })(); return () => { - if(url) { + if (url) { URL.revokeObjectURL(url); } - } - }, [compiledCode]); + }; + }, [compiledCode]); if (!scope) { return
Loading...
; } else { - if(scope instanceof Error) { + if (scope instanceof Error) { return
{scope.message}
; } else { - if(scope.default === undefined) { + if (scope.default === undefined) { return
Missing default component
; } else { - if(this.model.get("debug")) { + if (this.model.get("debug")) { console.log("props", props); } // @ts-ignore - let el = React.createElement(scope.default, props) + let el = React.createElement(scope.default, props); // check if @mui string is in compiledCode // if so, we need to wrap the element in a style wrapper // @ts-ignore const needsMuiFix = compiledCode.indexOf("@mui") !== -1; - if(this.model.get("debug")) { + if (this.model.get("debug")) { console.log("needsMuiFix", needsMuiFix); } - if(needsMuiFix) { + if (needsMuiFix) { el = muiFix.styleWrapper(el); } return el; - } } } - } - if(this.model.get("react_version") === 18) { + }; + if (this.model.get("react_version") === 18) { this.root = ReactDOMClient.createRoot(this.el); - this.root.render(); - } else { - // @ts-ignore - // ReactDOM16.render(, this.el); - + this.root.render( + + + , + ); + } else { + // @ts-ignore + // ReactDOM16.render(, this.el); } } diff --git a/tests/ui/event_test.py b/tests/ui/event_test.py index 10145b3..25d1b6d 100644 --- a/tests/ui/event_test.py +++ b/tests/ui/event_test.py @@ -1,7 +1,8 @@ import playwright.sync_api +import traitlets from IPython.display import display + import ipyreact -import traitlets class ButtonWithHandler(ipyreact.ReactWidget): diff --git a/tests/ui/jupyter_test.py b/tests/ui/jupyter_test.py index 82ba527..2cd88d4 100644 --- a/tests/ui/jupyter_test.py +++ b/tests/ui/jupyter_test.py @@ -2,12 +2,12 @@ from IPython.display import display - -def test_widget_ipyreact(ipywidgets_runner, page_session: playwright.sync_api.Page, assert_solara_snapshot): +def test_widget_ipyreact( + ipywidgets_runner, page_session: playwright.sync_api.Page, assert_solara_snapshot +): def kernel_code(): import ipyreact - class Counter(ipyreact.ReactWidget): _esm = """ import * as React from "react"; @@ -17,8 +17,10 @@ class Counter(ipyreact.ReactWidget): {value || 0} clicks };""" + c = Counter() display(c) + ipywidgets_runner(kernel_code) counter = page_session.locator(".counter-widget") counter.click() diff --git a/tests/ui/library_test.py b/tests/ui/library_test.py index 676bf42..c4319ff 100644 --- a/tests/ui/library_test.py +++ b/tests/ui/library_test.py @@ -1,11 +1,11 @@ -import ipyreact import playwright.sync_api from IPython.display import display -code = \ -""" +import ipyreact + +code = """ import Button from '@mui/material/Button'; -import * as React from "react"; +import * as React from "react"; export default function({value, set_value, debug}) { return };""" - c = Counter() \ No newline at end of file + + Counter() diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json deleted file mode 100644 index 737e3e6..0000000 --- a/tsconfig.eslint.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": [] -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d7a949b..edbe96c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "declaration": true, - "esModuleInterop":true, + "esModuleInterop": true, "lib": ["es2015", "dom"], "module": "commonjs", "moduleResolution": "node", @@ -18,9 +18,6 @@ "types": ["jest"], "jsx": "react" }, - "include": [ - "src/**/*.ts", - "src/**/*.tsx", - ], + "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["src/**/__tests__"] } diff --git a/webpack.config.js b/webpack.config.js index 856c37e..dcee185 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,42 +1,43 @@ -const path = require('path'); -const version = require('./package.json').version; +const path = require("path"); +const version = require("./package.json").version; // Custom webpack rules const rules = [ - { test: /\.mjs$/i, - use: [ - { - loader: 'raw-loader', - options: { - esModule: false, + { + test: /\.mjs$/i, + use: [ + { + loader: "raw-loader", + options: { + esModule: false, + }, }, - }, - ], }, - { test: /\.ts$/, loader: 'ts-loader' }, - { test: /\.tsx$/, loader: 'ts-loader' }, - { test: /\.js$/, loader: 'source-map-loader' }, - { test: /\.css$/, use: ['style-loader', 'css-loader']} + ], + }, + { test: /\.ts$/, loader: "ts-loader" }, + { test: /\.tsx$/, loader: "ts-loader" }, + { test: /\.js$/, loader: "source-map-loader" }, + { test: /\.css$/, use: ["style-loader", "css-loader"] }, ]; // Packages that shouldn't be bundled but loaded at runtime -const externals = ['@jupyter-widgets/base']; +const externals = ["@jupyter-widgets/base"]; const resolve = { // Add '.ts' and '.tsx' as resolvable extensions. - extensions: [".webpack.js", ".web.js", ".ts", ".js", ".tsx", ".jsx"] + extensions: [".webpack.js", ".web.js", ".ts", ".js", ".tsx", ".jsx"], }; const resolve16 = { // Add '.ts' and '.tsx' as resolvable extensions. extensions: [".webpack.js", ".web.js", ".ts", ".js", ".tsx", ".jsx"], alias: { - "react": "react16", + react: "react16", "react-dom": "react-dom16", - "react-dom/client": path.resolve(__dirname, 'src/client_react16.js') - } + "react-dom/client": path.resolve(__dirname, "src/client_react16.js"), + }, }; - module.exports = [ /** * Notebook extension @@ -45,17 +46,17 @@ module.exports = [ * the notebook. */ { - entry: './src/extension.ts', + entry: "./src/extension.ts", output: { - filename: 'index.js', - path: path.resolve(__dirname, 'ipyreact', 'nbextension'), - libraryTarget: 'amd', - publicPath: '', + filename: "index.js", + path: path.resolve(__dirname, "ipyreact", "nbextension"), + libraryTarget: "amd", + publicPath: "", }, module: { - rules: rules + rules: rules, }, - devtool: 'source-map', + devtool: "source-map", externals, resolve, }, @@ -88,42 +89,41 @@ module.exports = [ * the custom widget embedder. */ { - entry: './src/index.ts', + entry: "./src/index.ts", output: { - filename: 'index.js', - path: path.resolve(__dirname, 'dist'), - libraryTarget: 'amd', - library: "@widgetti/jupyter-react", - publicPath: 'https://unpkg.com/@widgetti/jupyter-react@' + version + '/dist/' + filename: "index.js", + path: path.resolve(__dirname, "dist"), + libraryTarget: "amd", + library: "@widgetti/jupyter-react", + publicPath: + "https://unpkg.com/@widgetti/jupyter-react@" + version + "/dist/", }, - devtool: 'source-map', + devtool: "source-map", module: { - rules: rules + rules: rules, }, externals, resolve, }, - /** * Documentation widget bundle * * This bundle is used to embed widgets in the package documentation. */ { - entry: './src/index.ts', + entry: "./src/index.ts", output: { - filename: 'embed-bundle.js', - path: path.resolve(__dirname, 'docs', 'source', '_static'), + filename: "embed-bundle.js", + path: path.resolve(__dirname, "docs", "source", "_static"), library: "@widgetti/jupyter-react", - libraryTarget: 'amd' + libraryTarget: "amd", }, module: { - rules: rules + rules: rules, }, - devtool: 'source-map', + devtool: "source-map", externals, resolve, - } - + }, ];