├── .github └── workflows │ ├── alignment.yml │ ├── check-tests.yml │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── cookiecutter.json ├── hooks └── post_gen_project.py └── {{cookiecutter.python_name}} ├── .github └── workflows │ ├── binder-on-pr.yml │ ├── build.yml │ ├── check-release.yml │ ├── enforce-label.yml │ ├── prep-release.yml │ ├── publish-release.yml │ └── update-integration-tests.yml ├── .gitignore ├── .prettierignore ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASE.md ├── babel.config.js ├── binder ├── environment.yml └── postBuild ├── conftest.py ├── install.json ├── jest.config.js ├── jupyter-config ├── nb-config │ └── {{cookiecutter.python_name}}.json └── server-config │ └── {{cookiecutter.python_name}}.json ├── package.json ├── pyproject.toml ├── schema └── plugin.json ├── setup.py ├── src ├── __tests__ │ └── {{cookiecutter.python_name}}.spec.ts ├── handler.ts └── index.ts ├── style ├── base.css ├── index.css ├── index.js └── variables.css ├── tsconfig.json ├── tsconfig.test.json ├── ui-tests ├── README.md ├── jupyter_server_test_config.py ├── package.json ├── playwright.config.js ├── tests │ └── {{cookiecutter.python_name}}.spec.ts └── yarn.lock └── {{cookiecutter.python_name}} ├── __init__.py ├── handlers.py └── tests ├── __init__.py └── test_handlers.py /.github/workflows/alignment.yml: -------------------------------------------------------------------------------- 1 | name: Check alignment with copier template 2 | 3 | on: 4 | push: 5 | branches: ["*.0"] 6 | pull_request: 7 | branches: ["*"] 8 | schedule: 9 | - cron: "0 0 * * *" 10 | 11 | jobs: 12 | template-alignment: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | kind: ["frontend", "server", "theme"] 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | - name: Install Python 23 | uses: actions/setup-python@v4 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install cookiecutter copier jinja2-time 27 | 28 | - name: Create from template 29 | env: 30 | KIND: ${{ matrix.kind }} 31 | run: | 32 | set -eux 33 | # Create template from cookiecutter 34 | python -c "from cookiecutter.main import cookiecutter; import json, os; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']=os.getenv('KIND'); d['labextension_name']='myextension'; cookiecutter('.', extra_context=d, no_input=True)" 35 | 36 | # Create template from copier 37 | mkdir copierext 38 | pushd copierext 39 | copier -l -d kind=${KIND} -d labextension_name=myextension -d author_name="My Name" -d author_email="me@test.com" -d repository="https://github.com/github_username/myextension" gh:jupyterlab/extension-template . 40 | rm .copier-answers.yml 41 | popd 42 | 43 | diff -r myextension copierext 44 | -------------------------------------------------------------------------------- /.github/workflows/check-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests validation 2 | 3 | on: 4 | push: 5 | branches: ["*.0"] 6 | pull_request: 7 | branches: ["*"] 8 | schedule: 9 | - cron: "0 0 * * *" 10 | 11 | jobs: 12 | tests: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Base Setup 19 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 20 | 21 | - name: Install dependencies 22 | run: python -m pip install -U cookiecutter "jupyterlab>=4.0.0,<5" 23 | 24 | - name: Create the extension 25 | run: | 26 | set -eux 27 | python -m cookiecutter --no-input . 28 | cat myextension/pyproject.toml 29 | 30 | - name: Test the extension 31 | working-directory: myextension 32 | run: | 33 | set -eux 34 | jlpm 35 | jlpm test 36 | 37 | - name: Install the extension 38 | working-directory: myextension 39 | run: | 40 | set -eux 41 | python -m pip install -v . 42 | cat myextension/labextension/package.json 43 | 44 | - name: List extensions 45 | run: | 46 | jupyter labextension list 47 | 48 | - name: Install dependencies 49 | working-directory: myextension/ui-tests 50 | env: 51 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 52 | run: jlpm install 53 | 54 | - name: Set up browser cache 55 | uses: actions/cache@v3 56 | with: 57 | path: | 58 | ${{ github.workspace }}/pw-browsers 59 | key: ${{ runner.os }}-${{ hashFiles('myextension/ui-tests/yarn.lock') }} 60 | 61 | - name: Install browser 62 | run: jlpm playwright install chromium 63 | working-directory: myextension/ui-tests 64 | 65 | - name: Execute integration tests 66 | working-directory: myextension/ui-tests 67 | run: | 68 | jlpm playwright test 69 | 70 | - name: Upload Playwright Test report 71 | if: always() 72 | uses: actions/upload-artifact@v3 73 | with: 74 | name: myextension-playwright-tests 75 | path: | 76 | myextension/ui-tests/test-results 77 | myextension/ui-tests/playwright-report 78 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["*.0"] 6 | pull_request: 7 | branches: ["*"] 8 | schedule: 9 | - cron: "0 0 * * *" 10 | 11 | jobs: 12 | names: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | include: 17 | - name: "my_extension" 18 | pyname: "my_extension" 19 | - name: "myextension" 20 | pyname: "myextension" 21 | - name: "my-extension" 22 | pyname: "my_extension" 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v3 27 | 28 | - name: Base Setup 29 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 30 | 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install cookiecutter 34 | 35 | - name: Create pure frontend extension 36 | env: 37 | NAME: ${{ matrix.name }} 38 | PYNAME: ${{ matrix.pyname }} 39 | run: | 40 | set -eux 41 | # Trick to use custom parameters 42 | python -c "from cookiecutter.main import cookiecutter; import json, os; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']=d['kind'][0]; d['labextension_name']=os.getenv('NAME'); cookiecutter('.', extra_context=d, no_input=True)" 43 | pushd ${PYNAME} 44 | python -m pip install "jupyterlab>=4.0.0,<5" 45 | jlpm 46 | jlpm stylelint-config-prettier-check 47 | jlpm lint:check 48 | python -m pip install -e . 49 | jupyter labextension develop . --overwrite 50 | jupyter labextension list 51 | jupyter labextension list 2>&1 | grep -ie "${NAME}.*OK" 52 | python -m jupyterlab.browser_check 53 | 54 | jupyter labextension uninstall ${NAME} 55 | python -m pip uninstall -y ${NAME} jupyterlab 56 | 57 | popd 58 | rm -rf ${NAME} 59 | 60 | no-tests: 61 | runs-on: ubuntu-latest 62 | strategy: 63 | matrix: 64 | # This will be used by the base setup action 65 | python-version: ["3.11"] 66 | 67 | steps: 68 | - name: Checkout 69 | uses: actions/checkout@v3 70 | 71 | - name: Base Setup 72 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 73 | 74 | - name: Install dependencies 75 | run: | 76 | python -m pip install cookiecutter 77 | 78 | - name: Create pure frontend extension 79 | run: | 80 | set -eux 81 | # Trick to use custom parameters 82 | python -c "from cookiecutter.main import cookiecutter; import json; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']=d['kind'][0]; d['test']='n'; cookiecutter('.', extra_context=d, no_input=True)" 83 | pushd myextension 84 | pip install "jupyterlab>=4.0.0,<5" 85 | jlpm 86 | jlpm lint:check 87 | pip install -e . 88 | jupyter labextension develop . --overwrite 89 | jupyter labextension list 90 | jupyter labextension list 2>&1 | grep -ie "myextension.*OK" 91 | python -m jupyterlab.browser_check 92 | 93 | jupyter labextension uninstall myextension 94 | pip uninstall -y myextension jupyterlab 95 | 96 | popd 97 | rm -rf myextension 98 | 99 | settings: 100 | runs-on: ubuntu-latest 101 | strategy: 102 | matrix: 103 | # This will be used by the base setup action 104 | python-version: ["3.8", "3.11"] 105 | 106 | steps: 107 | - name: Checkout 108 | uses: actions/checkout@v3 109 | 110 | - name: Base Setup 111 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 112 | 113 | - name: Install dependencies 114 | run: | 115 | python -m pip install cookiecutter 116 | 117 | - name: Create pure frontend extension 118 | run: | 119 | set -eux 120 | # Trick to use custom parameters 121 | python -c "from cookiecutter.main import cookiecutter; import json; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']=d['kind'][0]; d['has_settings']='y'; cookiecutter('.', extra_context=d, no_input=True)" 122 | pushd myextension 123 | pip install "jupyterlab>=4.0.0,<5" 124 | jlpm 125 | # It is not easily possible to get this version compatible with linter rules 126 | jlpm lint 127 | jlpm lint:check 128 | pip install -e . 129 | jupyter labextension develop . --overwrite 130 | jupyter labextension list 131 | jupyter labextension list 2>&1 | grep -ie "myextension.*OK" 132 | python -m jupyterlab.browser_check 133 | 134 | jupyter labextension uninstall myextension 135 | pip uninstall -y myextension jupyterlab 136 | 137 | popd 138 | rm -rf myextension 139 | 140 | server: 141 | runs-on: ${{ matrix.os }} 142 | strategy: 143 | fail-fast: false 144 | matrix: 145 | os: [ubuntu-latest, macos-latest, windows-latest] 146 | # This will be used by the base setup action 147 | python-version: ["3.8", "3.11"] 148 | 149 | steps: 150 | - name: Checkout 151 | uses: actions/checkout@v3 152 | 153 | - name: Base Setup 154 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 155 | 156 | - name: Install dependencies 157 | run: | 158 | python -m pip install cookiecutter build 159 | 160 | - name: Create server extension pip install 161 | run: | 162 | # Trick to use custom parameters 163 | python -c "from cookiecutter.main import cookiecutter; import json; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']='server'; cookiecutter('.', extra_context=d, no_input=True)" 164 | cd myextension 165 | cat pyproject.toml 166 | pip install . 167 | pip install "jupyterlab>=4.0.0,<5" 168 | jlpm 169 | jlpm lint:check 170 | 171 | - name: Check pip install method 172 | run: | 173 | set -eux 174 | jupyter server extension list 175 | jupyter server extension list 2>&1 | grep -ie "myextension.*OK" 176 | jupyter labextension list 177 | jupyter labextension list 2>&1 | grep -ie "myextension.*OK" 178 | 179 | # This test should be made outside the extension folder 180 | python -m jupyterlab.browser_check 181 | 182 | pip uninstall -y myextension jupyterlab 183 | rm -rf myextension 184 | shell: bash 185 | 186 | - name: Create server extension pip develop 187 | run: | 188 | # Trick to use custom parameters 189 | python -c "from cookiecutter.main import cookiecutter; import json; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']='server'; cookiecutter('.', extra_context=d, no_input=True)" 190 | cd myextension 191 | python -m pip install -e .[test] 192 | python -m pip install "jupyterlab>=4.0.0,<5" 193 | jupyter labextension develop . --overwrite 194 | jupyter server extension enable myextension 195 | 196 | # Check unit tests are passing 197 | python -m pytest -vv -r ap --cov myextension 198 | 199 | - name: Check pip develop method 200 | run: | 201 | set -eux 202 | jupyter server extension list 203 | jupyter server extension list 2>&1 | grep -ie "myextension.*OK" 204 | jupyter labextension list 205 | jupyter labextension list 2>&1 | grep -ie "myextension.*OK" 206 | 207 | # This test should be made outside the extension folder 208 | python -m jupyterlab.browser_check 209 | shell: bash 210 | 211 | - name: Build server extension in develop mode 212 | run: | 213 | jupyter labextension develop ./myextension --overwrite 214 | jupyter labextension build ./myextension 215 | 216 | jupyter labextension uninstall myextension 217 | python -m pip uninstall -y myextension jupyterlab 218 | 219 | python -c "import shutil; shutil.rmtree('myextension')" 220 | 221 | - name: Install server extension from a tarball 222 | run: | 223 | # Trick to use custom parameters 224 | python -c "from cookiecutter.main import cookiecutter; import json; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']='server'; cookiecutter('.', extra_context=d, no_input=True)" 225 | cd myextension 226 | python -m pip install "jupyterlab>=4.0.0,<5" 227 | jupyter lab clean --all 228 | python -m build 229 | cd dist 230 | python -m pip install myextension-0.1.0.tar.gz 231 | 232 | - name: Check install tarball method 233 | run: | 234 | set -eux 235 | jupyter labextension list 236 | jupyter labextension list 2>&1 | grep -ie "myextension.*OK" 237 | jupyter server extension list 238 | jupyter server extension list 2>&1 | grep -ie "myextension.*OK" 239 | 240 | jupyter lab build --dev-build --no-minimize 241 | 242 | python -m jupyterlab.browser_check 243 | 244 | cp myextension/dist/*.tar.gz myextension.tar.gz 245 | cp myextension/dist/*.whl myextension.whl 246 | python -m pip uninstall -y myextension jupyterlab 247 | rm -rf myextension 248 | shell: bash 249 | 250 | - uses: actions/upload-artifact@v2 251 | if: startsWith(runner.os, 'Linux') 252 | with: 253 | name: myextension-sdist 254 | path: | 255 | myextension.tar.gz 256 | myextension.whl 257 | 258 | test_isolated: 259 | needs: server 260 | runs-on: ubuntu-latest 261 | strategy: 262 | matrix: 263 | python-version: ["3.8", "3.11"] 264 | 265 | steps: 266 | - name: Checkout 267 | uses: actions/checkout@v3 268 | - name: Install Python 269 | uses: actions/setup-python@v4 270 | with: 271 | python-version: ${{ matrix.python-version }} 272 | architecture: "x64" 273 | - name: Setup pip cache 274 | uses: actions/cache@v3 275 | with: 276 | path: ~/.cache/pip 277 | key: pip-3.7-${{ hashFiles('package.json') }} 278 | restore-keys: | 279 | pip-3.7- 280 | pip- 281 | - uses: actions/download-artifact@v2 282 | with: 283 | name: myextension-sdist 284 | - name: Install and Test 285 | run: | 286 | set -eux 287 | # Remove NodeJS, twice to take care of system and locally installed node versions. 288 | sudo rm -rf $(which node) 289 | sudo rm -rf $(which node) 290 | 291 | python -m pip install myextension.tar.gz 292 | python -m pip install "jupyterlab>=4.0.0,<5" 293 | jupyter labextension list 2>&1 | grep -ie "myextension.*OK" 294 | jupyter server extension list 295 | jupyter server extension list 2>&1 | grep -ie "myextension.*OK" 296 | python -m jupyterlab.browser_check --no-browser-test 297 | 298 | theme: 299 | runs-on: ubuntu-latest 300 | strategy: 301 | matrix: 302 | # This will be used by the base setup action 303 | python-version: ["3.8", "3.11"] 304 | 305 | steps: 306 | - name: Checkout 307 | uses: actions/checkout@v3 308 | 309 | - name: Base Setup 310 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 311 | 312 | - name: Install dependencies 313 | run: | 314 | python -m pip install cookiecutter build 315 | 316 | - name: Create pure frontend extension 317 | run: | 318 | set -eux 319 | # Trick to use custom parameters 320 | python -c "from cookiecutter.main import cookiecutter; import json; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']='theme'; cookiecutter('.', extra_context=d, no_input=True)" 321 | pushd mytheme 322 | python -m pip install "jupyterlab>=4.0.0,<5" 323 | jlpm 324 | jlpm lint:check 325 | python -m pip install -e . 326 | jupyter labextension develop . --overwrite 327 | jupyter labextension list 328 | jupyter labextension list 2>&1 | grep -ie "mytheme.*OK" 329 | python -m jupyterlab.browser_check 330 | 331 | jupyter labextension uninstall mytheme 332 | python -m pip uninstall -y mytheme jupyterlab 333 | 334 | popd 335 | rm -rf mytheme 336 | 337 | pnpm_linker: 338 | runs-on: ubuntu-latest 339 | strategy: 340 | matrix: 341 | kind: ["frontend", "server", "theme"] 342 | 343 | steps: 344 | - name: Checkout 345 | uses: actions/checkout@v3 346 | 347 | - name: Base Setup 348 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 349 | 350 | - name: Install dependencies 351 | run: | 352 | python -m pip install cookiecutter 353 | 354 | - name: Create pure frontend extension 355 | env: 356 | KIND: ${{ matrix.kind }} 357 | run: | 358 | set -eux 359 | # Trick to use custom parameters 360 | python -c "from cookiecutter.main import cookiecutter; import json, os; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']=os.getenv('KIND'); d['labextension_name']='myextension'; cookiecutter('.', extra_context=d, no_input=True)" 361 | pushd myextension 362 | sed -i 's/^\(nodeLinker:\s\).*$/\1pnpm/' .yarnrc.yml 363 | python -m pip install "jupyterlab>=4.0.0,<5" 364 | jlpm 365 | if [ ! -d node_modules/.store ] ; then echo 'nodes_module directory should contain a .store directory when using pnpm nodeLinker'; exit 1; fi; 366 | jlpm build 367 | popd 368 | rm -rf myextension 369 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.egg-info/ 5 | .ipynb_checkpoints 6 | *.tsbuildinfo 7 | .eslintcache 8 | .stylelintcache 9 | {{cookiecutter.python_name}}/static 10 | 11 | # Created by https://www.gitignore.io/api/python 12 | # Edit at https://www.gitignore.io/?templates=python 13 | 14 | ### Python ### 15 | # Byte-compiled / optimized / DLL files 16 | __pycache__/ 17 | *.py[cod] 18 | *$py.class 19 | 20 | # C extensions 21 | *.so 22 | 23 | # Distribution / packaging 24 | .Python 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | lib/ 32 | lib64/ 33 | parts/ 34 | sdist/ 35 | var/ 36 | wheels/ 37 | pip-wheel-metadata/ 38 | share/python-wheels/ 39 | .installed.cfg 40 | *.egg 41 | MANIFEST 42 | 43 | # PyInstaller 44 | # Usually these files are written by a python script from a template 45 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 46 | *.manifest 47 | *.spec 48 | 49 | # Installer logs 50 | pip-log.txt 51 | pip-delete-this-directory.txt 52 | 53 | # Unit test / coverage reports 54 | htmlcov/ 55 | .tox/ 56 | .nox/ 57 | .coverage 58 | .coverage.* 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | .hypothesis/ 64 | .pytest_cache/ 65 | coverage/lcov-report/ 66 | coverage/lcov.info 67 | 68 | # Translations 69 | *.mo 70 | *.pot 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # Mr Developer 98 | .mr.developer.cfg 99 | .project 100 | .pydevproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | .dmypy.json 108 | dmypy.json 109 | 110 | # Pyre type checker 111 | .pyre/ 112 | 113 | # End of https://www.gitignore.io/api/python 114 | 115 | # OSX files 116 | .DS_Store 117 | 118 | # Default extension dir 119 | myextension 120 | .vscode/ 121 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017-2021, Project Jupyter Contributors 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 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * 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 | * 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JupyterLab extension-cookiecutter-ts 2 | 3 | [![Github Actions Status](https://github.com/jupyterlab/extension-cookiecutter-ts/workflows/CI/badge.svg)](https://github.com/jupyterlab/extension-cookiecutter-ts/actions/workflows/main.yml) 4 | 5 | ⚠️ **We strongly advice to use the template next generation: https://github.com/jupyterlab/extension-template** 6 | 7 | A [cookiecutter](https://github.com/audreyr/cookiecutter) template for creating 8 | a JupyterLab extension. Three kinds of extension are supported: 9 | - _frontend_: Pure frontend extension written in TypeScript. 10 | - _server_: Extension with frontend (in TypeScript) and backend (in Python) parts. 11 | - _theme_: Theme for JupyterLab (using CSS variables). 12 | 13 | > See also [extension-cookiecutter-js](https://github.com/jupyterlab/extension-cookiecutter-js) 14 | for an extension in CommonJS. 15 | 16 | ## Use the template to create package 17 | 18 | Install cookiecutter. 19 | 20 | ``` 21 | pip install cookiecutter 22 | ``` 23 | 24 | Use cookiecutter to generate a package, following the prompts to fill in the name and authorship of your new JupyterLab extension. 25 | 26 | ``` 27 | cookiecutter https://github.com/jupyterlab/extension-cookiecutter-ts --checkout 4.0 28 | ``` 29 | 30 | The available options are: 31 | 32 | - `kind`: 33 | * frontend --> Extension has only a frontend (TypeScript + CSS) part 34 | * server --> Extension has frontend and backend (new Python endpoint handlers for the server) parts 35 | * theme --> Extension provides a new theme (and nothing else) 36 | - `author_name`: The extension author name 37 | - `author_email`: The extension author email 38 | - `labextension_name`: Extension name 39 | - `python_name`: Pythonic extension name (if your extension has no server part, it will only be used to distribute the extension as a Python package) 40 | - `project_short_description`: Extension short description 41 | - `has_settings`: Whether the extension will have user settings or not. 42 | - `has_binder`: Whether to set up [binder](https://mybinder.readthedocs.io/en/latest/) for the extension or not. 43 | - `test`: Whether to add test set ups and skeletons for the extension or not 44 | - `repository`: Version Control System repository URI 45 | 46 | 47 | If you'd like to generate a package for a specific JupyterLab release, use the `--checkout` option and give a tag or commit from this repository. 48 | 49 | ``` 50 | cookiecutter https://github.com/jupyterlab/extension-cookiecutter-ts --checkout v1.0 51 | cookiecutter https://github.com/jupyterlab/extension-cookiecutter-ts --checkout v2.0 52 | cookiecutter https://github.com/jupyterlab/extension-cookiecutter-ts --checkout 3.0 53 | ``` 54 | 55 | ## A simple example 56 | 57 | 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. 58 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": ["frontend", "server", "theme"], 3 | "author_name": "My Name", 4 | "author_email": "me@test.com", 5 | "labextension_name": "{% if cookiecutter.kind == 'theme' %}mytheme{% else %}myextension{% endif %}", 6 | "python_name": "{{ cookiecutter.labextension_name | replace('-', '_') }}", 7 | "project_short_description": "A JupyterLab extension.", 8 | "has_settings": "n", 9 | "has_binder": "n", 10 | "test": "y", 11 | "repository": "https://github.com/github_username/{{ cookiecutter.labextension_name }}" 12 | } 13 | -------------------------------------------------------------------------------- /hooks/post_gen_project.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from pathlib import Path 3 | 4 | PROJECT_DIRECTORY = Path.cwd() 5 | 6 | 7 | def remove_path(path: Path) -> None: 8 | """Remove the provided path. 9 | 10 | If the target path is a directory, remove it recursively. 11 | """ 12 | if not path.exists(): 13 | return 14 | 15 | if path.is_file(): 16 | path.unlink() 17 | elif path.is_dir(): 18 | for f in path.iterdir(): 19 | remove_path(f) 20 | path.rmdir() 21 | 22 | 23 | if __name__ == "__main__": 24 | 25 | if not "{{ cookiecutter.has_settings }}".lower().startswith("y"): 26 | remove_path(PROJECT_DIRECTORY / "schema") 27 | 28 | if "{{ cookiecutter.kind }}".lower() == "theme": 29 | for f in ( 30 | "style/index.js", 31 | "style/base.css" 32 | ): 33 | remove_path(PROJECT_DIRECTORY / f) 34 | else: 35 | remove_path(PROJECT_DIRECTORY / "style/variables.css") 36 | 37 | if not "{{ cookiecutter.kind }}".lower() == "server": 38 | for f in ( 39 | "{{ cookiecutter.python_name }}/handlers.py", 40 | "src/handler.ts", 41 | "jupyter-config", 42 | "conftest.py", 43 | "{{ cookiecutter.python_name }}/tests" 44 | ): 45 | remove_path(PROJECT_DIRECTORY / f) 46 | 47 | if not "{{ cookiecutter.has_binder }}".lower().startswith("y"): 48 | remove_path(PROJECT_DIRECTORY / "binder") 49 | remove_path(PROJECT_DIRECTORY / ".github/workflows/binder-on-pr.yml") 50 | 51 | if not "{{ cookiecutter.test }}".lower().startswith("y"): 52 | remove_path(PROJECT_DIRECTORY / ".github" / "workflows" / "update-integration-tests.yml") 53 | remove_path(PROJECT_DIRECTORY / "src" / "__tests__") 54 | remove_path(PROJECT_DIRECTORY / "ui-tests") 55 | remove_path(PROJECT_DIRECTORY / "{{ cookiecutter.python_name }}" / "tests") 56 | remove_path(PROJECT_DIRECTORY / "babel.config.js") 57 | remove_path(PROJECT_DIRECTORY / "conftest.py") 58 | remove_path(PROJECT_DIRECTORY / "jest.config.js") 59 | remove_path(PROJECT_DIRECTORY / "tsconfig.test.json") 60 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/.github/workflows/binder-on-pr.yml: -------------------------------------------------------------------------------- 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 | {% raw %} 15 | github_token: ${{ secrets.github_token }} 16 | {% endraw %} 17 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: '*' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Base Setup 18 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 19 | 20 | - name: Install dependencies 21 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 22 | 23 | - name: Lint the extension 24 | run: | 25 | set -eux 26 | jlpm 27 | jlpm run lint:check 28 | {% if cookiecutter.test.lower().startswith('y') %} 29 | - name: Test the extension 30 | run: | 31 | set -eux 32 | jlpm run test 33 | {% endif %} 34 | - name: Build the extension 35 | run: | 36 | set -eux 37 | python -m pip install .[test] 38 | {% if cookiecutter.kind.lower() == 'server' %} 39 | pytest -vv -r ap --cov {{ cookiecutter.python_name }} 40 | jupyter server extension list 41 | jupyter server extension list 2>&1 | grep -ie "{{ cookiecutter.python_name }}.*OK" 42 | {% endif %} 43 | jupyter labextension list 44 | jupyter labextension list 2>&1 | grep -ie "{{ cookiecutter.labextension_name }}.*OK" 45 | python -m jupyterlab.browser_check 46 | 47 | - name: Package the extension 48 | run: | 49 | set -eux 50 | 51 | pip install build 52 | python -m build 53 | pip uninstall -y "{{ cookiecutter.python_name }}" jupyterlab 54 | 55 | - name: Upload extension packages 56 | uses: actions/upload-artifact@v3 57 | with: 58 | name: extension-artifacts 59 | path: dist/{{ cookiecutter.python_name }}* 60 | if-no-files-found: error 61 | 62 | test_isolated: 63 | needs: build 64 | runs-on: ubuntu-latest 65 | 66 | steps: 67 | - name: Install Python 68 | uses: actions/setup-python@v4 69 | with: 70 | python-version: '3.9' 71 | architecture: 'x64' 72 | - uses: actions/download-artifact@v3 73 | with: 74 | name: extension-artifacts 75 | - name: Install and Test 76 | run: | 77 | set -eux 78 | # Remove NodeJS, twice to take care of system and locally installed node versions. 79 | sudo rm -rf $(which node) 80 | sudo rm -rf $(which node) 81 | 82 | pip install "jupyterlab>=4.0.0,<5" {{ cookiecutter.python_name }}*.whl 83 | 84 | {% if cookiecutter.kind.lower() == 'server' %} 85 | jupyter server extension list 86 | jupyter server extension list 2>&1 | grep -ie "{{ cookiecutter.python_name }}.*OK" 87 | {% endif %} 88 | jupyter labextension list 89 | jupyter labextension list 2>&1 | grep -ie "{{ cookiecutter.labextension_name }}.*OK" 90 | python -m jupyterlab.browser_check --no-browser-test 91 | {% if cookiecutter.test.lower().startswith('y') %} 92 | integration-tests: 93 | name: Integration tests 94 | needs: build 95 | runs-on: ubuntu-latest 96 | 97 | env: 98 | PLAYWRIGHT_BROWSERS_PATH: ${{ "{{ github.workspace }}" }}/pw-browsers 99 | 100 | steps: 101 | - name: Checkout 102 | uses: actions/checkout@v3 103 | 104 | - name: Base Setup 105 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 106 | 107 | - name: Download extension package 108 | uses: actions/download-artifact@v3 109 | with: 110 | name: extension-artifacts 111 | 112 | - name: Install the extension 113 | run: | 114 | set -eux 115 | python -m pip install "jupyterlab>=4.0.0,<5" {{ cookiecutter.python_name }}*.whl 116 | 117 | - name: Install dependencies 118 | working-directory: ui-tests 119 | env: 120 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 121 | run: jlpm install 122 | {% raw %} 123 | - name: Set up browser cache 124 | uses: actions/cache@v3 125 | with: 126 | path: | 127 | ${{ github.workspace }}/pw-browsers 128 | key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} 129 | {% endraw %} 130 | - name: Install browser 131 | run: jlpm playwright install chromium 132 | working-directory: ui-tests 133 | 134 | - name: Execute integration tests 135 | working-directory: ui-tests 136 | run: | 137 | jlpm playwright test 138 | 139 | - name: Upload Playwright Test report 140 | if: always() 141 | uses: actions/upload-artifact@v3 142 | with: 143 | name: {{ cookiecutter.python_name }}-playwright-tests 144 | path: | 145 | ui-tests/test-results 146 | ui-tests/playwright-report{% endif %} 147 | 148 | check_links: 149 | name: Check Links 150 | runs-on: ubuntu-latest 151 | timeout-minutes: 15 152 | steps: 153 | - uses: actions/checkout@v3 154 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 155 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 156 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/.github/workflows/check-release.yml: -------------------------------------------------------------------------------- 1 | name: Check Release 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | branches: ["*"] 7 | 8 | jobs: 9 | check_release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Base Setup 15 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 16 | - name: Install Dependencies 17 | run: | 18 | pip install -e . 19 | - name: Check Release 20 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 21 | with: 22 | {% raw %} 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | {% endraw %} 25 | - name: Upload Distributions 26 | uses: actions/upload-artifact@v3 27 | with: 28 | name: {{ cookiecutter.python_name }}-releaser-dist-${{ '{{ github.run_number }}' }} 29 | path: .jupyter_releaser_checkout/dist 30 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/.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 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/.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 | since: 16 | description: "Use PRs with activity since this date or git reference" 17 | required: false 18 | since_last_stable: 19 | description: "Use PRs with activity since the last stable git tag" 20 | required: false 21 | type: boolean 22 | jobs: 23 | prep_release: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 27 | {% raw %} 28 | - name: Prep Release 29 | id: prep-release 30 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 31 | with: 32 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }} 33 | version_spec: ${{ github.event.inputs.version_spec }} 34 | post_version_spec: ${{ github.event.inputs.post_version_spec }} 35 | target: ${{ github.event.inputs.target }} 36 | branch: ${{ github.event.inputs.branch }} 37 | since: ${{ github.event.inputs.since }} 38 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 39 | 40 | - name: "** Next Step **" 41 | run: | 42 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}"{% endraw %} 43 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/.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 | steps: 19 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 20 | {% raw %} 21 | - name: Populate Release 22 | id: populate-release 23 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 24 | with: 25 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }} 26 | target: ${{ github.event.inputs.target }} 27 | branch: ${{ github.event.inputs.branch }} 28 | release_url: ${{ github.event.inputs.release_url }} 29 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }} 30 | 31 | - name: Finalize Release 32 | id: finalize-release 33 | env: 34 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 35 | PYPI_TOKEN_MAP: ${{ secrets.PYPI_TOKEN_MAP }} 36 | TWINE_USERNAME: __token__ 37 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 38 | uses: jupyter-server/jupyter-releaser/.github/actions/finalize-release@v2 39 | with: 40 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }} 41 | target: ${{ github.event.inputs.target }} 42 | release_url: ${{ steps.populate-release.outputs.release_url }} 43 | 44 | - name: "** Next Step **" 45 | if: ${{ success() }} 46 | run: | 47 | echo "Verify the final release" 48 | echo ${{ steps.finalize-release.outputs.release_url }} 49 | 50 | - name: "** Failure Message **" 51 | if: ${{ failure() }} 52 | run: | 53 | echo "Failed to Publish the Draft Release Url:" 54 | echo ${{ steps.populate-release.outputs.release_url }}{% endraw %} 55 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/.github/workflows/update-integration-tests.yml: -------------------------------------------------------------------------------- 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:{# Escape double curly brace #}{% raw %} 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.issue.pull_request && contains(github.event.comment.body, 'please update snapshots') 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | with: 26 | token: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Configure git to use https 29 | run: git config --global hub.protocol https 30 | 31 | - name: Get PR Info 32 | id: pr 33 | env: 34 | PR_NUMBER: ${{ github.event.issue.number }} 35 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | GH_REPO: ${{ github.repository }} 37 | COMMENT_AT: ${{ github.event.comment.created_at }} 38 | run: | 39 | pr="$(gh api /repos/${GH_REPO}/pulls/${PR_NUMBER})" 40 | head_sha="$(echo "$pr" | jq -r .head.sha)" 41 | pushed_at="$(echo "$pr" | jq -r .pushed_at)" 42 | 43 | if [[ $(date -d "$pushed_at" +%s) -gt $(date -d "$COMMENT_AT" +%s) ]]; then 44 | echo "Updating is not allowed because the PR was pushed to (at $pushed_at) after the triggering comment was issued (at $COMMENT_AT)" 45 | exit 1 46 | fi 47 | 48 | echo "head_sha=$head_sha" >> $GITHUB_OUTPUT 49 | 50 | - name: Checkout the branch from the PR that triggered the job 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | run: gh pr checkout ${{ github.event.issue.number }} 54 | 55 | - name: Validate the fetched branch HEAD revision 56 | env: 57 | EXPECTED_SHA: ${{ steps.pr.outputs.head_sha }} 58 | run: | 59 | actual_sha="$(git rev-parse HEAD)" 60 | 61 | if [[ "$actual_sha" != "$EXPECTED_SHA" ]]; then 62 | 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)" 63 | exit 1 64 | fi 65 | 66 | - name: Base Setup 67 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 68 | 69 | - name: Install dependencies 70 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 71 | 72 | - name: Install extension 73 | run: | 74 | set -eux 75 | jlpm 76 | python -m pip install . 77 | 78 | - uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@v1 79 | with: 80 | github_token: ${{ secrets.GITHUB_TOKEN }} 81 | # Playwright knows how to start JupyterLab server 82 | start_server_script: 'null' 83 | test_folder: ui-tests{% endraw %} 84 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.log 5 | .eslintcache 6 | .stylelintcache 7 | *.egg-info/ 8 | .ipynb_checkpoints 9 | *.tsbuildinfo 10 | {{cookiecutter.python_name}}/labextension 11 | # Version file is handled by hatchling 12 | {{cookiecutter.python_name}}/_version.py 13 | {% if cookiecutter.test.lower().startswith('y') %} 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 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | !/package.json 6 | {{cookiecutter.python_name}} 7 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableImmutableInstalls: false 2 | 3 | nodeLinker: node-modules 4 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) {% now 'utc', '%Y' %}, {{ cookiecutter.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 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/README.md: -------------------------------------------------------------------------------- 1 | # {{ cookiecutter.python_name }} 2 | 3 | [![Github Actions Status]({{ cookiecutter.repository }}/workflows/Build/badge.svg)]({{ cookiecutter.repository }}/actions/workflows/build.yml) 4 | {%- if cookiecutter.has_binder.lower().startswith('y') -%} 5 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/{{ cookiecutter.repository|replace("https://github.com/", "") }}/main?urlpath=lab) 6 | 7 | {%- endif %} 8 | {{ cookiecutter.project_short_description }} 9 | {% if cookiecutter.kind.lower() == 'server' %} 10 | This extension is composed of a Python package named `{{ cookiecutter.python_name }}` 11 | for the server extension and a NPM package named `{{ cookiecutter.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 {{ cookiecutter.python_name }} 24 | ``` 25 | 26 | ## Uninstall 27 | 28 | To remove the extension, execute: 29 | 30 | ```bash 31 | pip uninstall {{ cookiecutter.python_name }} 32 | ``` 33 | {% if cookiecutter.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 {{ cookiecutter.python_name }} directory 63 | # Install package in development mode 64 | pip install -e ".{% if cookiecutter.test.lower().startswith('y') and cookiecutter.kind.lower() == 'server' %}[test]{% endif %}" 65 | # Link your development version of the extension with JupyterLab 66 | jupyter labextension develop . --overwrite{% if cookiecutter.kind.lower() == 'server' %} 67 | # Server extension must be manually installed in develop mode 68 | jupyter server extension enable {{ cookiecutter.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 cookiecutter.kind.lower() == 'server' %} 93 | # Server extension must be manually disabled in develop mode 94 | jupyter server extension disable {{ cookiecutter.python_name }}{% endif %} 95 | pip uninstall {{ cookiecutter.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 `{{ cookiecutter.labextension_name }}` within that folder. 101 | {% if cookiecutter.test.lower().startswith('y') %} 102 | ### Testing the extension{% if cookiecutter.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 {{ cookiecutter.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 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a new release of {{ cookiecutter.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. 62 | 63 | Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. 64 | 65 | Here is a summary of the steps to cut a new release: 66 | 67 | - Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository 68 | - Go to the Actions panel 69 | - Run the "Step 1: Prep Release" workflow 70 | - Check the draft changelog 71 | - Run the "Step 2: Publish Release" workflow 72 | 73 | ## Publishing to `conda-forge` 74 | 75 | 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 76 | 77 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. 78 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@jupyterlab/testutils/lib/babel.config'); 2 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/binder/environment.yml: -------------------------------------------------------------------------------- 1 | # a mybinder.org-ready environment for demoing {{ cookiecutter.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 {{ cookiecutter.python_name | replace('_', '-') }}-demo 6 | # 7 | name: {{ cookiecutter.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 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/binder/postBuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ perform a development install of {{ cookiecutter.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 cookiecutter.kind.lower() == 'server' %} 35 | _( 36 | sys.executable, 37 | "-m", 38 | "jupyter", 39 | "server", 40 | "extension", 41 | "enable", 42 | "{{ cookiecutter.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 {{ cookiecutter.python_name }} is ready to run with:\n") 56 | print("\tjupyter lab\n") 57 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/conftest.py: -------------------------------------------------------------------------------- 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": {"{{ cookiecutter.python_name }}": True}}} 9 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "{{ cookiecutter.python_name }}", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package {{ cookiecutter.python_name }}" 5 | } 6 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/jest.config.js: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/jupyter-config/nb-config/{{cookiecutter.python_name}}.json: -------------------------------------------------------------------------------- 1 | { 2 | "NotebookApp": { 3 | "nbserver_extensions": { 4 | "{{ cookiecutter.python_name }}": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/jupyter-config/server-config/{{cookiecutter.python_name}}.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerApp": { 3 | "jpserver_extensions": { 4 | "{{ cookiecutter.python_name }}": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ cookiecutter.labextension_name }}", 3 | "version": "0.1.0", 4 | "description": "{{ cookiecutter.project_short_description }}", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "{{ cookiecutter.repository }}", 11 | "bugs": { 12 | "url": "{{ cookiecutter.repository }}/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": { 16 | "name": "{{ cookiecutter.author_name }}", 17 | "email": "{{ cookiecutter.author_email }}" 18 | }, 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}"{% if cookiecutter.has_settings.lower().startswith('y') %}, 22 | "schema/*.json"{% endif %} 23 | ], 24 | "main": "lib/index.js", 25 | "types": "lib/index.d.ts",{% if cookiecutter.kind != 'theme' %} 26 | "style": "style/index.css",{% endif %} 27 | "repository": { 28 | "type": "git", 29 | "url": "{{ cookiecutter.repository }}.git" 30 | },{% if cookiecutter.test.lower().startswith('y') %} 31 | "workspaces": [ 32 | "ui-tests" 33 | ],{% endif %} 34 | "scripts": { 35 | "build": "jlpm build:lib && jlpm build:labextension:dev", 36 | "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", 37 | "build:labextension": "jupyter labextension build .", 38 | "build:labextension:dev": "jupyter labextension build --development True .", 39 | "build:lib": "tsc --sourceMap", 40 | "build:lib:prod": "tsc", 41 | "clean": "jlpm clean:lib", 42 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 43 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 44 | "clean:labextension": "rimraf {{ cookiecutter.python_name }}/labextension {{ cookiecutter.python_name }}/_version.py", 45 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", 46 | "eslint": "jlpm eslint:check --fix", 47 | "eslint:check": "eslint . --cache --ext .ts,.tsx", 48 | "install:extension": "jlpm build", 49 | "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", 50 | "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", 51 | "prettier": "jlpm prettier:base --write --list-different", 52 | "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", 53 | "prettier:check": "jlpm prettier:base --check", 54 | "stylelint": "jlpm stylelint:check --fix", 55 | "stylelint:check": "stylelint --cache \"style/**/*.css\"",{% if cookiecutter.test.lower().startswith('y') %} 56 | "test": "jest --coverage",{% endif %} 57 | "watch": "run-p watch:src watch:labextension", 58 | "watch:src": "tsc -w --sourceMap", 59 | "watch:labextension": "jupyter labextension watch ." 60 | }, 61 | "dependencies": { 62 | "@jupyterlab/application": "^4.0.0"{% if cookiecutter.kind.lower() == 'theme' %}, 63 | "@jupyterlab/apputils": "^4.0.0"{% endif %}{% if cookiecutter.has_settings.lower().startswith('y') %}, 64 | "@jupyterlab/settingregistry": "^4.0.0"{% endif %}{% if cookiecutter.kind.lower() == 'server' %}, 65 | "@jupyterlab/coreutils": "^6.0.0", 66 | "@jupyterlab/services": "^7.0.0"{% endif %} 67 | }, 68 | "devDependencies": { 69 | "@jupyterlab/builder": "^4.0.0",{% if cookiecutter.test.lower().startswith('y') %} 70 | "@jupyterlab/testutils": "^4.0.0", 71 | "@types/jest": "^29.2.0",{% endif %} 72 | "@types/json-schema": "^7.0.11", 73 | "@types/react": "^18.0.26", 74 | "@typescript-eslint/eslint-plugin": "^5.55.0", 75 | "@typescript-eslint/parser": "^5.55.0", 76 | "css-loader": "^6.7.1", 77 | "eslint": "^8.36.0", 78 | "eslint-config-prettier": "^8.7.0", 79 | "eslint-plugin-prettier": "^4.2.1",{% if cookiecutter.test.lower().startswith('y') %} 80 | "jest": "^29.2.0",{% endif %}{% if cookiecutter.kind.lower() == 'server' %} 81 | "mkdirp": "^1.0.3",{% endif %} 82 | "npm-run-all": "^4.1.5", 83 | "prettier": "^2.8.7", 84 | "rimraf": "^4.4.1", 85 | "source-map-loader": "^1.0.2", 86 | "style-loader": "^3.3.1", 87 | "stylelint": "^14.9.1", 88 | "stylelint-config-prettier": "^9.0.4", 89 | "stylelint-config-recommended": "^8.0.0", 90 | "stylelint-config-standard": "^26.0.0", 91 | "stylelint-prettier": "^2.0.0", 92 | "typescript": "~5.0.2", 93 | "yjs": "^13.5.0" 94 | }, 95 | "sideEffects": [ 96 | "style/*.css"{% if cookiecutter.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 cookiecutter.kind.lower() == 'server' %} 105 | "discovery": { 106 | "server": { 107 | "managers": [ 108 | "pip" 109 | ], 110 | "base": { 111 | "name": "{{ cookiecutter.python_name }}" 112 | } 113 | } 114 | },{% endif %} 115 | "extension": true, 116 | "outputDir": "{{cookiecutter.python_name}}/labextension"{% if cookiecutter.has_settings.lower().startswith('y') %}, 117 | "schemaDir": "schema"{% endif %}{% if cookiecutter.kind.lower() == 'theme' %}, 118 | "themePath": "style/index.css"{% endif %} 119 | }, 120 | "eslintIgnore": [ 121 | "node_modules", 122 | "dist", 123 | "coverage", 124 | "**/*.d.ts"{% if cookiecutter.test.lower().startswith('y') %}, 125 | "tests", 126 | "**/__tests__", 127 | "ui-tests"{% endif %} 128 | ], 129 | "eslintConfig": { 130 | "extends": [ 131 | "eslint:recommended", 132 | "plugin:@typescript-eslint/eslint-recommended", 133 | "plugin:@typescript-eslint/recommended", 134 | "plugin:prettier/recommended" 135 | ], 136 | "parser": "@typescript-eslint/parser", 137 | "parserOptions": { 138 | "project": "tsconfig.json", 139 | "sourceType": "module" 140 | }, 141 | "plugins": [ 142 | "@typescript-eslint" 143 | ], 144 | "rules": { 145 | "@typescript-eslint/naming-convention": [ 146 | "error", 147 | { 148 | "selector": "interface", 149 | "format": [ 150 | "PascalCase" 151 | ], 152 | "custom": { 153 | "regex": "^I[A-Z]", 154 | "match": true 155 | } 156 | } 157 | ], 158 | "@typescript-eslint/no-unused-vars": [ 159 | "warn", 160 | { 161 | "args": "none" 162 | } 163 | ], 164 | "@typescript-eslint/no-explicit-any": "off", 165 | "@typescript-eslint/no-namespace": "off", 166 | "@typescript-eslint/no-use-before-define": "off", 167 | "@typescript-eslint/quotes": [ 168 | "error", 169 | "single", 170 | { 171 | "avoidEscape": true, 172 | "allowTemplateLiterals": false 173 | } 174 | ], 175 | "curly": [ 176 | "error", 177 | "all" 178 | ], 179 | "eqeqeq": "error", 180 | "prefer-arrow-callback": "error" 181 | } 182 | }, 183 | "prettier": { 184 | "singleQuote": true, 185 | "trailingComma": "none", 186 | "arrowParens": "avoid", 187 | "endOfLine": "auto" 188 | }, 189 | "stylelint": { 190 | "extends": [ 191 | "stylelint-config-recommended", 192 | "stylelint-config-standard", 193 | "stylelint-prettier/recommended" 194 | ], 195 | "rules": { 196 | "property-no-vendor-prefix": null, 197 | "selector-no-vendor-prefix": null, 198 | "value-no-vendor-prefix": null{% if cookiecutter.kind.lower() == "theme" %}, 199 | "alpha-value-notation": null, 200 | "color-function-notation": null{% endif %} 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "{{ cookiecutter.python_name }}" 7 | readme = "README.md" 8 | license = { file = "LICENSE" } 9 | requires-python = ">=3.8" 10 | classifiers = [ 11 | "Framework :: Jupyter", 12 | "Framework :: Jupyter :: JupyterLab", 13 | "Framework :: Jupyter :: JupyterLab :: 4", 14 | "Framework :: Jupyter :: JupyterLab :: Extensions", 15 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 16 | "License :: OSI Approved :: BSD License", 17 | "Programming Language :: Python", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.8", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | ] 24 | dependencies = [{% if cookiecutter.kind.lower() == "server" %} 25 | "jupyter_server>=2.0.1,<3"{% endif %} 26 | ] 27 | dynamic = ["version", "description", "authors", "urls", "keywords"] 28 | {% if cookiecutter.test.lower().startswith('y') and cookiecutter.kind.lower() == 'server' %} 29 | [project.optional-dependencies] 30 | test = [ 31 | "coverage", 32 | "pytest", 33 | "pytest-asyncio", 34 | "pytest-cov", 35 | "pytest-jupyter[server]>=0.6.0" 36 | ] 37 | {% endif %} 38 | [tool.hatch.version] 39 | source = "nodejs" 40 | 41 | [tool.hatch.metadata.hooks.nodejs] 42 | fields = ["description", "authors", "urls"] 43 | 44 | [tool.hatch.build.targets.sdist] 45 | artifacts = ["{{ cookiecutter.python_name }}/labextension"] 46 | exclude = [".github", "binder"] 47 | 48 | [tool.hatch.build.targets.wheel.shared-data] 49 | "{{ cookiecutter.python_name }}/labextension" = "share/jupyter/labextensions/{{ cookiecutter.labextension_name }}" 50 | "install.json" = "share/jupyter/labextensions/{{ cookiecutter.labextension_name }}/install.json"{% if cookiecutter.kind.lower() == "server" %} 51 | "jupyter-config/server-config" = "etc/jupyter/jupyter_server_config.d" 52 | "jupyter-config/nb-config" = "etc/jupyter/jupyter_notebook_config.d"{% endif %} 53 | 54 | [tool.hatch.build.hooks.version] 55 | path = "{{ cookiecutter.python_name }}/_version.py" 56 | 57 | [tool.hatch.build.hooks.jupyter-builder] 58 | dependencies = ["hatch-jupyter-builder>=0.5"] 59 | build-function = "hatch_jupyter_builder.npm_builder" 60 | ensured-targets = [{% if cookiecutter.kind.lower() != "theme" %} 61 | "{{ cookiecutter.python_name }}/labextension/static/style.js",{% endif %} 62 | "{{ cookiecutter.python_name }}/labextension/package.json", 63 | ] 64 | skip-if-exists = ["{{ cookiecutter.python_name }}/labextension/static/style.js"] 65 | 66 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 67 | build_cmd = "build:prod" 68 | npm = ["jlpm"] 69 | 70 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] 71 | build_cmd = "install:extension" 72 | npm = ["jlpm"] 73 | source_dir = "src" 74 | build_dir = "{{cookiecutter.python_name}}/labextension" 75 | 76 | [tool.jupyter-releaser.options] 77 | version_cmd = "hatch version" 78 | 79 | [tool.jupyter-releaser.hooks] 80 | before-build-npm = [ 81 | "python -m pip install 'jupyterlab>=4.0.0,<5'", 82 | "jlpm", 83 | "jlpm build:prod" 84 | ] 85 | before-build-python = ["jlpm clean:all"] 86 | 87 | [tool.check-wheel-contents] 88 | ignore = ["W002"] 89 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/schema/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "jupyter.lab.shortcuts": [], 3 | "title": "{{ cookiecutter.labextension_name }}", 4 | "description": "{{ cookiecutter.labextension_name }} settings.", 5 | "type": "object", 6 | "properties": {}, 7 | "additionalProperties": false 8 | } 9 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/setup.py: -------------------------------------------------------------------------------- 1 | __import__('setuptools').setup() 2 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/src/__tests__/{{cookiecutter.python_name}}.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests 3 | */ 4 | 5 | describe('{{ cookiecutter.labextension_name }}', () => { 6 | it('should be tested', () => { 7 | expect(1 + 1).toEqual(2); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/src/handler.ts: -------------------------------------------------------------------------------- 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 | '{{ cookiecutter.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 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JupyterFrontEnd, 3 | JupyterFrontEndPlugin 4 | } from '@jupyterlab/application';{% if cookiecutter.kind.lower() == 'theme' %} 5 | 6 | import { IThemeManager } from '@jupyterlab/apputils';{% endif %}{% if cookiecutter.has_settings.lower().startswith('y') %} 7 | 8 | import { ISettingRegistry } from '@jupyterlab/settingregistry';{% endif %}{% if cookiecutter.kind.lower() == 'server' %} 9 | 10 | import { requestAPI } from './handler';{% endif %} 11 | 12 | /** 13 | * Initialization data for the {{ cookiecutter.labextension_name }} extension. 14 | */ 15 | const plugin: JupyterFrontEndPlugin = { 16 | id: '{{ cookiecutter.labextension_name }}:plugin', 17 | description: '{{ cookiecutter.project_short_description }}', 18 | autoStart: true,{% if cookiecutter.kind.lower() == 'theme' %} 19 | requires: [IThemeManager],{% endif %}{% if cookiecutter.has_settings.lower().startswith('y') %} 20 | optional: [ISettingRegistry],{% endif %} 21 | activate: (app: JupyterFrontEnd{% if cookiecutter.kind.lower() == 'theme' %}, manager: IThemeManager{% endif %}{% if cookiecutter.has_settings.lower().startswith('y') %}, settingRegistry: ISettingRegistry | null{% endif %}) => { 22 | console.log('JupyterLab extension {{ cookiecutter.labextension_name }} is activated!');{% if cookiecutter.kind.lower() == 'theme' %} 23 | const style = '{{ cookiecutter.labextension_name }}/index.css'; 24 | 25 | manager.register({ 26 | name: '{{ cookiecutter.labextension_name }}', 27 | isLight: true, 28 | load: () => manager.loadCSS(style), 29 | unload: () => Promise.resolve(undefined) 30 | });{% endif %}{% if cookiecutter.has_settings.lower().startswith('y') %} 31 | 32 | if (settingRegistry) { 33 | settingRegistry 34 | .load(plugin.id) 35 | .then(settings => { 36 | console.log('{{ cookiecutter.labextension_name }} settings loaded:', settings.composite); 37 | }) 38 | .catch(reason => { 39 | console.error('Failed to load settings for {{ cookiecutter.labextension_name }}.', reason); 40 | }); 41 | }{% endif %}{% if cookiecutter.kind.lower() == 'server' %} 42 | 43 | requestAPI('get-example') 44 | .then(data => { 45 | console.log(data); 46 | }) 47 | .catch(reason => { 48 | console.error( 49 | `The {{ cookiecutter.python_name }} server extension appears to be missing.\n${reason}` 50 | ); 51 | });{% endif %} 52 | } 53 | }; 54 | 55 | export default plugin; 56 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/style/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | See the JupyterLab Developer Guide for useful CSS Patterns: 3 | 4 | https://jupyterlab.readthedocs.io/en/stable/developer/css.html 5 | */ 6 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/style/index.css: -------------------------------------------------------------------------------- 1 | {% if cookiecutter.kind.lower() == 'theme' %}@import './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 'base.css';{% endif %} 13 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/style/index.js: -------------------------------------------------------------------------------- 1 | import './base.css'; 2 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/style/variables.css: -------------------------------------------------------------------------------- 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: 0 2px 1px -1px var(--jp-shadow-umbra-color), 57 | 0 1px 1px 0 var(--jp-shadow-penumbra-color), 58 | 0 1px 3px 0 var(--jp-shadow-ambient-color); 59 | --jp-elevation-z2: 0 3px 1px -2px var(--jp-shadow-umbra-color), 60 | 0 2px 2px 0 var(--jp-shadow-penumbra-color), 61 | 0 1px 5px 0 var(--jp-shadow-ambient-color); 62 | --jp-elevation-z4: 0 2px 4px -1px var(--jp-shadow-umbra-color), 63 | 0 4px 5px 0 var(--jp-shadow-penumbra-color), 64 | 0 1px 10px 0 var(--jp-shadow-ambient-color); 65 | --jp-elevation-z6: 0 3px 5px -1px var(--jp-shadow-umbra-color), 66 | 0 6px 10px 0 var(--jp-shadow-penumbra-color), 67 | 0 1px 18px 0 var(--jp-shadow-ambient-color); 68 | --jp-elevation-z8: 0 5px 5px -3px var(--jp-shadow-umbra-color), 69 | 0 8px 10px 1px var(--jp-shadow-penumbra-color), 70 | 0 3px 14px 2px var(--jp-shadow-ambient-color); 71 | --jp-elevation-z12: 0 7px 8px -4px var(--jp-shadow-umbra-color), 72 | 0 12px 17px 2px var(--jp-shadow-penumbra-color), 73 | 0 5px 22px 4px var(--jp-shadow-ambient-color); 74 | --jp-elevation-z16: 0 8px 10px -5px var(--jp-shadow-umbra-color), 75 | 0 16px 24px 2px var(--jp-shadow-penumbra-color), 76 | 0 6px 30px 5px var(--jp-shadow-ambient-color); 77 | --jp-elevation-z20: 0 10px 13px -6px var(--jp-shadow-umbra-color), 78 | 0 20px 31px 3px var(--jp-shadow-penumbra-color), 79 | 0 8px 38px 7px var(--jp-shadow-ambient-color); 80 | --jp-elevation-z24: 0 11px 15px -7px var(--jp-shadow-umbra-color), 81 | 0 24px 38px 3px var(--jp-shadow-penumbra-color), 82 | 0 9px 46px 8px var(--jp-shadow-ambient-color); 83 | 84 | /* Borders 85 | * 86 | * The following variables, specify the visual styling of borders in JupyterLab. 87 | */ 88 | 89 | --jp-border-width: 1px; 90 | --jp-border-color0: var(--md-grey-400); 91 | --jp-border-color1: var(--md-grey-400); 92 | --jp-border-color2: var(--md-grey-300); 93 | --jp-border-color3: var(--md-grey-200); 94 | --jp-border-radius: 2px; 95 | 96 | /* UI Fonts 97 | * 98 | * The UI font CSS variables are used for the typography all of the JupyterLab 99 | * user interface elements that are not directly user generated content. 100 | * 101 | * The font sizing here is done assuming that the body font size of --jp-ui-font-size1 102 | * is applied to a parent element. When children elements, such as headings, are sized 103 | * in em all things will be computed relative to that body size. 104 | */ 105 | 106 | --jp-ui-font-scale-factor: 1.2; 107 | --jp-ui-font-size0: 0.8333em; 108 | --jp-ui-font-size1: 13px; /* Base font size */ 109 | --jp-ui-font-size2: 1.2em; 110 | --jp-ui-font-size3: 1.44em; 111 | --jp-ui-font-family: -apple-system, blinkmacsystemfont, 'Segoe UI', helvetica, 112 | arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 113 | 114 | /* 115 | * Use these font colors against the corresponding main layout colors. 116 | * In a light theme, these go from dark to light. 117 | */ 118 | 119 | /* Defaults use Material Design specification */ 120 | --jp-ui-font-color0: rgba(0, 0, 0, 1); 121 | --jp-ui-font-color1: rgba(0, 0, 0, 0.87); 122 | --jp-ui-font-color2: rgba(0, 0, 0, 0.54); 123 | --jp-ui-font-color3: rgba(0, 0, 0, 0.38); 124 | 125 | /* 126 | * Use these against the brand/accent/warn/error colors. 127 | * These will typically go from light to darker, in both a dark and light theme. 128 | */ 129 | 130 | --jp-ui-inverse-font-color0: rgba(255, 255, 255, 1); 131 | --jp-ui-inverse-font-color1: rgba(255, 255, 255, 1); 132 | --jp-ui-inverse-font-color2: rgba(255, 255, 255, 0.7); 133 | --jp-ui-inverse-font-color3: rgba(255, 255, 255, 0.5); 134 | 135 | /* Content Fonts 136 | * 137 | * Content font variables are used for typography of user generated content. 138 | * 139 | * The font sizing here is done assuming that the body font size of --jp-content-font-size1 140 | * is applied to a parent element. When children elements, such as headings, are sized 141 | * in em all things will be computed relative to that body size. 142 | */ 143 | 144 | --jp-content-line-height: 1.6; 145 | --jp-content-font-scale-factor: 1.2; 146 | --jp-content-font-size0: 0.8333em; 147 | --jp-content-font-size1: 14px; /* Base font size */ 148 | --jp-content-font-size2: 1.2em; 149 | --jp-content-font-size3: 1.44em; 150 | --jp-content-font-size4: 1.728em; 151 | --jp-content-font-size5: 2.0736em; 152 | 153 | /* This gives a magnification of about 125% in presentation mode over normal. */ 154 | --jp-content-presentation-font-size1: 17px; 155 | --jp-content-heading-line-height: 1; 156 | --jp-content-heading-margin-top: 1.2em; 157 | --jp-content-heading-margin-bottom: 0.8em; 158 | --jp-content-heading-font-weight: 500; 159 | 160 | /* Defaults use Material Design specification */ 161 | --jp-content-font-color0: rgba(0, 0, 0, 1); 162 | --jp-content-font-color1: rgba(0, 0, 0, 0.87); 163 | --jp-content-font-color2: rgba(0, 0, 0, 0.54); 164 | --jp-content-font-color3: rgba(0, 0, 0, 0.38); 165 | --jp-content-link-color: var(--md-blue-700); 166 | --jp-content-font-family: -apple-system, blinkmacsystemfont, 'Segoe UI', 167 | helvetica, arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 168 | 'Segoe UI Symbol'; 169 | 170 | /* 171 | * Code Fonts 172 | * 173 | * Code font variables are used for typography of code and other monospaces content. 174 | */ 175 | 176 | --jp-code-font-size: 13px; 177 | --jp-code-line-height: 1.3077; /* 17px for 13px base */ 178 | --jp-code-padding: 0.385em; /* 5px for 13px base */ 179 | --jp-code-font-family-default: menlo, consolas, 'DejaVu Sans Mono', monospace; 180 | --jp-code-font-family: var(--jp-code-font-family-default); 181 | 182 | /* This gives a magnification of about 125% in presentation mode over normal. */ 183 | --jp-code-presentation-font-size: 16px; 184 | 185 | /* may need to tweak cursor width if you change font size */ 186 | --jp-code-cursor-width0: 1.4px; 187 | --jp-code-cursor-width1: 2px; 188 | --jp-code-cursor-width2: 4px; 189 | 190 | /* Layout 191 | * 192 | * The following are the main layout colors use in JupyterLab. In a light 193 | * theme these would go from light to dark. 194 | */ 195 | 196 | --jp-layout-color0: white; 197 | --jp-layout-color1: white; 198 | --jp-layout-color2: var(--md-grey-200); 199 | --jp-layout-color3: var(--md-grey-400); 200 | --jp-layout-color4: var(--md-grey-600); 201 | 202 | /* Inverse Layout 203 | * 204 | * The following are the inverse layout colors use in JupyterLab. In a light 205 | * theme these would go from dark to light. 206 | */ 207 | 208 | --jp-inverse-layout-color0: #111; 209 | --jp-inverse-layout-color1: var(--md-grey-900); 210 | --jp-inverse-layout-color2: var(--md-grey-800); 211 | --jp-inverse-layout-color3: var(--md-grey-700); 212 | --jp-inverse-layout-color4: var(--md-grey-600); 213 | 214 | /* Brand/accent */ 215 | 216 | --jp-brand-color0: #ec0c4b; 217 | --jp-brand-color1: #ed225d; 218 | --jp-brand-color2: #ee376b; 219 | --jp-brand-color3: #ee3b6e; 220 | --jp-accent-color0: var(--md-green-700); 221 | --jp-accent-color1: var(--md-green-500); 222 | --jp-accent-color2: var(--md-green-300); 223 | --jp-accent-color3: var(--md-green-100); 224 | 225 | /* State colors (warn, error, success, info) */ 226 | 227 | --jp-warn-color0: var(--md-orange-700); 228 | --jp-warn-color1: var(--md-orange-500); 229 | --jp-warn-color2: var(--md-orange-300); 230 | --jp-warn-color3: var(--md-orange-100); 231 | --jp-error-color0: var(--md-red-700); 232 | --jp-error-color1: var(--md-red-500); 233 | --jp-error-color2: var(--md-red-300); 234 | --jp-error-color3: var(--md-red-100); 235 | --jp-success-color0: var(--md-green-700); 236 | --jp-success-color1: var(--md-green-500); 237 | --jp-success-color2: var(--md-green-300); 238 | --jp-success-color3: var(--md-green-100); 239 | --jp-info-color0: var(--md-cyan-700); 240 | --jp-info-color1: var(--md-cyan-500); 241 | --jp-info-color2: var(--md-cyan-300); 242 | --jp-info-color3: var(--md-cyan-100); 243 | 244 | /* Cell specific styles */ 245 | 246 | --jp-cell-padding: 5px; 247 | --jp-cell-collapser-width: 8px; 248 | --jp-cell-collapser-min-height: 20px; 249 | --jp-cell-collapser-not-active-hover-opacity: 0.6; 250 | --jp-cell-editor-background: var(--md-grey-100); 251 | --jp-cell-editor-border-color: var(--md-grey-300); 252 | --jp-cell-editor-box-shadow: inset 0 0 2px var(--md-blue-300); 253 | --jp-cell-editor-active-background: var(--jp-layout-color0); 254 | --jp-cell-editor-active-border-color: var(--jp-brand-color1); 255 | --jp-cell-prompt-width: 64px; 256 | --jp-cell-prompt-font-family: 'Source Code Pro', monospace; 257 | --jp-cell-prompt-letter-spacing: 0; 258 | --jp-cell-prompt-opacity: 1; 259 | --jp-cell-prompt-not-active-opacity: 0.5; 260 | --jp-cell-prompt-not-active-font-color: var(--md-grey-700); 261 | 262 | /* A custom blend of MD grey and blue 600 263 | * See https://meyerweb.com/eric/tools/color-blend/#546E7A:1E88E5:5:hex */ 264 | --jp-cell-inprompt-font-color: #307fc1; 265 | 266 | /* A custom blend of MD grey and orange 600 267 | * https://meyerweb.com/eric/tools/color-blend/#546E7A:F4511E:5:hex */ 268 | --jp-cell-outprompt-font-color: #bf5b3d; 269 | 270 | /* Notebook specific styles */ 271 | 272 | --jp-notebook-padding: 10px; 273 | --jp-notebook-select-background: var(--jp-layout-color1); 274 | --jp-notebook-multiselected-color: var(--md-blue-50); 275 | 276 | /* The scroll padding is calculated to fill enough space at the bottom of the 277 | notebook to show one single-line cell (with appropriate padding) at the top 278 | when the notebook is scrolled all the way to the bottom. We also subtract one 279 | pixel so that no scrollbar appears if we have just one single-line cell in the 280 | notebook. This padding is to enable a 'scroll past end' feature in a notebook. 281 | */ 282 | --jp-notebook-scroll-padding: calc( 283 | 100% - var(--jp-code-font-size) * var(--jp-code-line-height) - 284 | var(--jp-code-padding) - var(--jp-cell-padding) - 1px 285 | ); 286 | 287 | /* Rendermime styles */ 288 | 289 | --jp-rendermime-error-background: #fdd; 290 | --jp-rendermime-table-row-background: var(--md-grey-100); 291 | --jp-rendermime-table-row-hover-background: var(--md-light-blue-50); 292 | 293 | /* Dialog specific styles */ 294 | 295 | --jp-dialog-background: rgba(0, 0, 0, 0.25); 296 | 297 | /* Console specific styles */ 298 | 299 | --jp-console-padding: 10px; 300 | 301 | /* Toolbar specific styles */ 302 | 303 | --jp-toolbar-border-color: var(--jp-border-color1); 304 | --jp-toolbar-micro-height: 8px; 305 | --jp-toolbar-background: var(--jp-layout-color1); 306 | --jp-toolbar-box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.24); 307 | --jp-toolbar-header-margin: 4px 4px 0 4px; 308 | --jp-toolbar-active-background: var(--md-grey-300); 309 | 310 | /* Statusbar specific styles */ 311 | 312 | --jp-statusbar-height: 24px; 313 | 314 | /* Input field styles */ 315 | 316 | --jp-input-box-shadow: inset 0 0 2px var(--md-blue-300); 317 | --jp-input-active-background: var(--jp-layout-color1); 318 | --jp-input-hover-background: var(--jp-layout-color1); 319 | --jp-input-background: var(--md-grey-100); 320 | --jp-input-border-color: var(--jp-border-color1); 321 | --jp-input-active-border-color: var(--jp-brand-color1); 322 | 323 | /* General editor styles */ 324 | 325 | --jp-editor-selected-background: #d9d9d9; 326 | --jp-editor-selected-focused-background: #d7d4f0; 327 | --jp-editor-cursor-color: var(--jp-ui-font-color0); 328 | 329 | /* Code mirror specific styles */ 330 | 331 | --jp-mirror-editor-keyword-color: #008000; 332 | --jp-mirror-editor-atom-color: #88f; 333 | --jp-mirror-editor-number-color: #080; 334 | --jp-mirror-editor-def-color: #00f; 335 | --jp-mirror-editor-variable-color: var(--md-grey-900); 336 | --jp-mirror-editor-variable-2-color: #05a; 337 | --jp-mirror-editor-variable-3-color: #085; 338 | --jp-mirror-editor-punctuation-color: #05a; 339 | --jp-mirror-editor-property-color: #05a; 340 | --jp-mirror-editor-operator-color: #a2f; 341 | --jp-mirror-editor-comment-color: #408080; 342 | --jp-mirror-editor-string-color: #ba2121; 343 | --jp-mirror-editor-string-2-color: #708; 344 | --jp-mirror-editor-meta-color: #a2f; 345 | --jp-mirror-editor-qualifier-color: #555; 346 | --jp-mirror-editor-builtin-color: #008000; 347 | --jp-mirror-editor-bracket-color: #997; 348 | --jp-mirror-editor-tag-color: #170; 349 | --jp-mirror-editor-attribute-color: #00c; 350 | --jp-mirror-editor-header-color: blue; 351 | --jp-mirror-editor-quote-color: #090; 352 | --jp-mirror-editor-link-color: #00c; 353 | --jp-mirror-editor-error-color: #f00; 354 | --jp-mirror-editor-hr-color: #999; 355 | 356 | /* User colors */ 357 | 358 | --jp-collaborator-color1: #ad4a00; 359 | --jp-collaborator-color2: #7b6a00; 360 | --jp-collaborator-color3: #007e00; 361 | --jp-collaborator-color4: #008772; 362 | --jp-collaborator-color5: #0079b9; 363 | --jp-collaborator-color6: #8b45c6; 364 | --jp-collaborator-color7: #be208b; 365 | 366 | /* File or activity icons and switch semantic variables */ 367 | 368 | --jp-jupyter-icon-color: var(--md-orange-900); 369 | --jp-notebook-icon-color: var(--md-orange-700); 370 | --jp-json-icon-color: var(--md-orange-700); 371 | --jp-console-icon-background-color: var(--md-blue-700); 372 | --jp-console-icon-color: white; 373 | --jp-terminal-icon-background-color: var(--md-grey-200); 374 | --jp-terminal-icon-color: var(--md-grey-800); 375 | --jp-text-editor-icon-color: var(--md-grey-200); 376 | --jp-inspector-icon-color: var(--md-grey-200); 377 | --jp-switch-color: var(--md-grey-400); 378 | --jp-switch-true-position-color: var(--md-orange-700); 379 | --jp-switch-cursor-color: rgba(0, 0, 0, 0.8); 380 | 381 | /* Vega extension styles */ 382 | 383 | --jp-vega-background: white; 384 | 385 | /* Sidebar-related styles */ 386 | 387 | --jp-sidebar-min-width: 180px; 388 | } 389 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "incremental": true, 8 | "jsx": "react", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "preserveWatchOutput": true, 15 | "resolveJsonModule": true, 16 | "outDir": "lib", 17 | "rootDir": "src", 18 | "strict": true, 19 | "strictNullChecks": true, 20 | "target": "ES2018", 21 | "types": [{% if cookiecutter.test.lower().startswith('y') %}"jest"{% endif %}] 22 | }, 23 | "include": ["src/*"] 24 | } 25 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/ui-tests/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/master/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 new experimental UI mode that you may fall in love with; 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 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/ui-tests/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 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/ui-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ cookiecutter.labextension_name }}-ui-tests", 3 | "version": "1.0.0", 4 | "description": "JupyterLab {{ cookiecutter.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.0", 13 | "@playwright/test": "^1.32.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/ui-tests/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 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/ui-tests/tests/{{cookiecutter.python_name}}.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jupyterlab/galata'; 2 | 3 | /** 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 {{ cookiecutter.labextension_name }} is activated!') 20 | ).toHaveLength(1); 21 | }); 22 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/ui-tests/yarn.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/extension-cookiecutter-ts/0cea7f59cad331fc11077cc44fc770e855b2ab08/{{cookiecutter.python_name}}/ui-tests/yarn.lock -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/{{cookiecutter.python_name}}/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__{% if cookiecutter.kind.lower() == 'server' %} 2 | from .handlers import setup_handlers{% endif %} 3 | 4 | 5 | def _jupyter_labextension_paths(): 6 | return [{ 7 | "src": "labextension", 8 | "dest": "{{ cookiecutter.labextension_name }}" 9 | }]{% if cookiecutter.kind.lower() == 'server' %} 10 | 11 | 12 | def _jupyter_server_extension_points(): 13 | return [{ 14 | "module": "{{ cookiecutter.python_name }}" 15 | }] 16 | 17 | 18 | def _load_jupyter_server_extension(server_app): 19 | """Registers the API handler to receive HTTP requests from the frontend extension. 20 | 21 | Parameters 22 | ---------- 23 | server_app: jupyterlab.labapp.LabApp 24 | JupyterLab application instance 25 | """ 26 | setup_handlers(server_app.web_app) 27 | name = "{{ cookiecutter.python_name }}" 28 | server_app.log.info(f"Registered {name} server extension") 29 | 30 | 31 | # For backward compatibility with notebook server - useful for Binder/JupyterHub 32 | load_jupyter_server_extension = _load_jupyter_server_extension{% endif %} 33 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/{{cookiecutter.python_name}}/handlers.py: -------------------------------------------------------------------------------- 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 /{{ cookiecutter.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, "{{ cookiecutter.python_name | replace('_', '-') }}", "get-example") 23 | handlers = [(route_pattern, RouteHandler)] 24 | web_app.add_handlers(host_pattern, handlers) 25 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/{{cookiecutter.python_name}}/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Python unit tests for {{ cookiecutter.python_name }}.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.python_name}}/{{cookiecutter.python_name}}/tests/test_handlers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | async def test_get_example(jp_fetch): 5 | # When 6 | response = await jp_fetch("{{ cookiecutter.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 /{{ cookiecutter.python_name | replace('_', '-') }}/get-example endpoint!" 13 | } --------------------------------------------------------------------------------