JupyterLab extension ES module generation problems

I am trying to get my hybrid extension to build es modules for the front end portion and I cannot seem to find the “magic incantation” of configuration to do so. The extension code generates imports and has webpack wrappers to handle commonjs requires statements, but Jupyter raises the error that it cannot use import outside of a module. So, it is not recognizing it as an es module file. The output from tsc looks fine. It is the packaging by webpack that seems to be center of the problem.

My deployed package.json file look like:

{
    "name": "@mygroup/my-extension",
    "description": "My extension",
    "version": "0.1.0",
    "keywords": [
        "jupyter",
        "jupyterlab",
        "jupyterlab-extension",
    ],
    "dependencies": {
        "@babel/runtime": "^7.27.1",
        "@jupyterlab/application": "^4.0.0",
        "@jupyterlab/coreutils": "^6.0.0",
        "@jupyterlab/filebrowser": "4.3.1",
        "@jupyterlab/services": "^7.0.0",
        "@jupyterlab/ui-components": "^4.3.1",
        "@lumino/coreutils": "^2.2.1",
        "@lumino/disposable": "^2.1.4",
        "@lumino/signaling": "^2.1.4",
        "react": "^18.2.0"
    },
    "type": "module",
    "jupyterlab": {
        "discovery": {
            "server": {
                "managers": [
                    "pip"
                ],
                "base": {
                    "name": "my_extension"
                }
            }
        },
        "extension": true,
        "outputDir": "my_extension/labextension/static",
        "_build": {
            "load": "static/remoteEntry.mjs",
            "extension": true
        }
    },
    "federated_extensions": [{
            "name": "@mygroup/my-extension",
            "load": "static/remoteEntry.mjs",
            "extension": true
        }
    ]
}

My webpack configuration is:

const path = require('path')
const { ModuleFederationPlugin } = require('webpack').container
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = (env, argv) => {
  const production = argv.mode === 'production'
  return {
    mode: argv.mode || 'development',
    entry: path.resolve(__dirname, 'src', 'index.ts'),
    experiments: {
      outputModule: true
    },
    output: {
      filename: 'extension.[contenthash].mjs',
      chunkFormat: 'module',
      module: true,
      path: path.resolve(
        __dirname,
        'my_extension',
        'labextension',
        '@mygroup',
        'my-extension',
        'static',
      ),
      library: {
        type: 'module'
      },
      publicPath: path.posix.join('labextensions', '@mygroup', 'my-extension', 'static'),
      clean: true,
    },
    target: 'es2020',
    module: {
      rules: [
        {
          test: /\.ts$/,
          use: {
            loader: 'ts-loader',
            options: {
              configFile: path.resolve(__dirname, 'tsconfig.json'),
              transpileOnly: false,
            }
          },
          exclude: /node_modules/,
        },
        {
          test: /\.css$/,
          use: [
            production ? MiniCssExtractPlugin.loader : 'style-loader',
            {
              loader: 'css-loader',
              options: {
                esModule: true,
              }
            }],
          include: path.resolve(__dirname, 'style'),
        },
        {
          test: /\.(png|jpg|jpeg|gif|svg)$/,
          type: 'asset/inline',
          include: /node_modules/,
        }
      ]
    },
    resolve: {
      extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
      fallback: {
        crypto: require.resolve('crypto-browserify'),
        path: require.resolve('path-browserify'),
      }
    },
    optimization: {
      runtimeChunk: false,
    },
    devtool: production ? 'source-map' : 'inline-source-map',
    externals: {
      '@jupyterlab/application': 'module @jupyterlab/application',
      '@jupyterlab/apputils': 'module @jupyterlab/apputils',
      '@jupyterlab/coreutils': 'module @jupyterlab/coreutils',
      '@jupyterlab/filebrowser': 'module @jupyterlab/filebrowser',
      '@jupyterlab/services': 'module @jupyterlab/services',
      '@jupyterlab/ui-components': 'module @jupyterlab/ui-components',
      '@lumino/coreutils': 'module @lumino/coreutils',
      '@lumino/disposable': 'module @lumino/disposable',
      '@lumino/signaling': 'module @lumino/signaling',
    },
    plugins: [
      new CleanWebpackPlugin(),
      new ModuleFederationPlugin({
        name: 'myExtension',
        library: {
          type: 'module',
        },
        filename: 'remoteEntry.mjs',
        exposes: {
          './extension': path.resolve(__dirname, 'src', 'index.ts'),
        }
      }),
      production && new MiniCssExtractPlugin({
        filename: 'style.[contenthash].css',
      }),
      new CopyWebpackPlugin({
        patterns: [
          {
            from: path.resolve(__dirname, 'package.json'),
            to: path.resolve(
              __dirname,
              'my_extension',
              'labextension',
              '@mygroup',
              'my-extension'
            ),
            transform: (content) => {
              let pkg = JSON.parse(content.toString())
              pkg.jupyterlab._build = {
                load: path.posix.join('static', 'remoteEntry.mjs'),
                extension: true
              }
              pkg = {
                name: pkg.name,
                description: pkg.description,
                author: pkg.author,
                version: pkg.version,
                keywords: pkg.keywords,
                dependencies: pkg.dependencies,
                type: 'module',
                jupyterlab: pkg.jupyterlab,
                federated_extensions: [
                  {
                    name: pkg.name,
                    load: path.posix.join('static', 'remoteEntry.mjs'),
                    extension: true
                  }
                ],
              }
              return JSON.stringify(pkg, null, 2)
            }
          }
        ]
      })
    ]
  }
}

