├── .github ├── release.yml └── workflows │ ├── check-tests.yml │ ├── main.yml │ └── update-workflows.yml ├── .gitignore ├── LICENSE ├── README.md ├── copier.yml └── template ├── .github └── workflows │ ├── build.yml.jinja │ ├── check-release.yml.jinja │ ├── enforce-label.yml │ ├── prep-release.yml │ ├── publish-release.yml │ ├── {% if has_binder %}binder-on-pr.yml{% endif %} │ └── {% if test %}update-integration-tests.yml{% endif %} ├── .gitignore.jinja ├── .prettierignore.jinja ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE.jinja ├── README.md.jinja ├── RELEASE.md.jinja ├── install.json.jinja ├── package.json.jinja ├── pyproject.toml.jinja ├── setup.py ├── src ├── index.ts.jinja ├── {% if kind == 'server' %}handler.ts{% endif %}.jinja └── {% if test %}__tests__{% endif %} │ └── {{python_name}}.spec.ts.jinja ├── style ├── index.css.jinja ├── {% if kind != 'theme' %}base.css{% endif %} ├── {% if kind != 'theme' %}index.js{% endif %} └── {% if kind == 'theme' %}variables.css{% endif %} ├── tsconfig.json.jinja ├── {% if has_binder %}binder{% endif %} ├── environment.yml.jinja └── postBuild.jinja ├── {% if has_settings %}schema{% endif %} └── plugin.json.jinja ├── {% if kind == 'server' %}jupyter-config{% endif %} └── server-config │ └── {{python_name}}.json.jinja ├── {% if test %}babel.config.js{% endif %} ├── {% if test %}jest.config.js{% endif %} ├── {% if test %}tsconfig.test.json{% endif %} ├── {% if test %}ui-tests{% endif %} ├── README.md ├── jupyter_server_test_config.py ├── package.json.jinja ├── playwright.config.js ├── tests │ └── {{python_name}}.spec.ts.jinja └── yarn.lock ├── {% if test and kind == 'server' %}conftest.py{% endif %}.jinja ├── {{_copier_conf.answers_file}}.jinja └── {{python_name}} ├── __init__.py.jinja ├── {% if kind == 'server' %}handlers.py{% endif %}.jinja └── {% if test and kind == 'server' %}tests{% endif %} ├── __init__.py.jinja └── test_handlers.py.jinja /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: New Features ✨ 4 | labels: 5 | - enhancement 6 | - title: Bug Fixes 🐛 7 | labels: 8 | - bug 9 | - title: Documentation 📖 10 | labels: 11 | - documentation 12 | - title: Maintenance 🚧 13 | labels: 14 | - maintenance 15 | - title: Other Changes ➕ 16 | labels: 17 | - "*" 18 | -------------------------------------------------------------------------------- /.github/workflows/check-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests validation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: ["*"] 9 | schedule: 10 | - cron: "0 0 * * *" 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | tests: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Base Setup 24 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 25 | 26 | - name: Install dependencies 27 | run: python -m pip install -U "copier>=9.2.0" jinja2-time "jupyterlab>=4.0.0,<5" 28 | 29 | - name: Create the extension 30 | run: | 31 | set -eux 32 | mkdir myextension 33 | python -m copier copy -l -d author_name="My Name" -d repository="https://github.com/test/lab-extension" --vcs-ref HEAD --UNSAFE . myextension 34 | cat myextension/pyproject.toml 35 | 36 | - name: Test the extension 37 | working-directory: myextension 38 | run: | 39 | set -eux 40 | YARN_ENABLE_IMMUTABLE_INSTALLS=false jlpm 41 | jlpm test 42 | 43 | - name: Install the extension 44 | working-directory: myextension 45 | run: | 46 | set -eux 47 | python -m pip install -v . 48 | cat myextension/labextension/package.json 49 | 50 | - name: List extensions 51 | run: | 52 | jupyter labextension list 53 | 54 | - name: Install UI tests dependencies 55 | working-directory: myextension/ui-tests 56 | env: 57 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 58 | YARN_ENABLE_IMMUTABLE_INSTALLS: false 59 | run: jlpm install 60 | 61 | - name: Set up browser cache 62 | uses: actions/cache@v4 63 | with: 64 | path: | 65 | ${{ github.workspace }}/pw-browsers 66 | key: ${{ runner.os }}-${{ hashFiles('myextension/ui-tests/yarn.lock') }} 67 | 68 | - name: Install browser 69 | run: jlpm playwright install chromium 70 | working-directory: myextension/ui-tests 71 | 72 | - name: Execute integration tests 73 | working-directory: myextension/ui-tests 74 | run: | 75 | jlpm playwright test 76 | 77 | - name: Upload Playwright Test report 78 | if: always() 79 | uses: actions/upload-artifact@v4 80 | with: 81 | name: extension-playwright-tests 82 | path: | 83 | myextension/ui-tests/test-results 84 | myextension/ui-tests/playwright-report 85 | 86 | test-mimerenderer: 87 | runs-on: ubuntu-latest 88 | steps: 89 | - name: Checkout 90 | uses: actions/checkout@v4 91 | 92 | - name: Base Setup 93 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 94 | 95 | - name: Install dependencies 96 | run: python -m pip install -U "copier>=9.2.0" jinja2-time "jupyterlab>=4.0.0,<5" 97 | 98 | - name: Create the extension 99 | run: | 100 | set -eux 101 | mkdir myextension 102 | python -m copier copy -l -d kind=mimerenderer -d viewer_name="My Viewer" -d mimetype="application/vnd.my_org.my_type" -d mimetype_name="my_type" -d file_extension=".my_type" -d author_name="My Name" -d repository="https://github.com/test/lab-extension" --vcs-ref HEAD --UNSAFE . myextension 103 | 104 | - name: Install the extension 105 | working-directory: myextension 106 | run: | 107 | set -eux 108 | YARN_ENABLE_IMMUTABLE_INSTALLS=false python -m pip install -v . 109 | 110 | - name: List extensions 111 | run: | 112 | jupyter labextension list 113 | 114 | - name: Install UI tests dependencies 115 | working-directory: myextension/ui-tests 116 | env: 117 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 118 | YARN_ENABLE_IMMUTABLE_INSTALLS: false 119 | run: jlpm install 120 | 121 | - name: Set up browser cache 122 | uses: actions/cache@v4 123 | with: 124 | path: | 125 | ${{ github.workspace }}/pw-browsers 126 | key: ${{ runner.os }}-${{ hashFiles('myextension/ui-tests/yarn.lock') }} 127 | 128 | - name: Install browser 129 | run: jlpm playwright install chromium 130 | working-directory: myextension/ui-tests 131 | 132 | - name: Execute integration tests 133 | working-directory: myextension/ui-tests 134 | run: | 135 | # Generate reference snapshot first 136 | jlpm playwright test -u 137 | jlpm playwright test 138 | 139 | - name: Upload Playwright Test report 140 | if: always() 141 | uses: actions/upload-artifact@v4 142 | with: 143 | name: mimerenderer-playwright-tests 144 | path: | 145 | myextension/ui-tests/test-results 146 | myextension/ui-tests/playwright-report -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: ["*"] 9 | schedule: 10 | - cron: "0 0 * * *" 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | names: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | include: 22 | - name: "my_extension" 23 | pyname: "my_extension" 24 | - name: "myextension" 25 | pyname: "myextension" 26 | - name: "my-extension" 27 | pyname: "my_extension" 28 | 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | 33 | - name: Base Setup 34 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 35 | 36 | - name: Setup Git 37 | run: | 38 | git config --global user.name "github-actions[bot]" 39 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 40 | 41 | - name: Install dependencies 42 | run: | 43 | python -m pip install "copier>=9.2.0" jinja2-time 44 | 45 | - name: Create pure frontend extension 46 | env: 47 | NAME: ${{ matrix.name }} 48 | PYNAME: ${{ matrix.pyname }} 49 | run: | 50 | set -eux 51 | mkdir ${NAME} 52 | python -m copier copy -l -d author_name="My Name" -d labextension_name="${NAME}" -d python_name="${PYNAME}" -d repository="https://github.com/test/lab-extension" --vcs-ref HEAD --UNSAFE . ${NAME} 53 | pushd ${NAME} 54 | python -m pip install "jupyterlab>=4.0.0,<5" setuptools 55 | YARN_ENABLE_IMMUTABLE_INSTALLS=false jlpm 56 | jlpm lint:check 57 | python -m pip install -e . 58 | jupyter labextension develop . --overwrite 59 | jupyter labextension list 60 | jupyter labextension list 2>&1 | grep -ie "${NAME}.*OK" 61 | python -m jupyterlab.browser_check 62 | 63 | jupyter labextension uninstall ${NAME} 64 | python -m pip uninstall -y ${NAME} jupyterlab 65 | 66 | popd 67 | rm -rf ${NAME} 68 | 69 | quoted-description: 70 | runs-on: ubuntu-latest 71 | strategy: 72 | matrix: 73 | # This will be used by the base setup action 74 | python-version: ["3.13"] 75 | 76 | steps: 77 | - name: Checkout 78 | uses: actions/checkout@v4 79 | 80 | - name: Base Setup 81 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 82 | 83 | - name: Install dependencies 84 | run: | 85 | python -m pip install "copier>=9.2.0" jinja2-time 86 | 87 | - name: Setup Git 88 | run: | 89 | git config --global user.name "github-actions[bot]" 90 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 91 | 92 | - name: Create pure frontend extension 93 | run: | 94 | set -eux 95 | mkdir myextension 96 | python -m copier copy -l -d author_name="My Name" -d description="Let's \"rock and roll\"" -d repository="https://github.com/test/lab-extension" --vcs-ref HEAD --UNSAFE . myextension 97 | pushd myextension 98 | pip install "jupyterlab>=4.0.0,<5" setuptools 99 | YARN_ENABLE_IMMUTABLE_INSTALLS=false jlpm 100 | jlpm lint:check 101 | pip install -e . 102 | jupyter labextension develop . --overwrite 103 | jupyter labextension list 104 | jupyter labextension list 2>&1 | grep -ie "myextension.*OK" 105 | python -m jupyterlab.browser_check 106 | 107 | jupyter labextension uninstall myextension 108 | pip uninstall -y myextension jupyterlab 109 | 110 | popd 111 | rm -rf myextension 112 | 113 | no-tests: 114 | runs-on: ubuntu-latest 115 | strategy: 116 | matrix: 117 | # This will be used by the base setup action 118 | python-version: ["3.13"] 119 | 120 | steps: 121 | - name: Checkout 122 | uses: actions/checkout@v4 123 | 124 | - name: Base Setup 125 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 126 | 127 | - name: Install dependencies 128 | run: | 129 | python -m pip install "copier>=9.2.0" jinja2-time 130 | 131 | - name: Setup Git 132 | run: | 133 | git config --global user.name "github-actions[bot]" 134 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 135 | 136 | - name: Create pure frontend extension 137 | run: | 138 | set -eux 139 | mkdir myextension 140 | python -m copier copy -l -d author_name="My Name" -d test=n -d repository="https://github.com/test/lab-extension" --vcs-ref HEAD --UNSAFE . myextension 141 | pushd myextension 142 | pip install "jupyterlab>=4.0.0,<5" setuptools 143 | YARN_ENABLE_IMMUTABLE_INSTALLS=false jlpm 144 | jlpm lint:check 145 | pip install -e . 146 | jupyter labextension develop . --overwrite 147 | jupyter labextension list 148 | jupyter labextension list 2>&1 | grep -ie "myextension.*OK" 149 | python -m jupyterlab.browser_check 150 | 151 | jupyter labextension uninstall myextension 152 | pip uninstall -y myextension jupyterlab 153 | 154 | popd 155 | rm -rf myextension 156 | 157 | settings: 158 | runs-on: ubuntu-latest 159 | strategy: 160 | matrix: 161 | # This will be used by the base setup action 162 | python-version: ["3.9", "3.13"] 163 | 164 | steps: 165 | - name: Checkout 166 | uses: actions/checkout@v4 167 | 168 | - name: Base Setup 169 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 170 | 171 | - name: Install dependencies 172 | run: | 173 | python -m pip install "copier>=9.2.0" jinja2-time 174 | 175 | - name: Setup Git 176 | run: | 177 | git config --global user.name "github-actions[bot]" 178 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 179 | 180 | - name: Create pure frontend extension 181 | run: | 182 | set -eux 183 | mkdir myextension 184 | python -m copier copy -l -d author_name="My Name" -d has_settings=y -d repository="https://github.com/test/lab-extension" --vcs-ref HEAD --UNSAFE . myextension 185 | pushd myextension 186 | pip install "jupyterlab>=4.0.0,<5" setuptools 187 | YARN_ENABLE_IMMUTABLE_INSTALLS=false jlpm 188 | # It is not easily possible to get this version compatible with linter rules 189 | jlpm lint 190 | jlpm lint:check 191 | pip install -e . 192 | jupyter labextension develop . --overwrite 193 | jupyter labextension list 194 | jupyter labextension list 2>&1 | grep -ie "myextension.*OK" 195 | python -m jupyterlab.browser_check 196 | 197 | jupyter labextension uninstall myextension 198 | pip uninstall -y myextension jupyterlab 199 | 200 | popd 201 | rm -rf myextension 202 | 203 | server: 204 | runs-on: ${{ matrix.os }} 205 | strategy: 206 | fail-fast: false 207 | matrix: 208 | os: [ubuntu-latest, macos-latest, windows-latest] 209 | # This will be used by the base setup action 210 | python-version: ["3.9", "3.13"] 211 | 212 | steps: 213 | - name: Checkout 214 | uses: actions/checkout@v4 215 | 216 | - name: Base Setup 217 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 218 | 219 | - name: Install dependencies 220 | run: | 221 | python -m pip install "copier>=9.2.0" jinja2-time build 222 | 223 | - name: Setup Git 224 | run: | 225 | git config --global user.name "github-actions[bot]" 226 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 227 | 228 | - name: Create server extension pip install 229 | env: 230 | YARN_ENABLE_IMMUTABLE_INSTALLS: false 231 | run: | 232 | mkdir myextension 233 | python -m copier copy -l -d kind=server -d author_name="My Name" -d repository="https://github.com/test/lab-extension" --vcs-ref HEAD --UNSAFE . myextension 234 | cd myextension 235 | cat pyproject.toml 236 | pip install . 237 | pip install "jupyterlab>=4.0.0,<5" 238 | jlpm 239 | jlpm lint:check 240 | 241 | - name: Check pip install method 242 | run: | 243 | set -eux 244 | jupyter server extension list 245 | jupyter server extension list 2>&1 | grep -ie "myextension.*OK" 246 | jupyter labextension list 247 | jupyter labextension list 2>&1 | grep -ie "myextension.*OK" 248 | 249 | # This test should be made outside the extension folder 250 | python -m jupyterlab.browser_check 251 | 252 | pip uninstall -y myextension jupyterlab 253 | rm -rf myextension 254 | shell: bash 255 | 256 | - name: Create server extension pip develop 257 | env: 258 | YARN_ENABLE_IMMUTABLE_INSTALLS: false 259 | run: | 260 | mkdir myextension 261 | python -m copier copy -l -d kind=server -d author_name="My Name" -d repository="https://github.com/test/lab-extension" --vcs-ref HEAD --UNSAFE . myextension 262 | cd myextension 263 | python -m pip install -e .[test] 264 | python -m pip install "jupyterlab>=4.0.0,<5" setuptools 265 | jupyter labextension develop . --overwrite 266 | jupyter server extension enable myextension 267 | 268 | # Check unit tests are passing 269 | python -m pytest -vv -r ap --cov myextension 270 | 271 | - name: Check pip develop method 272 | run: | 273 | set -eux 274 | jupyter server extension list 275 | jupyter server extension list 2>&1 | grep -ie "myextension.*OK" 276 | jupyter labextension list 277 | jupyter labextension list 2>&1 | grep -ie "myextension.*OK" 278 | 279 | # This test should be made outside the extension folder 280 | python -m jupyterlab.browser_check 281 | shell: bash 282 | 283 | - name: Build server extension in develop mode 284 | run: | 285 | jupyter labextension develop ./myextension --overwrite 286 | jupyter labextension build ./myextension 287 | 288 | jupyter labextension uninstall myextension 289 | python -m pip uninstall -y myextension jupyterlab 290 | 291 | - run: | 292 | set -eux 293 | rm -rf myextension 294 | shell: bash 295 | 296 | - name: Install server extension from a tarball 297 | env: 298 | YARN_ENABLE_IMMUTABLE_INSTALLS: false 299 | run: | 300 | mkdir myextension 301 | python -m copier copy -l -d kind=server -d author_name="My Name" -d repository="https://github.com/test/lab-extension" --vcs-ref HEAD --UNSAFE . myextension 302 | cd myextension 303 | python -m pip install "jupyterlab>=4.0.0,<5" 304 | jupyter lab clean --all 305 | python -m build 306 | cd dist 307 | python -m pip install myextension-0.1.0.tar.gz 308 | 309 | - name: Check install tarball method 310 | run: | 311 | set -eux 312 | jupyter labextension list 313 | jupyter labextension list 2>&1 | grep -ie "myextension.*OK" 314 | jupyter server extension list 315 | jupyter server extension list 2>&1 | grep -ie "myextension.*OK" 316 | 317 | jupyter lab build --dev-build --no-minimize 318 | 319 | python -m jupyterlab.browser_check 320 | 321 | cp myextension/dist/*.tar.gz myextension.tar.gz 322 | cp myextension/dist/*.whl myextension.whl 323 | python -m pip uninstall -y myextension jupyterlab 324 | rm -rf myextension 325 | shell: bash 326 | 327 | - uses: actions/upload-artifact@v4 328 | if: startsWith(runner.os, 'Linux') 329 | with: 330 | name: myextension-sdist-${{ matrix.python-version }} 331 | path: | 332 | myextension.tar.gz 333 | myextension.whl 334 | 335 | test_isolated: 336 | needs: server 337 | runs-on: ubuntu-latest 338 | strategy: 339 | matrix: 340 | python-version: ["3.9", "3.13"] 341 | 342 | steps: 343 | - name: Checkout 344 | uses: actions/checkout@v4 345 | - name: Install Python 346 | uses: actions/setup-python@v5 347 | with: 348 | python-version: ${{ matrix.python-version }} 349 | architecture: "x64" 350 | - name: Setup pip cache 351 | uses: actions/cache@v4 352 | with: 353 | path: ~/.cache/pip 354 | key: pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} 355 | restore-keys: | 356 | pip-${{ matrix.python-version }}- 357 | pip- 358 | - uses: actions/download-artifact@v4 359 | with: 360 | name: myextension-sdist-${{ matrix.python-version }} 361 | - name: Install and Test 362 | run: | 363 | set -eux 364 | # Remove NodeJS, twice to take care of system and locally installed node versions. 365 | sudo rm -rf $(which node) 366 | sudo rm -rf $(which node) 367 | 368 | python -m pip install myextension.tar.gz 369 | python -m pip install "jupyterlab>=4.0.0,<5" 370 | jupyter labextension list 2>&1 | grep -ie "myextension.*OK" 371 | jupyter server extension list 372 | jupyter server extension list 2>&1 | grep -ie "myextension.*OK" 373 | python -m jupyterlab.browser_check --no-browser-test 374 | 375 | theme: 376 | runs-on: ubuntu-latest 377 | strategy: 378 | matrix: 379 | # This will be used by the base setup action 380 | python-version: ["3.9", "3.13"] 381 | 382 | steps: 383 | - name: Checkout 384 | uses: actions/checkout@v4 385 | 386 | - name: Base Setup 387 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 388 | 389 | - name: Install dependencies 390 | run: | 391 | python -m pip install "copier>=9.2.0" jinja2-time build 392 | 393 | - name: Setup Git 394 | run: | 395 | git config --global user.name "github-actions[bot]" 396 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 397 | 398 | - name: Create pure frontend extension 399 | run: | 400 | set -eux 401 | mkdir mytheme 402 | python -m copier copy -l -d kind=theme -d author_name="My Name" -d repository="https://github.com/test/lab-extension" --vcs-ref HEAD --UNSAFE . mytheme 403 | pushd mytheme 404 | python -m pip install "jupyterlab>=4.0.0,<5" setuptools 405 | YARN_ENABLE_IMMUTABLE_INSTALLS=false jlpm 406 | jlpm lint:check 407 | python -m pip install -e . 408 | jupyter labextension develop . --overwrite 409 | jupyter labextension list 410 | jupyter labextension list 2>&1 | grep -ie "mytheme.*OK" 411 | python -m jupyterlab.browser_check 412 | 413 | jupyter labextension uninstall mytheme 414 | python -m pip uninstall -y mytheme jupyterlab 415 | 416 | popd 417 | rm -rf mytheme 418 | 419 | mimerenderer: 420 | runs-on: ubuntu-latest 421 | strategy: 422 | matrix: 423 | # This will be used by the base setup action 424 | python-version: ["3.9", "3.13"] 425 | 426 | steps: 427 | - name: Checkout 428 | uses: actions/checkout@v4 429 | 430 | - name: Base Setup 431 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 432 | 433 | - name: Install dependencies 434 | run: | 435 | python -m pip install "copier>=9.2.0" jinja2-time build 436 | 437 | - name: Setup Git 438 | run: | 439 | git config --global user.name "github-actions[bot]" 440 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 441 | 442 | - name: Create pure frontend extension 443 | run: | 444 | set -eux 445 | mkdir myextension 446 | python -m copier copy -l -d kind=mimerenderer -d viewer_name="My Viewer" -d mimetype="application/vnd.my_org.my_type" -d mimetype_name="my_type" -d file_extension=".my_type" -d author_name="My Name" -d repository="https://github.com/test/lab-extension" --vcs-ref HEAD --UNSAFE . myextension 447 | pushd myextension 448 | python -m pip install "jupyterlab>=4.0.0,<5" setuptools 449 | YARN_ENABLE_IMMUTABLE_INSTALLS=false jlpm 450 | jlpm lint:check 451 | python -m pip install -e . 452 | jupyter labextension develop . --overwrite 453 | jupyter labextension list 454 | jupyter labextension list 2>&1 | grep -ie "myextension.*OK" 455 | python -m jupyterlab.browser_check 456 | 457 | jupyter labextension uninstall myextension 458 | python -m pip uninstall -y myextension jupyterlab 459 | 460 | popd 461 | rm -rf myextension 462 | 463 | pnpm_linker: 464 | runs-on: ubuntu-latest 465 | strategy: 466 | matrix: 467 | kind: ["frontend", "server", "theme"] 468 | 469 | steps: 470 | - name: Checkout 471 | uses: actions/checkout@v4 472 | 473 | - name: Base Setup 474 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 475 | 476 | - name: Install dependencies 477 | run: | 478 | python -m pip install "copier>=9.2.0" jinja2-time 479 | 480 | - name: Setup Git 481 | run: | 482 | git config --global user.name "github-actions[bot]" 483 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 484 | 485 | - name: Create pure frontend extension 486 | env: 487 | KIND: ${{ matrix.kind }} 488 | run: | 489 | set -eux 490 | mkdir myextension 491 | python -m copier copy -l -d kind=${KIND} -d author_name="My Name" -d labextension_name=myextension -d repository="https://github.com/test/lab-extension" --vcs-ref HEAD --UNSAFE . myextension 492 | pushd myextension 493 | sed -i 's/^\(nodeLinker:\s\).*$/\1pnpm/' .yarnrc.yml 494 | python -m pip install "jupyterlab>=4.0.0,<5" 495 | YARN_ENABLE_IMMUTABLE_INSTALLS=false jlpm 496 | if [ ! -d node_modules/.store ] ; then echo 'nodes_module directory should contain a .store directory when using pnpm nodeLinker'; exit 1; fi; 497 | jlpm build 498 | popd 499 | rm -rf myextension 500 | -------------------------------------------------------------------------------- /.github/workflows/update-workflows.yml: -------------------------------------------------------------------------------- 1 | name: Update releaser worflows 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | # Monthly on the 23th 7 | - cron: '0 2 23 * *' 8 | 9 | jobs: 10 | releaser_workflows: 11 | name: Update releaser workflows 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - run: | 22 | set -eux 23 | wget -O prep-release.yml https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/main/example-workflows/prep-release.yml 24 | wget -O publish-release.yml https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/main/example-workflows/publish-release.yml 25 | working-directory: template/.github/workflows 26 | 27 | - name: List files changed 28 | id: files-changed 29 | shell: bash -l {0} 30 | run: | 31 | set -ex 32 | export CHANGES=$(git status --porcelain | tee /tmp/modified.log | wc -l) 33 | cat /tmp/modified.log 34 | 35 | echo "N_CHANGES=${CHANGES}" >> $GITHUB_OUTPUT 36 | 37 | git diff 38 | 39 | - name: Commit any changes 40 | if: steps.files-changed.outputs.N_CHANGES != '0' 41 | shell: bash -l {0} 42 | run: | 43 | git config user.name "github-actions[bot]" 44 | git config user.email "github-actions[bot]@users.noreply.github.com" 45 | 46 | git pull --no-tags 47 | 48 | export SHA=$(git rev-parse --short HEAD) 49 | export BRANCH_NAME=new-releaser-workflows-${SHA} 50 | git checkout -b "${BRANCH_NAME}" 51 | 52 | # Needed for hub to create the pull request correctly 53 | # Ref: https://github.com/github/hub/issues/1538 https://github.com/github/hub/pull/1705 54 | git remote set-head origin --auto 55 | 56 | git add * 57 | git commit -m "Automatic update of the releaser workflows" 58 | 59 | git push --set-upstream origin "${BRANCH_NAME}" 60 | gh pr create -B "main" -t "Updated releaser workflows available at ${SHA}" -b "Update releaser workflows from official GitHub repository" 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.egg-info/ 5 | .ipynb_checkpoints 6 | *.tsbuildinfo 7 | .eslintcache 8 | .stylelintcache 9 | 10 | # Created by https://www.gitignore.io/api/python 11 | # Edit at https://www.gitignore.io/?templates=python 12 | 13 | ### Python ### 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | pip-wheel-metadata/ 37 | share/python-wheels/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .nox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | coverage/lcov-report/ 65 | coverage/lcov.info 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | .spyproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | 96 | # Mr Developer 97 | .mr.developer.cfg 98 | .project 99 | .pydevproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | .dmypy.json 107 | dmypy.json 108 | 109 | # Pyre type checker 110 | .pyre/ 111 | 112 | # End of https://www.gitignore.io/api/python 113 | 114 | # OSX files 115 | .DS_Store 116 | 117 | # Default extension dir 118 | myextension 119 | .vscode/ 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JupyterLab extension template 2 | 3 | [![Github Actions Status](https://github.com/jupyterlab/extension-template/workflows/CI/badge.svg)](https://github.com/jupyterlab/extension-template/actions/workflows/main.yml) 4 | 5 | A [copier](https://copier.readthedocs.io) template for creating 6 | a JupyterLab extension. Four kinds of extension are supported: 7 | - _frontend_: Pure frontend extension written in TypeScript. 8 | - _mimerenderer_: MIME renderer extension. 9 | - _server_: Extension with frontend (in TypeScript) and backend (in Python) parts. 10 | - _theme_: Theme for JupyterLab (using CSS variables). 11 | 12 | ## Use the template to create extension 13 | 14 | 1. Install copier and some plugins. 15 | 16 | With `pip`: 17 | 18 | ```sh 19 | pip install "copier~=9.2" jinja2-time 20 | ``` 21 | 22 | Or with `conda` / `mamba`: 23 | 24 | ```sh 25 | conda install -c conda-forge "copier>=9.2,<10" jinja2-time 26 | ``` 27 | 28 | 2. Create an extension directory and go to it. 29 | 30 | ```sh 31 | mkdir myextension 32 | cd myextension 33 | ``` 34 | 35 | 3. Use copier to generate an extension, following the prompts to fill all required information. 36 | 37 | ```sh 38 | copier copy --trust https://github.com/jupyterlab/extension-template . 39 | ``` 40 | 41 | > If you are using Visual Studio Code, you may be interested in the 42 | > [configuration template](https://github.com/jupyterlab/vscode-config-template) for JupyterLab extension. 43 | 44 | --- 45 | 46 | If you'd like to generate an extension for a older release, use the `--vcs-ref` option and give a tag or commit from this repository. 47 | 48 | ```sh 49 | copier copy --vcs-ref v4.0.0 --trust https://github.com/jupyterlab/extension-template . 50 | ``` 51 | 52 | > If you are looking for a template compatible with JupyterLab version prior to 4.0.0, look at 53 | > the [cookiecutter template](https://github.com/jupyterlab/extension-cookiecutter-ts) or the 54 | > [mimerenderer template](https://github.com/jupyterlab/mimerender-cookiecutter-ts). 55 | 56 | ## Update an extension to the latest template version 57 | 58 | > This only works with an older version of the _copier_ template. It does not work 59 | > with an extension generated using the cookiecutter template. In that case, you 60 | > could try the script `python -m jupyterlab.upgrade_extension`. 61 | 62 | Extension generated from the copier template can be [updated](https://copier.readthedocs.io/en/stable/updating/) 63 | with a newer version of the template by executing the command: 64 | 65 | ```sh 66 | copier update --trust 67 | ``` 68 | 69 | ## A simple example 70 | 71 | Your new extension includes a very simple example of a working extension. Use this example as a guide to build your own extension. Have a look at the [extension examples](https://github.com/jupyterlab/extension-examples) repository for more information on various JupyterLab features. 72 | -------------------------------------------------------------------------------- /copier.yml: -------------------------------------------------------------------------------- 1 | _min_copier_version: "7.1.0" 2 | _subdirectory: template 3 | _jinja_extensions: 4 | - jinja2_time.TimeExtension 5 | 6 | kind: 7 | type: str 8 | help: What is your extension kind? 9 | default: frontend 10 | choices: 11 | - frontend 12 | - mimerenderer 13 | - server 14 | - theme 15 | 16 | author_name: 17 | type: str 18 | help: Extension author name 19 | placeholder: "My Name" 20 | validator: >- 21 | {% if not (author_name | regex_search('^[^\s].*$')) %} 22 | author_name cannot be empty nor start with a blank character. 23 | {% endif %} 24 | 25 | author_email: 26 | type: str 27 | help: Extension author email 28 | default: "" 29 | placeholder: "me@test.com" 30 | # Allow empty email or test it against regex 31 | validator: >- 32 | {% if author_email and not (author_email | regex_search('^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$')) %} 33 | author_email must be a valid email address. 34 | {% endif %} 35 | 36 | labextension_name: 37 | type: str 38 | help: JavaScript package name 39 | default: "{% if kind == 'theme' %}mytheme{% else %}myextension{% endif %}" 40 | 41 | python_name: 42 | type: str 43 | help: Python package name 44 | default: "{{ labextension_name | replace('-', '_') | replace('/', '_') | trim('@') }}" 45 | 46 | project_short_description: 47 | type: str 48 | help: Extension short description 49 | default: "A JupyterLab extension." 50 | 51 | has_settings: 52 | when: "{{ kind != 'mimerenderer' }}" 53 | type: bool 54 | help: Does the extension have user settings or schema-defined UI elements? 55 | default: no 56 | 57 | has_binder: 58 | type: bool 59 | help: Do you want to set up Binder example? 60 | default: no 61 | 62 | test: 63 | type: bool 64 | help: Do you want to set up tests for the extension? 65 | default: yes 66 | 67 | repository: 68 | type: str 69 | help: Git remote repository URL 70 | placeholder: https://github.com/github_username/my-extension 71 | 72 | viewer_name: 73 | when: "{{ kind == 'mimerenderer' }}" 74 | type: str 75 | help: What is the MIME type viewer name? 76 | default: "" 77 | placeholder: My Viewer 78 | 79 | mimetype: 80 | when: "{{ kind == 'mimerenderer' }}" 81 | type: str 82 | help: MIME type 83 | default: "" 84 | placeholder: "application/vnd.my_organization.my_type" 85 | 86 | mimetype_name: 87 | when: "{{ kind == 'mimerenderer' }}" 88 | type: str 89 | help: MIME type name 90 | default: "" 91 | placeholder: my_type 92 | 93 | file_extension: 94 | when: "{{ kind == 'mimerenderer' }}" 95 | type: str 96 | help: MIME type file extension 97 | default: "" 98 | placeholder: .my_type 99 | 100 | data_format: 101 | when: "{{ kind == 'mimerenderer' }}" 102 | type: str 103 | help: MIME type content format 104 | choices: 105 | - string 106 | - json 107 | default: string 108 | -------------------------------------------------------------------------------- /template/.github/workflows/build.yml.jinja: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: '*' 8 | {% raw %} 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 11 | cancel-in-progress: true 12 | {% endraw %} 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Base Setup 22 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 23 | 24 | - name: Install dependencies 25 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 26 | 27 | - name: Lint the extension 28 | run: | 29 | set -eux 30 | jlpm 31 | jlpm run lint:check 32 | {% if test %} 33 | - name: Test the extension 34 | run: | 35 | set -eux 36 | jlpm run test 37 | {% endif %} 38 | - name: Build the extension 39 | run: | 40 | set -eux 41 | python -m pip install .[test] 42 | {% if kind.lower() == 'server' %}{% if test %} 43 | pytest -vv -r ap --cov {{ python_name }}{% endif %} 44 | jupyter server extension list 45 | jupyter server extension list 2>&1 | grep -ie "{{ python_name }}.*OK" 46 | {% endif %} 47 | jupyter labextension list 48 | jupyter labextension list 2>&1 | grep -ie "{{ labextension_name }}.*OK" 49 | python -m jupyterlab.browser_check 50 | 51 | - name: Package the extension 52 | run: | 53 | set -eux 54 | 55 | pip install build 56 | python -m build 57 | pip uninstall -y "{{ python_name }}" jupyterlab 58 | 59 | - name: Upload extension packages 60 | uses: actions/upload-artifact@v4 61 | with: 62 | name: extension-artifacts 63 | path: dist/{{ python_name }}* 64 | if-no-files-found: error 65 | 66 | test_isolated: 67 | needs: build 68 | runs-on: ubuntu-latest 69 | 70 | steps: 71 | - name: Install Python 72 | uses: actions/setup-python@v5 73 | with: 74 | python-version: '3.9' 75 | architecture: 'x64' 76 | - uses: actions/download-artifact@v4 77 | with: 78 | name: extension-artifacts 79 | - name: Install and Test 80 | run: | 81 | set -eux 82 | # Remove NodeJS, twice to take care of system and locally installed node versions. 83 | sudo rm -rf $(which node) 84 | sudo rm -rf $(which node) 85 | 86 | pip install "jupyterlab>=4.0.0,<5" {{ python_name }}*.whl 87 | 88 | {% if kind.lower() == 'server' %} 89 | jupyter server extension list 90 | jupyter server extension list 2>&1 | grep -ie "{{ python_name }}.*OK" 91 | {% endif %} 92 | jupyter labextension list 93 | jupyter labextension list 2>&1 | grep -ie "{{ labextension_name }}.*OK" 94 | python -m jupyterlab.browser_check --no-browser-test 95 | {% if test %} 96 | integration-tests: 97 | name: Integration tests 98 | needs: build 99 | runs-on: ubuntu-latest 100 | 101 | env: 102 | PLAYWRIGHT_BROWSERS_PATH: ${{ "{{ github.workspace }}" }}/pw-browsers 103 | 104 | steps: 105 | - name: Checkout 106 | uses: actions/checkout@v4 107 | 108 | - name: Base Setup 109 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 110 | 111 | - name: Download extension package 112 | uses: actions/download-artifact@v4 113 | with: 114 | name: extension-artifacts 115 | 116 | - name: Install the extension 117 | run: | 118 | set -eux 119 | python -m pip install "jupyterlab>=4.0.0,<5" {{ python_name }}*.whl 120 | 121 | - name: Install dependencies 122 | working-directory: ui-tests 123 | env: 124 | YARN_ENABLE_IMMUTABLE_INSTALLS: 0 125 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 126 | run: jlpm install 127 | {% raw %} 128 | - name: Set up browser cache 129 | uses: actions/cache@v4 130 | with: 131 | path: | 132 | ${{ github.workspace }}/pw-browsers 133 | key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} 134 | {% endraw %} 135 | - name: Install browser 136 | run: | 137 | set -eux 138 | jlpm playwright install-deps 139 | jlpm playwright install chromium 140 | working-directory: ui-tests 141 | 142 | - name: Execute integration tests 143 | working-directory: ui-tests 144 | run: | 145 | jlpm playwright test 146 | 147 | - name: Upload Playwright Test report 148 | if: always() 149 | uses: actions/upload-artifact@v4 150 | with: 151 | name: {{ python_name }}-playwright-tests 152 | path: | 153 | ui-tests/test-results 154 | ui-tests/playwright-report{% endif %} 155 | 156 | check_links: 157 | name: Check Links 158 | runs-on: ubuntu-latest 159 | timeout-minutes: 15 160 | steps: 161 | - uses: actions/checkout@v4 162 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 163 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 164 | -------------------------------------------------------------------------------- /template/.github/workflows/check-release.yml.jinja: -------------------------------------------------------------------------------- 1 | name: Check Release 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | branches: ["*"] 7 | {% raw %} 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | {% endraw %} 12 | jobs: 13 | check_release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Base Setup 19 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 20 | - name: Check Release 21 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 22 | with: 23 | {% raw %} 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | {% endraw %} 26 | - name: Upload Distributions 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: {{ python_name }}-releaser-dist-${{ '{{ github.run_number }}' }} 30 | path: .jupyter_releaser_checkout/dist 31 | -------------------------------------------------------------------------------- /template/.github/workflows/enforce-label.yml: -------------------------------------------------------------------------------- 1 | name: Enforce PR label 2 | 3 | on: 4 | pull_request: 5 | types: [labeled, unlabeled, opened, edited, synchronize] 6 | jobs: 7 | enforce-label: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | pull-requests: write 11 | steps: 12 | - name: enforce-triage-label 13 | uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1 14 | -------------------------------------------------------------------------------- /template/.github/workflows/prep-release.yml: -------------------------------------------------------------------------------- 1 | name: "Step 1: Prep Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version_spec: 6 | description: "New Version Specifier" 7 | default: "next" 8 | required: false 9 | branch: 10 | description: "The branch to target" 11 | required: false 12 | post_version_spec: 13 | description: "Post Version Specifier" 14 | required: false 15 | # silent: 16 | # description: "Set a placeholder in the changelog and don't publish the release." 17 | # required: false 18 | # type: boolean 19 | since: 20 | description: "Use PRs with activity since this date or git reference" 21 | required: false 22 | since_last_stable: 23 | description: "Use PRs with activity since the last stable git tag" 24 | required: false 25 | type: boolean 26 | jobs: 27 | prep_release: 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: write 31 | steps: 32 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 33 | 34 | - name: Prep Release 35 | id: prep-release 36 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | version_spec: ${{ github.event.inputs.version_spec }} 40 | # silent: ${{ github.event.inputs.silent }} 41 | post_version_spec: ${{ github.event.inputs.post_version_spec }} 42 | branch: ${{ github.event.inputs.branch }} 43 | since: ${{ github.event.inputs.since }} 44 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 45 | 46 | - name: "** Next Step **" 47 | run: | 48 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" 49 | -------------------------------------------------------------------------------- /template/.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: "Step 2: Publish Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | branch: 6 | description: "The target branch" 7 | required: false 8 | release_url: 9 | description: "The URL of the draft GitHub release" 10 | required: false 11 | steps_to_skip: 12 | description: "Comma separated list of steps to skip" 13 | required: false 14 | 15 | jobs: 16 | publish_release: 17 | runs-on: ubuntu-latest 18 | environment: release 19 | permissions: 20 | id-token: write 21 | steps: 22 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 23 | 24 | - uses: actions/create-github-app-token@v1 25 | id: app-token 26 | with: 27 | app-id: ${{ vars.APP_ID }} 28 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 29 | 30 | - name: Populate Release 31 | id: populate-release 32 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 33 | with: 34 | token: ${{ steps.app-token.outputs.token }} 35 | branch: ${{ github.event.inputs.branch }} 36 | release_url: ${{ github.event.inputs.release_url }} 37 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }} 38 | 39 | - name: Finalize Release 40 | id: finalize-release 41 | env: 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2 44 | with: 45 | token: ${{ steps.app-token.outputs.token }} 46 | release_url: ${{ steps.populate-release.outputs.release_url }} 47 | 48 | - name: "** Next Step **" 49 | if: ${{ success() }} 50 | run: | 51 | echo "Verify the final release" 52 | echo ${{ steps.finalize-release.outputs.release_url }} 53 | 54 | - name: "** Failure Message **" 55 | if: ${{ failure() }} 56 | run: | 57 | echo "Failed to Publish the Draft Release Url:" 58 | echo ${{ steps.populate-release.outputs.release_url }} 59 | -------------------------------------------------------------------------------- /template/.github/workflows/{% if has_binder %}binder-on-pr.yml{% endif %}: -------------------------------------------------------------------------------- 1 | name: Binder Badge 2 | on: 3 | pull_request_target: 4 | types: [opened] 5 | 6 | jobs: 7 | binder: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | pull-requests: write 11 | steps: 12 | - uses: jupyterlab/maintainer-tools/.github/actions/binder-link@v1 13 | with: 14 | github_token: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /template/.github/workflows/{% if test %}update-integration-tests.yml{% endif %}: -------------------------------------------------------------------------------- 1 | name: Update Playwright Snapshots 2 | 3 | on: 4 | issue_comment: 5 | types: [created, edited] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | update-snapshots: 13 | if: > 14 | ( 15 | github.event.issue.author_association == 'OWNER' || 16 | github.event.issue.author_association == 'COLLABORATOR' || 17 | github.event.issue.author_association == 'MEMBER' || 18 | github.event.comment.author_association == 'OWNER' || 19 | github.event.comment.author_association == 'COLLABORATOR' || 20 | github.event.comment.author_association == 'MEMBER' 21 | ) && github.event.issue.pull_request && contains(github.event.comment.body, 'please update snapshots') 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: React to the triggering comment 26 | run: | 27 | gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions --raw-field 'content=+1' 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | with: 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Get PR Info 37 | id: pr 38 | env: 39 | PR_NUMBER: ${{ github.event.issue.number }} 40 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | GH_REPO: ${{ github.repository }} 42 | COMMENT_AT: ${{ github.event.comment.created_at }} 43 | run: | 44 | pr="$(gh api /repos/${GH_REPO}/pulls/${PR_NUMBER})" 45 | head_sha="$(echo "$pr" | jq -r .head.sha)" 46 | pushed_at="$(echo "$pr" | jq -r .pushed_at)" 47 | 48 | if [[ $(date -d "$pushed_at" +%s) -gt $(date -d "$COMMENT_AT" +%s) ]]; then 49 | echo "Updating is not allowed because the PR was pushed to (at $pushed_at) after the triggering comment was issued (at $COMMENT_AT)" 50 | exit 1 51 | fi 52 | 53 | echo "head_sha=$head_sha" >> $GITHUB_OUTPUT 54 | 55 | - name: Checkout the branch from the PR that triggered the job 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | run: gh pr checkout ${{ github.event.issue.number }} 59 | 60 | - name: Validate the fetched branch HEAD revision 61 | env: 62 | EXPECTED_SHA: ${{ steps.pr.outputs.head_sha }} 63 | run: | 64 | actual_sha="$(git rev-parse HEAD)" 65 | 66 | if [[ "$actual_sha" != "$EXPECTED_SHA" ]]; then 67 | echo "The HEAD of the checked out branch ($actual_sha) differs from the HEAD commit available at the time when trigger comment was submitted ($EXPECTED_SHA)" 68 | exit 1 69 | fi 70 | 71 | - name: Base Setup 72 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 73 | 74 | - name: Install dependencies 75 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 76 | 77 | - name: Install extension 78 | run: | 79 | set -eux 80 | jlpm 81 | python -m pip install . 82 | 83 | - uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@v1 84 | with: 85 | github_token: ${{ secrets.GITHUB_TOKEN }} 86 | # Playwright knows how to start JupyterLab server 87 | start_server_script: 'null' 88 | test_folder: ui-tests 89 | npm_client: jlpm 90 | -------------------------------------------------------------------------------- /template/.gitignore.jinja: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.log 5 | .eslintcache 6 | .stylelintcache 7 | *.egg-info/ 8 | .ipynb_checkpoints 9 | *.tsbuildinfo 10 | {{python_name}}/labextension 11 | # Version file is handled by hatchling 12 | {{python_name}}/_version.py 13 | {% if test %} 14 | # Integration tests 15 | ui-tests/test-results/ 16 | ui-tests/playwright-report/ 17 | {% endif %} 18 | # Created by https://www.gitignore.io/api/python 19 | # Edit at https://www.gitignore.io/?templates=python 20 | 21 | ### Python ### 22 | # Byte-compiled / optimized / DLL files 23 | __pycache__/ 24 | *.py[cod] 25 | *$py.class 26 | 27 | # C extensions 28 | *.so 29 | 30 | # Distribution / packaging 31 | .Python 32 | build/ 33 | develop-eggs/ 34 | dist/ 35 | downloads/ 36 | eggs/ 37 | .eggs/ 38 | lib/ 39 | lib64/ 40 | parts/ 41 | sdist/ 42 | var/ 43 | wheels/ 44 | pip-wheel-metadata/ 45 | share/python-wheels/ 46 | .installed.cfg 47 | *.egg 48 | MANIFEST 49 | 50 | # PyInstaller 51 | # Usually these files are written by a python script from a template 52 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 53 | *.manifest 54 | *.spec 55 | 56 | # Installer logs 57 | pip-log.txt 58 | pip-delete-this-directory.txt 59 | 60 | # Unit test / coverage reports 61 | htmlcov/ 62 | .tox/ 63 | .nox/ 64 | .coverage 65 | .coverage.* 66 | .cache 67 | nosetests.xml 68 | coverage/ 69 | coverage.xml 70 | *.cover 71 | .hypothesis/ 72 | .pytest_cache/ 73 | 74 | # Translations 75 | *.mo 76 | *.pot 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # celery beat schedule file 91 | celerybeat-schedule 92 | 93 | # SageMath parsed files 94 | *.sage.py 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # Mr Developer 104 | .mr.developer.cfg 105 | .project 106 | .pydevproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | # Pyre type checker 117 | .pyre/ 118 | 119 | # End of https://www.gitignore.io/api/python 120 | 121 | # OSX files 122 | .DS_Store 123 | 124 | # Yarn cache 125 | .yarn/ 126 | -------------------------------------------------------------------------------- /template/.prettierignore.jinja: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | !/package.json 6 | {{python_name}} 7 | -------------------------------------------------------------------------------- /template/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /template/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /template/LICENSE.jinja: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) {% now 'utc', '%Y' %}, {{ author_name }} 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /template/README.md.jinja: -------------------------------------------------------------------------------- 1 | # {{ python_name }} 2 | 3 | [![Github Actions Status]({{ repository|trim('/') }}/workflows/Build/badge.svg)]({{ repository|trim('/') }}/actions/workflows/build.yml) 4 | {% if has_binder -%} 5 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/{{ repository|replace("https://github.com/", "") }}/main?urlpath=lab) 6 | 7 | {% endif %} 8 | {{ project_short_description }} 9 | {% if kind.lower() == 'server' %} 10 | This extension is composed of a Python package named `{{ python_name }}` 11 | for the server extension and a NPM package named `{{ labextension_name }}` 12 | for the frontend extension. 13 | {% endif %} 14 | ## Requirements 15 | 16 | - JupyterLab >= 4.0.0 17 | 18 | ## Install 19 | 20 | To install the extension, execute: 21 | 22 | ```bash 23 | pip install {{ python_name }} 24 | ``` 25 | 26 | ## Uninstall 27 | 28 | To remove the extension, execute: 29 | 30 | ```bash 31 | pip uninstall {{ python_name }} 32 | ``` 33 | {% if kind.lower() == 'server' %} 34 | ## Troubleshoot 35 | 36 | If you are seeing the frontend extension, but it is not working, check 37 | that the server extension is enabled: 38 | 39 | ```bash 40 | jupyter server extension list 41 | ``` 42 | 43 | If the server extension is installed and enabled, but you are not seeing 44 | the frontend extension, check the frontend extension is installed: 45 | 46 | ```bash 47 | jupyter labextension list 48 | ``` 49 | {% endif %} 50 | ## Contributing 51 | 52 | ### Development install 53 | 54 | Note: You will need NodeJS to build the extension package. 55 | 56 | The `jlpm` command is JupyterLab's pinned version of 57 | [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use 58 | `yarn` or `npm` in lieu of `jlpm` below. 59 | 60 | ```bash 61 | # Clone the repo to your local environment 62 | # Change directory to the {{ python_name }} directory 63 | # Install package in development mode 64 | pip install -e ".{% if test and kind.lower() == 'server' %}[test]{% endif %}" 65 | # Link your development version of the extension with JupyterLab 66 | jupyter labextension develop . --overwrite{% if kind.lower() == 'server' %} 67 | # Server extension must be manually installed in develop mode 68 | jupyter server extension enable {{ python_name }}{% endif %} 69 | # Rebuild extension Typescript source after making changes 70 | jlpm build 71 | ``` 72 | 73 | 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 extension. 74 | 75 | ```bash 76 | # Watch the source directory in one terminal, automatically rebuilding when needed 77 | jlpm watch 78 | # Run JupyterLab in another terminal 79 | jupyter lab 80 | ``` 81 | 82 | With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). 83 | 84 | By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command: 85 | 86 | ```bash 87 | jupyter lab build --minimize=False 88 | ``` 89 | 90 | ### Development uninstall 91 | 92 | ```bash{% if kind.lower() == 'server' %} 93 | # Server extension must be manually disabled in develop mode 94 | jupyter server extension disable {{ python_name }}{% endif %} 95 | pip uninstall {{ python_name }} 96 | ``` 97 | 98 | In development mode, you will also need to remove the symlink created by `jupyter labextension develop` 99 | command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` 100 | folder is located. Then you can remove the symlink named `{{ labextension_name }}` within that folder. 101 | {% if test %} 102 | ### Testing the extension{% if kind.lower() == 'server' %} 103 | 104 | #### Server tests 105 | 106 | This extension is using [Pytest](https://docs.pytest.org/) for Python code testing. 107 | 108 | Install test dependencies (needed only once): 109 | 110 | ```sh 111 | pip install -e ".[test]" 112 | # Each time you install the Python package, you need to restore the front-end extension link 113 | jupyter labextension develop . --overwrite 114 | ``` 115 | 116 | To execute them, run: 117 | 118 | ```sh 119 | pytest -vv -r ap --cov {{ python_name }} 120 | ```{% endif %} 121 | 122 | #### Frontend tests 123 | 124 | This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. 125 | 126 | To execute them, execute: 127 | 128 | ```sh 129 | jlpm 130 | jlpm test 131 | ``` 132 | 133 | #### Integration tests 134 | 135 | This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests). 136 | More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. 137 | 138 | More information are provided within the [ui-tests](./ui-tests/README.md) README. 139 | {% endif %} 140 | ### Packaging the extension 141 | 142 | See [RELEASE](RELEASE.md) 143 | -------------------------------------------------------------------------------- /template/RELEASE.md.jinja: -------------------------------------------------------------------------------- 1 | # Making a new release of {{ python_name }} 2 | 3 | The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). 4 | 5 | ## Manual release 6 | 7 | ### Python package 8 | 9 | This extension can be distributed as Python packages. All of the Python 10 | packaging instructions are in the `pyproject.toml` file to wrap your extension in a 11 | Python package. Before generating a package, you first need to install some tools: 12 | 13 | ```bash 14 | pip install build twine hatch 15 | ``` 16 | 17 | Bump the version using `hatch`. By default this will create a tag. 18 | See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. 19 | 20 | ```bash 21 | hatch version 22 | ``` 23 | 24 | Make sure to clean up all the development files before building the package: 25 | 26 | ```bash 27 | jlpm clean:all 28 | ``` 29 | 30 | You could also clean up the local git repository: 31 | 32 | ```bash 33 | git clean -dfX 34 | ``` 35 | 36 | To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: 37 | 38 | ```bash 39 | python -m build 40 | ``` 41 | 42 | > `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. 43 | 44 | Then to upload the package to PyPI, do: 45 | 46 | ```bash 47 | twine upload dist/* 48 | ``` 49 | 50 | ### NPM package 51 | 52 | To publish the frontend part of the extension as a NPM package, do: 53 | 54 | ```bash 55 | npm login 56 | npm publish --access public 57 | ``` 58 | 59 | ## Automated releases with the Jupyter Releaser 60 | 61 | The extension repository should already be compatible with the Jupyter Releaser. But 62 | the GitHub repository and the package managers need to be properly set up. Please 63 | follow the instructions of the Jupyter Releaser [checklist](https://jupyter-releaser.readthedocs.io/en/latest/how_to_guides/convert_repo_from_repo.html). 64 | 65 | Here is a summary of the steps to cut a new release: 66 | 67 | - Go to the Actions panel 68 | - Run the "Step 1: Prep Release" workflow 69 | - Check the draft changelog 70 | - Run the "Step 2: Publish Release" workflow 71 | 72 | > [!NOTE] 73 | > Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) 74 | > for more information. 75 | 76 | ## Publishing to `conda-forge` 77 | 78 | If the package is not on conda forge yet, check the documentation to learn how to add it: https://conda-forge.org/docs/maintainer/adding_pkgs.html 79 | 80 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. 81 | -------------------------------------------------------------------------------- /template/install.json.jinja: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "{{ python_name }}", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package {{ python_name }}" 5 | } 6 | -------------------------------------------------------------------------------- /template/package.json.jinja: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ labextension_name }}", 3 | "version": "0.1.0", 4 | "description": "{{ project_short_description | replace('"', '\\"') }}", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "{{ repository }}", 11 | "bugs": { 12 | "url": "{{ repository | trim('/') }}/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": {% if author_email %}{ 16 | "name": "{{ author_name }}", 17 | "email": "{{ author_email }}" 18 | },{% else %}"{{ author_name }}",{% endif %} 19 | "files": [ 20 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 21 | "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", 22 | "src/**/*.{ts,tsx}"{% if has_settings %}, 23 | "schema/*.json"{% endif %} 24 | ], 25 | "main": "lib/index.js", 26 | "types": "lib/index.d.ts",{% if kind != 'theme' %} 27 | "style": "style/index.css",{% endif %} 28 | "repository": { 29 | "type": "git", 30 | "url": "{{ repository | trim('/') }}.git" 31 | }, 32 | "scripts": { 33 | "build": "jlpm build:lib && jlpm build:labextension:dev", 34 | "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", 35 | "build:labextension": "jupyter labextension build .", 36 | "build:labextension:dev": "jupyter labextension build --development True .", 37 | "build:lib": "tsc --sourceMap", 38 | "build:lib:prod": "tsc", 39 | "clean": "jlpm clean:lib", 40 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 41 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 42 | "clean:labextension": "rimraf {{ python_name }}/labextension {{ python_name }}/_version.py", 43 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", 44 | "eslint": "jlpm eslint:check --fix", 45 | "eslint:check": "eslint . --cache --ext .ts,.tsx", 46 | "install:extension": "jlpm build", 47 | "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", 48 | "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", 49 | "prettier": "jlpm prettier:base --write --list-different", 50 | "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", 51 | "prettier:check": "jlpm prettier:base --check", 52 | "stylelint": "jlpm stylelint:check --fix", 53 | "stylelint:check": "stylelint --cache \"style/**/*.css\"",{% if test %} 54 | "test": "jest --coverage",{% endif %} 55 | "watch": "run-p watch:src watch:labextension", 56 | "watch:src": "tsc -w --sourceMap", 57 | "watch:labextension": "jupyter labextension watch ." 58 | }, 59 | "dependencies": { 60 | {% if kind.lower() != 'mimerenderer' %}"@jupyterlab/application": "^4.0.0"{% if kind.lower() == 'theme' %}, 61 | "@jupyterlab/apputils": "^4.0.0"{% endif %}{% if kind.lower() == 'server' %}, 62 | "@jupyterlab/coreutils": "^6.0.0", 63 | "@jupyterlab/services": "^7.0.0"{% endif %}{% if has_settings %}, 64 | "@jupyterlab/settingregistry": "^4.0.0"{% endif %}{% else %}"@jupyterlab/rendermime-interfaces": "^3.8.0", 65 | "@lumino/widgets": "^2.1.0"{% endif %} 66 | }, 67 | "devDependencies": { 68 | "@jupyterlab/builder": "^4.0.0",{% if test %} 69 | "@jupyterlab/testutils": "^4.0.0", 70 | "@types/jest": "^29.2.0",{% endif %} 71 | "@types/json-schema": "^7.0.11", 72 | "@types/react": "^18.0.26", 73 | "@types/react-addons-linked-state-mixin": "^0.14.22", 74 | "@typescript-eslint/eslint-plugin": "^6.1.0", 75 | "@typescript-eslint/parser": "^6.1.0", 76 | "css-loader": "^6.7.1", 77 | "eslint": "^8.36.0", 78 | "eslint-config-prettier": "^8.8.0", 79 | "eslint-plugin-prettier": "^5.0.0",{% if test %} 80 | "jest": "^29.2.0",{% endif %}{% if kind.lower() == 'server' %} 81 | "mkdirp": "^1.0.3",{% endif %} 82 | "npm-run-all2": "^7.0.1", 83 | "prettier": "^3.0.0", 84 | "rimraf": "^5.0.1", 85 | "source-map-loader": "^1.0.2", 86 | "style-loader": "^3.3.1", 87 | "stylelint": "^15.10.1", 88 | "stylelint-config-recommended": "^13.0.0", 89 | "stylelint-config-standard": "^34.0.0", 90 | "stylelint-csstree-validator": "^3.0.0", 91 | "stylelint-prettier": "^4.0.0", 92 | "typescript": "~5.0.2", 93 | "yjs": "^13.5.0" 94 | }, 95 | "sideEffects": [ 96 | "style/*.css"{% if kind.lower() != 'theme' %}, 97 | "style/index.js" 98 | ], 99 | "styleModule": "style/index.js",{% else %} 100 | ],{% endif %} 101 | "publishConfig": { 102 | "access": "public" 103 | }, 104 | "jupyterlab": { {%- if kind.lower() == 'server' %} 105 | "discovery": { 106 | "server": { 107 | "managers": [ 108 | "pip" 109 | ], 110 | "base": { 111 | "name": "{{ python_name }}" 112 | } 113 | } 114 | },{% endif %}{% if kind.lower() == 'mimerenderer' %} 115 | "mimeExtension": true,{% else %} 116 | "extension": true,{% endif %} 117 | "outputDir": "{{python_name}}/labextension"{% if has_settings %}, 118 | "schemaDir": "schema"{% endif %}{% if kind.lower() == 'theme' %}, 119 | "themePath": "style/index.css"{% endif %} 120 | }, 121 | "eslintIgnore": [ 122 | "node_modules", 123 | "dist", 124 | "coverage", 125 | "**/*.d.ts"{% if test %}, 126 | "tests", 127 | "**/__tests__", 128 | "ui-tests"{% endif %} 129 | ], 130 | "eslintConfig": { 131 | "extends": [ 132 | "eslint:recommended", 133 | "plugin:@typescript-eslint/eslint-recommended", 134 | "plugin:@typescript-eslint/recommended", 135 | "plugin:prettier/recommended" 136 | ], 137 | "parser": "@typescript-eslint/parser", 138 | "parserOptions": { 139 | "project": "tsconfig.json", 140 | "sourceType": "module" 141 | }, 142 | "plugins": [ 143 | "@typescript-eslint" 144 | ], 145 | "rules": { 146 | "@typescript-eslint/naming-convention": [ 147 | "error", 148 | { 149 | "selector": "interface", 150 | "format": [ 151 | "PascalCase" 152 | ], 153 | "custom": { 154 | "regex": "^I[A-Z]", 155 | "match": true 156 | } 157 | } 158 | ], 159 | "@typescript-eslint/no-unused-vars": [ 160 | "warn", 161 | { 162 | "args": "none" 163 | } 164 | ], 165 | "@typescript-eslint/no-explicit-any": "off", 166 | "@typescript-eslint/no-namespace": "off", 167 | "@typescript-eslint/no-use-before-define": "off", 168 | "@typescript-eslint/quotes": [ 169 | "error", 170 | "single", 171 | { 172 | "avoidEscape": true, 173 | "allowTemplateLiterals": false 174 | } 175 | ], 176 | "curly": [ 177 | "error", 178 | "all" 179 | ], 180 | "eqeqeq": "error", 181 | "prefer-arrow-callback": "error" 182 | } 183 | }, 184 | "prettier": { 185 | "singleQuote": true, 186 | "trailingComma": "none", 187 | "arrowParens": "avoid", 188 | "endOfLine": "auto", 189 | "overrides": [ 190 | { 191 | "files": "package.json", 192 | "options": { 193 | "tabWidth": 4 194 | } 195 | } 196 | ] 197 | }, 198 | "stylelint": { 199 | "extends": [ 200 | "stylelint-config-recommended", 201 | "stylelint-config-standard", 202 | "stylelint-prettier/recommended" 203 | ], 204 | "plugins": [ 205 | "stylelint-csstree-validator" 206 | ], 207 | "rules": { 208 | "csstree/validator": true, 209 | "property-no-vendor-prefix": null, 210 | "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$", 211 | "selector-no-vendor-prefix": null, 212 | "value-no-vendor-prefix": null{% if kind.lower() == "theme" %}, 213 | "alpha-value-notation": null, 214 | "color-function-notation": null{% endif %} 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /template/pyproject.toml.jinja: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version>=0.3.2"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "{{ python_name }}" 7 | readme = "README.md" 8 | license = { file = "LICENSE" } 9 | requires-python = ">=3.9" 10 | classifiers = [ 11 | "Framework :: Jupyter", 12 | "Framework :: Jupyter :: JupyterLab", 13 | "Framework :: Jupyter :: JupyterLab :: 4", 14 | "Framework :: Jupyter :: JupyterLab :: Extensions",{% if kind.lower() == "mimerenderer" %} 15 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Mime Renderers",{% endif %} 16 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt",{% if kind.lower() == "theme" %} 17 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Themes",{% endif %} 18 | "License :: OSI Approved :: BSD License", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Programming Language :: Python :: 3.12", 25 | "Programming Language :: Python :: 3.13", 26 | ] 27 | dependencies = [{% if kind.lower() == "server" %} 28 | "jupyter_server>=2.4.0,<3"{% endif %} 29 | ] 30 | dynamic = ["version", "description", "authors", "urls", "keywords"] 31 | {% if test and kind.lower() == 'server' %} 32 | [project.optional-dependencies] 33 | test = [ 34 | "coverage", 35 | "pytest", 36 | "pytest-asyncio", 37 | "pytest-cov", 38 | "pytest-jupyter[server]>=0.6.0" 39 | ] 40 | {% endif %} 41 | [tool.hatch.version] 42 | source = "nodejs" 43 | 44 | [tool.hatch.metadata.hooks.nodejs] 45 | fields = ["description", "authors", "urls", "keywords"] 46 | 47 | [tool.hatch.build.targets.sdist] 48 | artifacts = ["{{ python_name }}/labextension"] 49 | exclude = [".github", "binder"] 50 | 51 | [tool.hatch.build.targets.wheel.shared-data] 52 | "{{ python_name }}/labextension" = "share/jupyter/labextensions/{{ labextension_name }}" 53 | "install.json" = "share/jupyter/labextensions/{{ labextension_name }}/install.json"{% if kind.lower() == "server" %} 54 | "jupyter-config/server-config" = "etc/jupyter/jupyter_server_config.d"{% endif %} 55 | 56 | [tool.hatch.build.hooks.version] 57 | path = "{{ python_name }}/_version.py" 58 | 59 | [tool.hatch.build.hooks.jupyter-builder] 60 | dependencies = ["hatch-jupyter-builder>=0.5"] 61 | build-function = "hatch_jupyter_builder.npm_builder" 62 | ensured-targets = [{% if kind.lower() != "theme" %} 63 | "{{ python_name }}/labextension/static/style.js",{% endif %} 64 | "{{ python_name }}/labextension/package.json", 65 | ] 66 | skip-if-exists = ["{{ python_name }}/labextension/static/style.js"] 67 | 68 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 69 | build_cmd = "build:prod" 70 | npm = ["jlpm"] 71 | 72 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] 73 | build_cmd = "install:extension" 74 | npm = ["jlpm"] 75 | source_dir = "src" 76 | build_dir = "{{python_name}}/labextension" 77 | 78 | [tool.jupyter-releaser.options] 79 | version_cmd = "hatch version" 80 | 81 | [tool.jupyter-releaser.hooks] 82 | before-build-npm = [ 83 | "python -m pip install 'jupyterlab>=4.0.0,<5'", 84 | "jlpm", 85 | "jlpm build:prod" 86 | ] 87 | before-build-python = ["jlpm clean:all"] 88 | 89 | [tool.check-wheel-contents] 90 | ignore = ["W002"] 91 | -------------------------------------------------------------------------------- /template/setup.py: -------------------------------------------------------------------------------- 1 | __import__("setuptools").setup() 2 | -------------------------------------------------------------------------------- /template/src/index.ts.jinja: -------------------------------------------------------------------------------- 1 | {% if kind.lower() != 'mimerenderer' %}import { 2 | JupyterFrontEnd, 3 | JupyterFrontEndPlugin 4 | } from '@jupyterlab/application';{% if kind.lower() == 'theme' %} 5 | 6 | import { IThemeManager } from '@jupyterlab/apputils';{% endif %}{% if has_settings %} 7 | 8 | import { ISettingRegistry } from '@jupyterlab/settingregistry';{% endif %}{% if kind.lower() == 'server' %} 9 | 10 | import { requestAPI } from './handler';{% endif %} 11 | 12 | /** 13 | * Initialization data for the {{ labextension_name }} extension. 14 | */ 15 | const plugin: JupyterFrontEndPlugin = { 16 | id: '{{ labextension_name }}:plugin', 17 | description: '{{ project_short_description | replace("'", "\\'") }}', 18 | autoStart: true,{% if kind.lower() == 'theme' %} 19 | requires: [IThemeManager],{% endif %}{% if has_settings %} 20 | optional: [ISettingRegistry],{% endif %} 21 | activate: (app: JupyterFrontEnd{% if kind.lower() == 'theme' %}, manager: IThemeManager{% endif %}{% if has_settings %}, settingRegistry: ISettingRegistry | null{% endif %}) => { 22 | console.log('JupyterLab extension {{ labextension_name }} is activated!');{% if kind.lower() == 'theme' %} 23 | const style = '{{ labextension_name }}/index.css'; 24 | 25 | manager.register({ 26 | name: '{{ labextension_name }}', 27 | isLight: true, 28 | load: () => manager.loadCSS(style), 29 | unload: () => Promise.resolve(undefined) 30 | });{% endif %}{% if has_settings %} 31 | 32 | if (settingRegistry) { 33 | settingRegistry 34 | .load(plugin.id) 35 | .then(settings => { 36 | console.log('{{ labextension_name }} settings loaded:', settings.composite); 37 | }) 38 | .catch(reason => { 39 | console.error('Failed to load settings for {{ labextension_name }}.', reason); 40 | }); 41 | }{% endif %}{% if kind.lower() == 'server' %} 42 | 43 | requestAPI('get-example') 44 | .then(data => { 45 | console.log(data); 46 | }) 47 | .catch(reason => { 48 | console.error( 49 | `The {{ python_name }} server extension appears to be missing.\n${reason}` 50 | ); 51 | });{% endif %} 52 | } 53 | }; 54 | 55 | export default plugin; 56 | {% else %}import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; 57 | {% if data_format == 'json' %} 58 | import { JSONObject } from '@lumino/coreutils'; 59 | {% endif %} 60 | import { Widget } from '@lumino/widgets'; 61 | 62 | /** 63 | * The default mime type for the extension. 64 | */ 65 | const MIME_TYPE = '{{ mimetype }}'; 66 | 67 | /** 68 | * The class name added to the extension. 69 | */ 70 | const CLASS_NAME = 'mimerenderer-{{ mimetype_name }}'; 71 | 72 | /** 73 | * A widget for rendering {{ mimetype_name }}. 74 | */ 75 | export class OutputWidget extends Widget implements IRenderMime.IRenderer { 76 | /** 77 | * Construct a new output widget. 78 | */ 79 | constructor(options: IRenderMime.IRendererOptions) { 80 | super(); 81 | this._mimeType = options.mimeType; 82 | this.addClass(CLASS_NAME); 83 | } 84 | 85 | /** 86 | * Render {{mimetype_name}} into this widget's node. 87 | */ 88 | renderModel(model: IRenderMime.IMimeModel): Promise { 89 | {% if data_format == 'json' %}const data = model.data[this._mimeType] as JSONObject; 90 | this.node.textContent = JSON.stringify(data); 91 | {% else %}const data = model.data[this._mimeType] as string; 92 | this.node.textContent = data.slice(0, 16384);{% endif %} 93 | return Promise.resolve(); 94 | } 95 | 96 | private _mimeType: string; 97 | } 98 | 99 | /** 100 | * A mime renderer factory for {{ mimetype_name }} data. 101 | */ 102 | export const rendererFactory: IRenderMime.IRendererFactory = { 103 | safe: true, 104 | mimeTypes: [MIME_TYPE], 105 | createRenderer: options => new OutputWidget(options) 106 | }; 107 | 108 | /** 109 | * Extension definition. 110 | */ 111 | const extension: IRenderMime.IExtension = { 112 | id: '{{labextension_name}}:plugin', 113 | // description: 'Adds MIME type renderer for {{ mimetype_name }} content', 114 | rendererFactory, 115 | rank: 100, 116 | dataType: '{{ data_format }}', 117 | fileTypes: [ 118 | { 119 | name: '{{ mimetype_name }}', 120 | mimeTypes: [MIME_TYPE], 121 | extensions: ['{{ file_extension }}'] 122 | } 123 | ], 124 | documentWidgetFactoryOptions: { 125 | name: '{{ viewer_name }}', 126 | primaryFileType: '{{ mimetype_name }}', 127 | fileTypes: ['{{ mimetype_name }}'], 128 | defaultFor: ['{{ mimetype_name }}'] 129 | } 130 | }; 131 | 132 | export default extension; 133 | {% endif %} -------------------------------------------------------------------------------- /template/src/{% if kind == 'server' %}handler.ts{% endif %}.jinja: -------------------------------------------------------------------------------- 1 | import { URLExt } from '@jupyterlab/coreutils'; 2 | 3 | import { ServerConnection } from '@jupyterlab/services'; 4 | 5 | /** 6 | * Call the API extension 7 | * 8 | * @param endPoint API REST end point for the extension 9 | * @param init Initial values for the request 10 | * @returns The response body interpreted as JSON 11 | */ 12 | export async function requestAPI( 13 | endPoint = '', 14 | init: RequestInit = {} 15 | ): Promise { 16 | // Make request to Jupyter API 17 | const settings = ServerConnection.makeSettings(); 18 | const requestUrl = URLExt.join( 19 | settings.baseUrl, 20 | '{{ python_name | replace("_", "-") }}', // API Namespace 21 | endPoint 22 | ); 23 | 24 | let response: Response; 25 | try { 26 | response = await ServerConnection.makeRequest(requestUrl, init, settings); 27 | } catch (error) { 28 | throw new ServerConnection.NetworkError(error as any); 29 | } 30 | 31 | let data: any = await response.text(); 32 | 33 | if (data.length > 0) { 34 | try { 35 | data = JSON.parse(data); 36 | } catch (error) { 37 | console.log('Not a JSON response body.', response); 38 | } 39 | } 40 | 41 | if (!response.ok) { 42 | throw new ServerConnection.ResponseError(response, data.message || data); 43 | } 44 | 45 | return data; 46 | } 47 | -------------------------------------------------------------------------------- /template/src/{% if test %}__tests__{% endif %}/{{python_name}}.spec.ts.jinja: -------------------------------------------------------------------------------- 1 | /** 2 | * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests 3 | */ 4 | 5 | describe('{{ labextension_name }}', () => { 6 | it('should be tested', () => { 7 | expect(1 + 1).toEqual(2); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /template/style/index.css.jinja: -------------------------------------------------------------------------------- 1 | {% if kind.lower() == 'theme' %}@import url('./variables.css'); 2 | 3 | /* Set the default typography for monospace elements */ 4 | tt, 5 | code, 6 | kbd, 7 | samp, 8 | pre { 9 | font-family: var(--jp-code-font-family); 10 | font-size: var(--jp-code-font-size); 11 | line-height: var(--jp-code-line-height); 12 | }{% else %}@import url('base.css');{% endif %} 13 | -------------------------------------------------------------------------------- /template/style/{% if kind != 'theme' %}base.css{% endif %}: -------------------------------------------------------------------------------- 1 | /* 2 | See the JupyterLab Developer Guide for useful CSS Patterns: 3 | 4 | https://jupyterlab.readthedocs.io/en/stable/developer/css.html 5 | */ 6 | -------------------------------------------------------------------------------- /template/style/{% if kind != 'theme' %}index.js{% endif %}: -------------------------------------------------------------------------------- 1 | import './base.css'; 2 | -------------------------------------------------------------------------------- /template/style/{% if kind == 'theme' %}variables.css{% endif %}: -------------------------------------------------------------------------------- 1 | /* ---------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |--------------------------------------------------------------------------- */ 5 | 6 | /* 7 | The following CSS variables define the main, public API for styling JupyterLab. 8 | These variables should be used by all plugins wherever possible. In other 9 | words, plugins should not define custom colors, sizes, etc unless absolutely 10 | necessary. This enables users to change the visual theme of JupyterLab 11 | by changing these variables. 12 | 13 | Many variables appear in an ordered sequence (0,1,2,3). These sequences 14 | are designed to work well together, so for example, `--jp-border-color1` should 15 | be used with `--jp-layout-color1`. The numbers have the following meanings: 16 | 17 | * 0: super-primary, reserved for special emphasis 18 | * 1: primary, most important under normal situations 19 | * 2: secondary, next most important under normal situations 20 | * 3: tertiary, next most important under normal situations 21 | 22 | Throughout JupyterLab, we are mostly following principles from Google's 23 | Material Design when selecting colors. We are not, however, following 24 | all of MD as it is not optimized for dense, information rich UIs. 25 | */ 26 | 27 | :root { 28 | /* Elevation 29 | * 30 | * We style box-shadows using Material Design's idea of elevation. These particular numbers are taken from here: 31 | * 32 | * https://github.com/material-components/material-components-web 33 | * https://material-components-web.appspot.com/elevation.html 34 | */ 35 | 36 | --jp-shadow-base-lightness: 0; 37 | --jp-shadow-umbra-color: rgba( 38 | var(--jp-shadow-base-lightness), 39 | var(--jp-shadow-base-lightness), 40 | var(--jp-shadow-base-lightness), 41 | 0.2 42 | ); 43 | --jp-shadow-penumbra-color: rgba( 44 | var(--jp-shadow-base-lightness), 45 | var(--jp-shadow-base-lightness), 46 | var(--jp-shadow-base-lightness), 47 | 0.14 48 | ); 49 | --jp-shadow-ambient-color: rgba( 50 | var(--jp-shadow-base-lightness), 51 | var(--jp-shadow-base-lightness), 52 | var(--jp-shadow-base-lightness), 53 | 0.12 54 | ); 55 | --jp-elevation-z0: none; 56 | --jp-elevation-z1: 57 | 0 2px 1px -1px var(--jp-shadow-umbra-color), 58 | 0 1px 1px 0 var(--jp-shadow-penumbra-color), 59 | 0 1px 3px 0 var(--jp-shadow-ambient-color); 60 | --jp-elevation-z2: 61 | 0 3px 1px -2px var(--jp-shadow-umbra-color), 62 | 0 2px 2px 0 var(--jp-shadow-penumbra-color), 63 | 0 1px 5px 0 var(--jp-shadow-ambient-color); 64 | --jp-elevation-z4: 65 | 0 2px 4px -1px var(--jp-shadow-umbra-color), 66 | 0 4px 5px 0 var(--jp-shadow-penumbra-color), 67 | 0 1px 10px 0 var(--jp-shadow-ambient-color); 68 | --jp-elevation-z6: 69 | 0 3px 5px -1px var(--jp-shadow-umbra-color), 70 | 0 6px 10px 0 var(--jp-shadow-penumbra-color), 71 | 0 1px 18px 0 var(--jp-shadow-ambient-color); 72 | --jp-elevation-z8: 73 | 0 5px 5px -3px var(--jp-shadow-umbra-color), 74 | 0 8px 10px 1px var(--jp-shadow-penumbra-color), 75 | 0 3px 14px 2px var(--jp-shadow-ambient-color); 76 | --jp-elevation-z12: 77 | 0 7px 8px -4px var(--jp-shadow-umbra-color), 78 | 0 12px 17px 2px var(--jp-shadow-penumbra-color), 79 | 0 5px 22px 4px var(--jp-shadow-ambient-color); 80 | --jp-elevation-z16: 81 | 0 8px 10px -5px var(--jp-shadow-umbra-color), 82 | 0 16px 24px 2px var(--jp-shadow-penumbra-color), 83 | 0 6px 30px 5px var(--jp-shadow-ambient-color); 84 | --jp-elevation-z20: 85 | 0 10px 13px -6px var(--jp-shadow-umbra-color), 86 | 0 20px 31px 3px var(--jp-shadow-penumbra-color), 87 | 0 8px 38px 7px var(--jp-shadow-ambient-color); 88 | --jp-elevation-z24: 89 | 0 11px 15px -7px var(--jp-shadow-umbra-color), 90 | 0 24px 38px 3px var(--jp-shadow-penumbra-color), 91 | 0 9px 46px 8px var(--jp-shadow-ambient-color); 92 | 93 | /* Borders 94 | * 95 | * The following variables, specify the visual styling of borders in JupyterLab. 96 | */ 97 | 98 | --jp-border-width: 1px; 99 | --jp-border-color0: var(--md-grey-400); 100 | --jp-border-color1: var(--md-grey-400); 101 | --jp-border-color2: var(--md-grey-300); 102 | --jp-border-color3: var(--md-grey-200); 103 | --jp-border-radius: 2px; 104 | 105 | /* UI Fonts 106 | * 107 | * The UI font CSS variables are used for the typography all of the JupyterLab 108 | * user interface elements that are not directly user generated content. 109 | * 110 | * The font sizing here is done assuming that the body font size of --jp-ui-font-size1 111 | * is applied to a parent element. When children elements, such as headings, are sized 112 | * in em all things will be computed relative to that body size. 113 | */ 114 | 115 | --jp-ui-font-scale-factor: 1.2; 116 | --jp-ui-font-size0: 0.8333em; 117 | --jp-ui-font-size1: 13px; /* Base font size */ 118 | --jp-ui-font-size2: 1.2em; 119 | --jp-ui-font-size3: 1.44em; 120 | --jp-ui-font-family: 121 | -apple-system, blinkmacsystemfont, 'Segoe UI', helvetica, arial, sans-serif, 122 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 123 | 124 | /* 125 | * Use these font colors against the corresponding main layout colors. 126 | * In a light theme, these go from dark to light. 127 | */ 128 | 129 | /* Defaults use Material Design specification */ 130 | --jp-ui-font-color0: rgba(0, 0, 0, 1); 131 | --jp-ui-font-color1: rgba(0, 0, 0, 0.87); 132 | --jp-ui-font-color2: rgba(0, 0, 0, 0.54); 133 | --jp-ui-font-color3: rgba(0, 0, 0, 0.38); 134 | 135 | /* 136 | * Use these against the brand/accent/warn/error colors. 137 | * These will typically go from light to darker, in both a dark and light theme. 138 | */ 139 | 140 | --jp-ui-inverse-font-color0: rgba(255, 255, 255, 1); 141 | --jp-ui-inverse-font-color1: rgba(255, 255, 255, 1); 142 | --jp-ui-inverse-font-color2: rgba(255, 255, 255, 0.7); 143 | --jp-ui-inverse-font-color3: rgba(255, 255, 255, 0.5); 144 | 145 | /* Content Fonts 146 | * 147 | * Content font variables are used for typography of user generated content. 148 | * 149 | * The font sizing here is done assuming that the body font size of --jp-content-font-size1 150 | * is applied to a parent element. When children elements, such as headings, are sized 151 | * in em all things will be computed relative to that body size. 152 | */ 153 | 154 | --jp-content-line-height: 1.6; 155 | --jp-content-font-scale-factor: 1.2; 156 | --jp-content-font-size0: 0.8333em; 157 | --jp-content-font-size1: 14px; /* Base font size */ 158 | --jp-content-font-size2: 1.2em; 159 | --jp-content-font-size3: 1.44em; 160 | --jp-content-font-size4: 1.728em; 161 | --jp-content-font-size5: 2.0736em; 162 | 163 | /* This gives a magnification of about 125% in presentation mode over normal. */ 164 | --jp-content-presentation-font-size1: 17px; 165 | --jp-content-heading-line-height: 1; 166 | --jp-content-heading-margin-top: 1.2em; 167 | --jp-content-heading-margin-bottom: 0.8em; 168 | --jp-content-heading-font-weight: 500; 169 | 170 | /* Defaults use Material Design specification */ 171 | --jp-content-font-color0: rgba(0, 0, 0, 1); 172 | --jp-content-font-color1: rgba(0, 0, 0, 0.87); 173 | --jp-content-font-color2: rgba(0, 0, 0, 0.54); 174 | --jp-content-font-color3: rgba(0, 0, 0, 0.38); 175 | --jp-content-link-color: var(--md-blue-700); 176 | --jp-content-font-family: 177 | -apple-system, blinkmacsystemfont, 'Segoe UI', helvetica, arial, sans-serif, 178 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 179 | 180 | /* 181 | * Code Fonts 182 | * 183 | * Code font variables are used for typography of code and other monospaces content. 184 | */ 185 | 186 | --jp-code-font-size: 13px; 187 | --jp-code-line-height: 1.3077; /* 17px for 13px base */ 188 | --jp-code-padding: 0.385em; /* 5px for 13px base */ 189 | --jp-code-font-family-default: menlo, consolas, 'DejaVu Sans Mono', monospace; 190 | --jp-code-font-family: var(--jp-code-font-family-default); 191 | 192 | /* This gives a magnification of about 125% in presentation mode over normal. */ 193 | --jp-code-presentation-font-size: 16px; 194 | 195 | /* may need to tweak cursor width if you change font size */ 196 | --jp-code-cursor-width0: 1.4px; 197 | --jp-code-cursor-width1: 2px; 198 | --jp-code-cursor-width2: 4px; 199 | 200 | /* Layout 201 | * 202 | * The following are the main layout colors use in JupyterLab. In a light 203 | * theme these would go from light to dark. 204 | */ 205 | 206 | --jp-layout-color0: white; 207 | --jp-layout-color1: white; 208 | --jp-layout-color2: var(--md-grey-200); 209 | --jp-layout-color3: var(--md-grey-400); 210 | --jp-layout-color4: var(--md-grey-600); 211 | 212 | /* Inverse Layout 213 | * 214 | * The following are the inverse layout colors use in JupyterLab. In a light 215 | * theme these would go from dark to light. 216 | */ 217 | 218 | --jp-inverse-layout-color0: #111; 219 | --jp-inverse-layout-color1: var(--md-grey-900); 220 | --jp-inverse-layout-color2: var(--md-grey-800); 221 | --jp-inverse-layout-color3: var(--md-grey-700); 222 | --jp-inverse-layout-color4: var(--md-grey-600); 223 | 224 | /* Brand/accent */ 225 | 226 | --jp-brand-color0: #ec0c4b; 227 | --jp-brand-color1: #ed225d; 228 | --jp-brand-color2: #ee376b; 229 | --jp-brand-color3: #ee3b6e; 230 | --jp-accent-color0: var(--md-green-700); 231 | --jp-accent-color1: var(--md-green-500); 232 | --jp-accent-color2: var(--md-green-300); 233 | --jp-accent-color3: var(--md-green-100); 234 | 235 | /* State colors (warn, error, success, info) */ 236 | 237 | --jp-warn-color0: var(--md-orange-700); 238 | --jp-warn-color1: var(--md-orange-500); 239 | --jp-warn-color2: var(--md-orange-300); 240 | --jp-warn-color3: var(--md-orange-100); 241 | --jp-error-color0: var(--md-red-700); 242 | --jp-error-color1: var(--md-red-500); 243 | --jp-error-color2: var(--md-red-300); 244 | --jp-error-color3: var(--md-red-100); 245 | --jp-success-color0: var(--md-green-700); 246 | --jp-success-color1: var(--md-green-500); 247 | --jp-success-color2: var(--md-green-300); 248 | --jp-success-color3: var(--md-green-100); 249 | --jp-info-color0: var(--md-cyan-700); 250 | --jp-info-color1: var(--md-cyan-500); 251 | --jp-info-color2: var(--md-cyan-300); 252 | --jp-info-color3: var(--md-cyan-100); 253 | 254 | /* Cell specific styles */ 255 | 256 | --jp-cell-padding: 5px; 257 | --jp-cell-collapser-width: 8px; 258 | --jp-cell-collapser-min-height: 20px; 259 | --jp-cell-collapser-not-active-hover-opacity: 0.6; 260 | --jp-cell-editor-background: var(--md-grey-100); 261 | --jp-cell-editor-border-color: var(--md-grey-300); 262 | --jp-cell-editor-box-shadow: inset 0 0 2px var(--md-blue-300); 263 | --jp-cell-editor-active-background: var(--jp-layout-color0); 264 | --jp-cell-editor-active-border-color: var(--jp-brand-color1); 265 | --jp-cell-prompt-width: 64px; 266 | --jp-cell-prompt-font-family: 'Source Code Pro', monospace; 267 | --jp-cell-prompt-letter-spacing: 0; 268 | --jp-cell-prompt-opacity: 1; 269 | --jp-cell-prompt-not-active-opacity: 0.5; 270 | --jp-cell-prompt-not-active-font-color: var(--md-grey-700); 271 | 272 | /* A custom blend of MD grey and blue 600 273 | * See https://meyerweb.com/eric/tools/color-blend/#546E7A:1E88E5:5:hex */ 274 | --jp-cell-inprompt-font-color: #307fc1; 275 | 276 | /* A custom blend of MD grey and orange 600 277 | * https://meyerweb.com/eric/tools/color-blend/#546E7A:F4511E:5:hex */ 278 | --jp-cell-outprompt-font-color: #bf5b3d; 279 | 280 | /* Notebook specific styles */ 281 | 282 | --jp-notebook-padding: 10px; 283 | --jp-notebook-select-background: var(--jp-layout-color1); 284 | --jp-notebook-multiselected-color: var(--md-blue-50); 285 | 286 | /* The scroll padding is calculated to fill enough space at the bottom of the 287 | notebook to show one single-line cell (with appropriate padding) at the top 288 | when the notebook is scrolled all the way to the bottom. We also subtract one 289 | pixel so that no scrollbar appears if we have just one single-line cell in the 290 | notebook. This padding is to enable a 'scroll past end' feature in a notebook. 291 | */ 292 | --jp-notebook-scroll-padding: calc( 293 | 100% - var(--jp-code-font-size) * var(--jp-code-line-height) - 294 | var(--jp-code-padding) - var(--jp-cell-padding) - 1px 295 | ); 296 | 297 | /* Rendermime styles */ 298 | 299 | --jp-rendermime-error-background: #fdd; 300 | --jp-rendermime-table-row-background: var(--md-grey-100); 301 | --jp-rendermime-table-row-hover-background: var(--md-light-blue-50); 302 | 303 | /* Dialog specific styles */ 304 | 305 | --jp-dialog-background: rgba(0, 0, 0, 0.25); 306 | 307 | /* Console specific styles */ 308 | 309 | --jp-console-padding: 10px; 310 | 311 | /* Toolbar specific styles */ 312 | 313 | --jp-toolbar-border-color: var(--jp-border-color1); 314 | --jp-toolbar-micro-height: 8px; 315 | --jp-toolbar-background: var(--jp-layout-color1); 316 | --jp-toolbar-box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.24); 317 | --jp-toolbar-header-margin: 4px 4px 0 4px; 318 | --jp-toolbar-active-background: var(--md-grey-300); 319 | 320 | /* Statusbar specific styles */ 321 | 322 | --jp-statusbar-height: 24px; 323 | 324 | /* Input field styles */ 325 | 326 | --jp-input-box-shadow: inset 0 0 2px var(--md-blue-300); 327 | --jp-input-active-background: var(--jp-layout-color1); 328 | --jp-input-hover-background: var(--jp-layout-color1); 329 | --jp-input-background: var(--md-grey-100); 330 | --jp-input-border-color: var(--jp-border-color1); 331 | --jp-input-active-border-color: var(--jp-brand-color1); 332 | 333 | /* General editor styles */ 334 | 335 | --jp-editor-selected-background: #d9d9d9; 336 | --jp-editor-selected-focused-background: #d7d4f0; 337 | --jp-editor-cursor-color: var(--jp-ui-font-color0); 338 | 339 | /* Code mirror specific styles */ 340 | 341 | --jp-mirror-editor-keyword-color: #008000; 342 | --jp-mirror-editor-atom-color: #88f; 343 | --jp-mirror-editor-number-color: #080; 344 | --jp-mirror-editor-def-color: #00f; 345 | --jp-mirror-editor-variable-color: var(--md-grey-900); 346 | --jp-mirror-editor-variable-2-color: #05a; 347 | --jp-mirror-editor-variable-3-color: #085; 348 | --jp-mirror-editor-punctuation-color: #05a; 349 | --jp-mirror-editor-property-color: #05a; 350 | --jp-mirror-editor-operator-color: #a2f; 351 | --jp-mirror-editor-comment-color: #408080; 352 | --jp-mirror-editor-string-color: #ba2121; 353 | --jp-mirror-editor-string-2-color: #708; 354 | --jp-mirror-editor-meta-color: #a2f; 355 | --jp-mirror-editor-qualifier-color: #555; 356 | --jp-mirror-editor-builtin-color: #008000; 357 | --jp-mirror-editor-bracket-color: #997; 358 | --jp-mirror-editor-tag-color: #170; 359 | --jp-mirror-editor-attribute-color: #00c; 360 | --jp-mirror-editor-header-color: blue; 361 | --jp-mirror-editor-quote-color: #090; 362 | --jp-mirror-editor-link-color: #00c; 363 | --jp-mirror-editor-error-color: #f00; 364 | --jp-mirror-editor-hr-color: #999; 365 | 366 | /* User colors */ 367 | 368 | --jp-collaborator-color1: #ad4a00; 369 | --jp-collaborator-color2: #7b6a00; 370 | --jp-collaborator-color3: #007e00; 371 | --jp-collaborator-color4: #008772; 372 | --jp-collaborator-color5: #0079b9; 373 | --jp-collaborator-color6: #8b45c6; 374 | --jp-collaborator-color7: #be208b; 375 | 376 | /* File or activity icons and switch semantic variables */ 377 | 378 | --jp-jupyter-icon-color: var(--md-orange-900); 379 | --jp-notebook-icon-color: var(--md-orange-700); 380 | --jp-json-icon-color: var(--md-orange-700); 381 | --jp-console-icon-background-color: var(--md-blue-700); 382 | --jp-console-icon-color: white; 383 | --jp-terminal-icon-background-color: var(--md-grey-200); 384 | --jp-terminal-icon-color: var(--md-grey-800); 385 | --jp-text-editor-icon-color: var(--md-grey-200); 386 | --jp-inspector-icon-color: var(--md-grey-200); 387 | --jp-switch-color: var(--md-grey-400); 388 | --jp-switch-true-position-color: var(--md-orange-700); 389 | --jp-switch-cursor-color: rgba(0, 0, 0, 0.8); 390 | 391 | /* Vega extension styles */ 392 | 393 | --jp-vega-background: white; 394 | 395 | /* Sidebar-related styles */ 396 | 397 | --jp-sidebar-min-width: 180px; 398 | } 399 | -------------------------------------------------------------------------------- /template/tsconfig.json.jinja: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "incremental": true, 8 | "jsx": "react", 9 | "lib": ["DOM", "ES2018", "ES2020.Intl"], 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "noEmitOnError": true, 13 | "noImplicitAny": true, 14 | "noUnusedLocals": true, 15 | "preserveWatchOutput": true, 16 | "resolveJsonModule": true, 17 | "outDir": "lib", 18 | "rootDir": "src", 19 | "strict": true, 20 | "strictNullChecks": true, 21 | "target": "ES2018"{% if test %}, 22 | "types": ["jest"]{% endif %} 23 | }, 24 | "include": ["src/*"] 25 | } 26 | -------------------------------------------------------------------------------- /template/{% if has_binder %}binder{% endif %}/environment.yml.jinja: -------------------------------------------------------------------------------- 1 | # a mybinder.org-ready environment for demoing {{ python_name }} 2 | # this environment may also be used locally on Linux/MacOS/Windows, e.g. 3 | # 4 | # conda env update --file binder/environment.yml 5 | # conda activate {{ python_name | replace('_', '-') }}-demo 6 | # 7 | name: {{ python_name | replace('_', '-') }}-demo 8 | 9 | channels: 10 | - conda-forge 11 | 12 | dependencies: 13 | # runtime dependencies 14 | - python >=3.10,<3.11.0a0 15 | - jupyterlab >=4.0.0,<5 16 | # labextension build dependencies 17 | - nodejs >=18,<19 18 | - pip 19 | - wheel 20 | # additional packages for demos 21 | # - ipywidgets 22 | -------------------------------------------------------------------------------- /template/{% if has_binder %}binder{% endif %}/postBuild.jinja: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ perform a development install of {{ python_name }} 3 | 4 | On Binder, this will run _after_ the environment has been fully created from 5 | the environment.yml in this directory. 6 | 7 | This script should also run locally on Linux/MacOS/Windows: 8 | 9 | python3 binder/postBuild 10 | """ 11 | import subprocess 12 | import sys 13 | from pathlib import Path 14 | 15 | 16 | ROOT = Path.cwd() 17 | 18 | def _(*args, **kwargs): 19 | """ Run a command, echoing the args 20 | 21 | fails hard if something goes wrong 22 | """ 23 | print("\n\t", " ".join(args), "\n") 24 | return_code = subprocess.call(args, **kwargs) 25 | if return_code != 0: 26 | print("\nERROR", return_code, " ".join(args)) 27 | sys.exit(return_code) 28 | 29 | # verify the environment is self-consistent before even starting 30 | _(sys.executable, "-m", "pip", "check") 31 | 32 | # install the labextension 33 | _(sys.executable, "-m", "pip", "install", "-e", ".") 34 | _(sys.executable, "-m", "jupyter", "labextension", "develop", "--overwrite", "."){% if kind.lower() == 'server' %} 35 | _( 36 | sys.executable, 37 | "-m", 38 | "jupyter", 39 | "server", 40 | "extension", 41 | "enable", 42 | "{{ python_name }}", 43 | ){% endif %} 44 | 45 | # verify the environment the extension didn't break anything 46 | _(sys.executable, "-m", "pip", "check") 47 | 48 | # list the extensions 49 | _("jupyter", "server", "extension", "list") 50 | 51 | # initially list installed extensions to determine if there are any surprises 52 | _("jupyter", "labextension", "list") 53 | 54 | 55 | print("JupyterLab with {{ python_name }} is ready to run with:\n") 56 | print("\tjupyter lab\n") 57 | -------------------------------------------------------------------------------- /template/{% if has_settings %}schema{% endif %}/plugin.json.jinja: -------------------------------------------------------------------------------- 1 | { 2 | "jupyter.lab.shortcuts": [], 3 | "title": "{{ labextension_name }}", 4 | "description": "{{ labextension_name }} settings.", 5 | "type": "object", 6 | "properties": {}, 7 | "additionalProperties": false 8 | } 9 | -------------------------------------------------------------------------------- /template/{% if kind == 'server' %}jupyter-config{% endif %}/server-config/{{python_name}}.json.jinja: -------------------------------------------------------------------------------- 1 | { 2 | "ServerApp": { 3 | "jpserver_extensions": { 4 | "{{ python_name }}": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /template/{% if test %}babel.config.js{% endif %}: -------------------------------------------------------------------------------- 1 | module.exports = require('@jupyterlab/testutils/lib/babel.config'); 2 | -------------------------------------------------------------------------------- /template/{% if test %}jest.config.js{% endif %}: -------------------------------------------------------------------------------- 1 | const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); 2 | 3 | const esModules = [ 4 | '@codemirror', 5 | '@jupyter/ydoc', 6 | '@jupyterlab/', 7 | 'lib0', 8 | 'nanoid', 9 | 'vscode-ws-jsonrpc', 10 | 'y-protocols', 11 | 'y-websocket', 12 | 'yjs' 13 | ].join('|'); 14 | 15 | const baseConfig = jestJupyterLab(__dirname); 16 | 17 | module.exports = { 18 | ...baseConfig, 19 | automock: false, 20 | collectCoverageFrom: [ 21 | 'src/**/*.{ts,tsx}', 22 | '!src/**/*.d.ts', 23 | '!src/**/.ipynb_checkpoints/*' 24 | ], 25 | coverageReporters: ['lcov', 'text'], 26 | testRegex: 'src/.*/.*.spec.ts[x]?$', 27 | transformIgnorePatterns: [`/node_modules/(?!${esModules}).+`] 28 | }; 29 | -------------------------------------------------------------------------------- /template/{% if test %}tsconfig.test.json{% endif %}: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /template/{% if test %}ui-tests{% endif %}/README.md: -------------------------------------------------------------------------------- 1 | # Integration Testing 2 | 3 | This folder contains the integration tests of the extension. 4 | 5 | They are defined using [Playwright](https://playwright.dev/docs/intro) test runner 6 | and [Galata](https://github.com/jupyterlab/jupyterlab/tree/main/galata) helper. 7 | 8 | The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). 9 | 10 | The JupyterLab server configuration to use for the integration test is defined 11 | in [jupyter_server_test_config.py](./jupyter_server_test_config.py). 12 | 13 | The default configuration will produce video for failing tests and an HTML report. 14 | 15 | > There is a UI mode that you may like; see [that video](https://www.youtube.com/watch?v=jF0yA-JLQW0). 16 | 17 | ## Run the tests 18 | 19 | > All commands are assumed to be executed from the root directory 20 | 21 | To run the tests, you need to: 22 | 23 | 1. Compile the extension: 24 | 25 | ```sh 26 | jlpm install 27 | jlpm build:prod 28 | ``` 29 | 30 | > Check the extension is installed in JupyterLab. 31 | 32 | 2. Install test dependencies (needed only once): 33 | 34 | ```sh 35 | cd ./ui-tests 36 | jlpm install 37 | jlpm playwright install 38 | cd .. 39 | ``` 40 | 41 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: 42 | 43 | ```sh 44 | cd ./ui-tests 45 | jlpm playwright test 46 | ``` 47 | 48 | Test results will be shown in the terminal. In case of any test failures, the test report 49 | will be opened in your browser at the end of the tests execution; see 50 | [Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) 51 | for configuring that behavior. 52 | 53 | ## Update the tests snapshots 54 | 55 | > All commands are assumed to be executed from the root directory 56 | 57 | If you are comparing snapshots to validate your tests, you may need to update 58 | the reference snapshots stored in the repository. To do that, you need to: 59 | 60 | 1. Compile the extension: 61 | 62 | ```sh 63 | jlpm install 64 | jlpm build:prod 65 | ``` 66 | 67 | > Check the extension is installed in JupyterLab. 68 | 69 | 2. Install test dependencies (needed only once): 70 | 71 | ```sh 72 | cd ./ui-tests 73 | jlpm install 74 | jlpm playwright install 75 | cd .. 76 | ``` 77 | 78 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) command: 79 | 80 | ```sh 81 | cd ./ui-tests 82 | jlpm playwright test -u 83 | ``` 84 | 85 | > Some discrepancy may occurs between the snapshots generated on your computer and 86 | > the one generated on the CI. To ease updating the snapshots on a PR, you can 87 | > type `please update playwright snapshots` to trigger the update by a bot on the CI. 88 | > Once the bot has computed new snapshots, it will commit them to the PR branch. 89 | 90 | ## Create tests 91 | 92 | > All commands are assumed to be executed from the root directory 93 | 94 | To create tests, the easiest way is to use the code generator tool of playwright: 95 | 96 | 1. Compile the extension: 97 | 98 | ```sh 99 | jlpm install 100 | jlpm build:prod 101 | ``` 102 | 103 | > Check the extension is installed in JupyterLab. 104 | 105 | 2. Install test dependencies (needed only once): 106 | 107 | ```sh 108 | cd ./ui-tests 109 | jlpm install 110 | jlpm playwright install 111 | cd .. 112 | ``` 113 | 114 | 3. Start the server: 115 | 116 | ```sh 117 | cd ./ui-tests 118 | jlpm start 119 | ``` 120 | 121 | 4. Execute the [Playwright code generator](https://playwright.dev/docs/codegen) in **another terminal**: 122 | 123 | ```sh 124 | cd ./ui-tests 125 | jlpm playwright codegen localhost:8888 126 | ``` 127 | 128 | ## Debug tests 129 | 130 | > All commands are assumed to be executed from the root directory 131 | 132 | To debug tests, a good way is to use the inspector tool of playwright: 133 | 134 | 1. Compile the extension: 135 | 136 | ```sh 137 | jlpm install 138 | jlpm build:prod 139 | ``` 140 | 141 | > Check the extension is installed in JupyterLab. 142 | 143 | 2. Install test dependencies (needed only once): 144 | 145 | ```sh 146 | cd ./ui-tests 147 | jlpm install 148 | jlpm playwright install 149 | cd .. 150 | ``` 151 | 152 | 3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): 153 | 154 | ```sh 155 | cd ./ui-tests 156 | jlpm playwright test --debug 157 | ``` 158 | 159 | ## Upgrade Playwright and the browsers 160 | 161 | To update the web browser versions, you must update the package `@playwright/test`: 162 | 163 | ```sh 164 | cd ./ui-tests 165 | jlpm up "@playwright/test" 166 | jlpm playwright install 167 | ``` 168 | -------------------------------------------------------------------------------- /template/{% if test %}ui-tests{% endif %}/jupyter_server_test_config.py: -------------------------------------------------------------------------------- 1 | """Server configuration for integration tests. 2 | 3 | !! Never use this configuration in production because it 4 | opens the server to the world and provide access to JupyterLab 5 | JavaScript objects through the global window variable. 6 | """ 7 | from jupyterlab.galata import configure_jupyter_server 8 | 9 | configure_jupyter_server(c) 10 | 11 | # Uncomment to set server log level to debug level 12 | # c.ServerApp.log_level = "DEBUG" 13 | -------------------------------------------------------------------------------- /template/{% if test %}ui-tests{% endif %}/package.json.jinja: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ labextension_name }}-ui-tests", 3 | "version": "1.0.0", 4 | "description": "JupyterLab {{ labextension_name }} Integration Tests", 5 | "private": true, 6 | "scripts": { 7 | "start": "jupyter lab --config jupyter_server_test_config.py", 8 | "test": "jlpm playwright test", 9 | "test:update": "jlpm playwright test --update-snapshots" 10 | }, 11 | "devDependencies": { 12 | "@jupyterlab/galata": "^5.0.5", 13 | "@playwright/test": "^1.37.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /template/{% if test %}ui-tests{% endif %}/playwright.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration for Playwright using default from @jupyterlab/galata 3 | */ 4 | const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); 5 | 6 | module.exports = { 7 | ...baseConfig, 8 | webServer: { 9 | command: 'jlpm start', 10 | url: 'http://localhost:8888/lab', 11 | timeout: 120 * 1000, 12 | reuseExistingServer: !process.env.CI 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /template/{% if test %}ui-tests{% endif %}/tests/{{python_name}}.spec.ts.jinja: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jupyterlab/galata'; 2 | 3 | {% if kind != 'mimerenderer' %}/** 4 | * Don't load JupyterLab webpage before running the tests. 5 | * This is required to ensure we capture all log messages. 6 | */ 7 | test.use({ autoGoto: false }); 8 | 9 | test('should emit an activation console message', async ({ page }) => { 10 | const logs: string[] = []; 11 | 12 | page.on('console', message => { 13 | logs.push(message.text()); 14 | }); 15 | 16 | await page.goto(); 17 | 18 | expect( 19 | logs.filter(s => s === 'JupyterLab extension {{ labextension_name }} is activated!') 20 | ).toHaveLength(1); 21 | }); 22 | {% else %}test('should display {{ mimetype_name }} data file', async ({ page }) => { 23 | const filename = 'test{{ file_extension }}'; 24 | await page.menu.clickMenuItem('File>New>Text File'); 25 | 26 | // Set MIME type content in fill 27 | await page.getByRole('main').getByRole('textbox').fill(''); 28 | 29 | await page.menu.clickMenuItem('File>Save Text'); 30 | 31 | await page.locator('.jp-Dialog').getByRole('textbox').fill(filename); 32 | 33 | await page.getByRole('button', { name: 'Rename' }).click(); 34 | await page.waitForTimeout(200); 35 | 36 | // Close file opened as editor 37 | await page.activity.closePanel('test.my_type'); 38 | 39 | await page.filebrowser.open(filename); 40 | 41 | const view = page.getByRole('main').locator('.mimerenderer-{{ mimetype_name }}'); 42 | 43 | expect(await view.screenshot()).toMatchSnapshot('{{ mimetype_name }}-file.png'); 44 | }); 45 | 46 | test('should display notebook {{ mimetype_name }} output', async ({ page }) => { 47 | await page.menu.clickMenuItem('File>New>Notebook'); 48 | 49 | await page.getByRole('button', { name: 'Select' }).click(); 50 | 51 | await page.notebook.setCell( 52 | 0, 53 | 'code', 54 | `from IPython.display import display 55 | # Example of MIME type content 56 | output = { 57 | "{{ mimetype }}": "" 58 | } 59 | 60 | display(output, raw=True)` 61 | ); 62 | 63 | await page.notebook.run(); 64 | 65 | const outputs = page 66 | .getByRole('main') 67 | .locator('.mimerenderer-{{ mimetype_name }}.jp-OutputArea-output'); 68 | 69 | await expect(outputs).toHaveCount(1); 70 | }); 71 | {% endif %} -------------------------------------------------------------------------------- /template/{% if test %}ui-tests{% endif %}/yarn.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/extension-template/c7390994289898a95ffd3e5a2e9da36da2ecf7fb/template/{% if test %}ui-tests{% endif %}/yarn.lock -------------------------------------------------------------------------------- /template/{% if test and kind == 'server' %}conftest.py{% endif %}.jinja: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest_plugins = ("pytest_jupyter.jupyter_server", ) 4 | 5 | 6 | @pytest.fixture 7 | def jp_server_config(jp_server_config): 8 | return {"ServerApp": {"jpserver_extensions": {"{{ python_name }}": True}}} 9 | -------------------------------------------------------------------------------- /template/{{_copier_conf.answers_file}}.jinja: -------------------------------------------------------------------------------- 1 | # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY 2 | {{_copier_answers|to_nice_yaml}} 3 | -------------------------------------------------------------------------------- /template/{{python_name}}/__init__.py.jinja: -------------------------------------------------------------------------------- 1 | try: 2 | from ._version import __version__ 3 | except ImportError: 4 | # Fallback when using the package in dev mode without installing 5 | # in editable mode with pip. It is highly recommended to install 6 | # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs 7 | import warnings 8 | warnings.warn("Importing '{{ python_name }}' outside a proper installation.") 9 | __version__ = "dev"{% if kind.lower() == 'server' %} 10 | from .handlers import setup_handlers{% endif %} 11 | 12 | 13 | def _jupyter_labextension_paths(): 14 | return [{ 15 | "src": "labextension", 16 | "dest": "{{ labextension_name }}" 17 | }]{% if kind.lower() == 'server' %} 18 | 19 | 20 | def _jupyter_server_extension_points(): 21 | return [{ 22 | "module": "{{ python_name }}" 23 | }] 24 | 25 | 26 | def _load_jupyter_server_extension(server_app): 27 | """Registers the API handler to receive HTTP requests from the frontend extension. 28 | 29 | Parameters 30 | ---------- 31 | server_app: jupyterlab.labapp.LabApp 32 | JupyterLab application instance 33 | """ 34 | setup_handlers(server_app.web_app) 35 | name = "{{ python_name }}" 36 | server_app.log.info(f"Registered {name} server extension"){% endif %} 37 | -------------------------------------------------------------------------------- /template/{{python_name}}/{% if kind == 'server' %}handlers.py{% endif %}.jinja: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from jupyter_server.base.handlers import APIHandler 4 | from jupyter_server.utils import url_path_join 5 | import tornado 6 | 7 | class RouteHandler(APIHandler): 8 | # The following decorator should be present on all verb methods (head, get, post, 9 | # patch, put, delete, options) to ensure only authorized user can request the 10 | # Jupyter server 11 | @tornado.web.authenticated 12 | def get(self): 13 | self.finish(json.dumps({ 14 | "data": "This is /{{ python_name | replace('_', '-') }}/get-example endpoint!" 15 | })) 16 | 17 | 18 | def setup_handlers(web_app): 19 | host_pattern = ".*$" 20 | 21 | base_url = web_app.settings["base_url"] 22 | route_pattern = url_path_join(base_url, "{{ python_name | replace('_', '-') }}", "get-example") 23 | handlers = [(route_pattern, RouteHandler)] 24 | web_app.add_handlers(host_pattern, handlers) 25 | -------------------------------------------------------------------------------- /template/{{python_name}}/{% if test and kind == 'server' %}tests{% endif %}/__init__.py.jinja: -------------------------------------------------------------------------------- 1 | """Python unit tests for {{ python_name }}.""" 2 | -------------------------------------------------------------------------------- /template/{{python_name}}/{% if test and kind == 'server' %}tests{% endif %}/test_handlers.py.jinja: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | async def test_get_example(jp_fetch): 5 | # When 6 | response = await jp_fetch("{{ python_name | replace('_', '-') }}", "get-example") 7 | 8 | # Then 9 | assert response.code == 200 10 | payload = json.loads(response.body) 11 | assert payload == { 12 | "data": "This is /{{ python_name | replace('_', '-') }}/get-example endpoint!" 13 | } --------------------------------------------------------------------------------