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.