My tsconfig.json file is:

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "composite": true,
    "declaration": true,
    "emitDeclarationOnly": false,
    "esModuleInterop": true,
    "incremental": true,
    "forceConsistentCasingInFileNames": true,
    "jsx": "react",
    "lib": ["DOM", "es2020", "ES2020.Intl"],
    "module": "es2020",
    "moduleResolution": "node",
    "noEmitOnError": true,
    "noUnusedLocals": true,
    "preserveWatchOutput": true,
    "resolveJsonModule": true,
    "outDir": "lib",
    "rootDir": "src",
    "skipLibCheck": true,
    "strict": true,
    "target": "es2020"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

And my pyproject.toml is:

[build-system]
requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version>=0.3.2"]
build-backend = "hatchling.build"

[project]
name = "my_extension"
readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.9"
classifiers = [
    "Framework :: Jupyter",
    "Framework :: Jupyter :: JupyterLab",
    "Framework :: Jupyter :: JupyterLab :: 4",
    "Framework :: Jupyter :: JupyterLab :: Extensions",
    "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt",
    "License :: OSI Approved :: BSD License",
    "Programming Language :: Python",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
]
dependencies = [
    "jupyter_server>=2.4.0,<3"
]
dynamic = ["version", "description", "authors", "urls", "keywords"]

[tool.hatch.version]
source = "nodejs"
build-env = "default"

[tool.hatch.metadata.hooks.nodejs]
fields = ["description", "authors", "urls", "keywords"]

[tool.hatch.build.targets.sdist]
artifacts = ["my_extension/labextension"]
exclude = [".github", "binder"]

[tool.hatch.build.targets.wheel.shared-data]
"my_extension/labextension" = "share/jupyter/labextensions"
"jupyter-config/server-config" = "etc/jupyter/jupyter_server_config.d"

[tool.hatch.build.hooks.version]
path = "my_extension/_version.py"

[tool.hatch.build.hooks.jupyter-builder]
dependencies = ["hatch-jupyter-builder>=0.5"]
build-function = "hatch_jupyter_builder.npm_builder"
build-targets = ["my_extension/labextension/static/**"]
ensured-targets = [
    "my_extension/labextension/static/remoteEntry.js",
]
name = "my-extension"

[tool.hatch.build.hooks.jupyter-builder.build-kwargs]
npm = ["jlpm"]
build_cmd = "build:prod"
source-dir = "src"

[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs]
npm = ["jlpm"]
build_cmd = "build:dev"
source-dir = "src"

[tool.jupyter-releaser.options]
version_cmd = "hatch version"

[tool.jupyter-releaser.hooks]
before-build-npm = [
    "python -m pip install 'jupyterlab>=4.0.0,<5'",
    "jlpm",
    "jlpm build:prod"
]
before-build-python = ["jlpm clean:all"]

[tool.check-wheel-contents]
ignore = ["W002"]

The error being recorded in the browser console is:

remoteEntry.mjs:1 Uncaught SyntaxError: Cannot use import statement outside a module (at remoteEntry.mjs:1:1)

hook.js:608 TypeError: Cannot read properties of undefined (reading ‘init’)
at loadComponent (bootstrap.js:64:1)
at async bootstrap.js:80:1
at async Promise.allSettled (index 2)
at async bootstrap (bootstrap.js:78:1)
overrideMethod @ hook.js:608
(anonymous) @ bootstrap.js:90
bootstrap @ bootstrap.js:87
await in bootstrap
./build/bootstrap.js @ bootstrap.js:98
webpack_require @ bootstrap:19
(anonymous) @ startup:5
(anonymous) @ startup:5

I have tried checked/tried various modifications to the configuration of the build and have not come up with one that works.

I have tracked down the problem to the bootstrap.js script creation. It does not seem to make any provision for construction of the type=”module” attribute for es module extension packages:

function loadScript(url) {
return new Promise((resolve, reject) => {
const newScript = document.createElement(‘script’);
newScript.onerror = reject;
newScript.onload = resolve;
newScript.async = true;
document.head.appendChild(newScript);
newScript.src = url;
});
}

Is there a work around for this? This code is being generated by the build of jupyter lab and looks like it is being generated by webpack. I rebuilt the jupyter lab using jupyter lab build –minimize=False –dev-build=True for debugging purposes. Is there another switch I should use to make it recognize ES modules on the front end?

2 Likes

I also inspected the script elements in the page and there is a json script element with the id jupyter-config-data that contains, in part:

“federated_extensions”: [{
“entrypoints”: null,
“extension”: “./extension”,
“load”: “static/remoteEntry.5cbb9d2323598fbda535.js”,
“name”: “jupyterlab_pygments”,
“style”: “./style”
}, {
“entrypoints”: null,
“extension”: “./extension”,
“load”: “static/remoteEntry.f2d2e79c8a92fd2ec840.js”,
“name”: “@jupyter-notebookjupyter-notebookjupyter-notebookjupyter-notebook/lab-extension”,
“style”: “./style”
}, {
“entrypoints”: null,
“extension”: true,
“load”: “static/remyoteEntry.mjs”,
“name”: “@mygroup/my-extension”
}
],

This supplies the front end with the necessary information to build the script elements. With the exception of the file extension, it provides no queues regarding the module type (commonjs or es). I could find no checks of the file extension in the process of creating the script extensions.