diff --git a/.github/.gitignore b/.github/.gitignore
new file mode 100644
index 00000000..2d19fc76
--- /dev/null
+++ b/.github/.gitignore
@@ -0,0 +1 @@
+*.html
diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml
new file mode 100644
index 00000000..17b0ce32
--- /dev/null
+++ b/.github/workflows/R-CMD-check.yaml
@@ -0,0 +1,31 @@
+# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
+# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
+on:
+ push:
+ branches: [main, master]
+ paths:
+ - "pkg-r/**"
+ - ".github/workflows/R-CMD-check.yaml"
+ pull_request:
+ paths:
+ - "pkg-r/**"
+ - ".github/workflows/R-CMD-check.yaml"
+
+name: R-CMD-check.yaml
+
+permissions: read-all
+
+jobs:
+ routine:
+ uses: rstudio/shiny-workflows/.github/workflows/routine.yaml@v1
+ permissions:
+ contents: write
+ with:
+ working-directory: pkg-r
+ format-r-code: true
+ check-js: false
+ R-CMD-check:
+ uses: rstudio/shiny-workflows/.github/workflows/R-CMD-check.yaml@v1
+ with:
+ working-directory: pkg-r
+ extra-check-build-args: '"--compact-vignettes=gs+qpdf"'
diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml
new file mode 100644
index 00000000..027924a2
--- /dev/null
+++ b/.github/workflows/pkgdown.yaml
@@ -0,0 +1,66 @@
+# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
+# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
+on:
+ push:
+ branches: [main, master]
+ paths:
+ - 'pkg-r/man/**/*'
+ - 'pkg-r/vignettes/**/*'
+ - 'pkg-r/pkgdown/**/*'
+ - '.github/workflows/pkgdown.yaml'
+ pull_request:
+ paths:
+ - 'pkg-r/man/**/*'
+ - 'pkg-r/vignettes/**/*'
+ - 'pkg-r/pkgdown/**/*'
+ - '.github/workflows/pkgdown.yaml'
+ release:
+ types: [published]
+ workflow_dispatch:
+
+name: pkgdown.yaml
+
+permissions:
+ contents: write
+
+jobs:
+ pkgdown:
+ runs-on: ubuntu-latest
+ # Only restrict concurrency for non-PR jobs
+ concurrency:
+ group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }}
+ env:
+ GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
+ permissions:
+ contents: write
+
+ # Only run on release events for tags start with "r/v*"
+ if: github.event_name != 'release' || startsWith(github.ref, 'refs/tags/r/v')
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: r-lib/actions/setup-pandoc@v2
+
+ - uses: r-lib/actions/setup-r@v2
+ with:
+ use-public-rspm: true
+
+ - uses: r-lib/actions/setup-r-dependencies@v2
+ with:
+ extra-packages: any::pkgdown, local::.
+ needs: website
+ working-directory: pkg-r
+
+ - name: Build site
+ working-directory: pkg-r
+ shell: Rscript {0}
+ run: pkgdown::build_site()
+
+ - name: Deploy to GitHub pages ๐
+ if: github.event_name != 'pull_request'
+ uses: JamesIves/github-pages-deploy-action@v4.5.0
+ with:
+ clean: false
+ branch: gh-pages
+ folder: docs
diff --git a/.github/workflows/py-check.yaml b/.github/workflows/py-check.yaml
new file mode 100644
index 00000000..34e173dc
--- /dev/null
+++ b/.github/workflows/py-check.yaml
@@ -0,0 +1,59 @@
+name: py-check.yaml
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'pkg-py/**/*'
+ - 'pyproject.toml'
+ - '.github/workflows/py-check.yaml'
+ pull_request:
+ paths:
+ - 'pkg-py/**/*'
+ - 'pyproject.toml'
+ - '.github/workflows/py-check.yaml'
+
+permissions:
+ contents: read
+
+env:
+ UV_VERSION: "0.9.x"
+
+jobs:
+ py-test:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version:
+ - "3.10"
+ - "3.11"
+ - "3.12"
+ - "3.13"
+ - "3.14"
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: ๐ Install uv
+ uses: astral-sh/setup-uv@v6.1.0
+ with:
+ version: ${{ env.UV_VERSION }}
+
+ - name: ๐ Set up Python ${{ matrix.python-version }}
+ run: uv python install ${{ matrix.python-version }}
+
+ - name: ๐ฆ Install the project
+ run: uv sync --python ${{ matrix.python-version }} --all-extras --all-groups
+
+ - name: ๐ Show uv.lock
+ run: cat uv.lock
+
+ - name: ๐งช Check tests
+ run: make py-check-tests
+
+ - name: ๐ Check types
+ run: make py-check-types
+
+ - name: ๐ Check formatting
+ run: make py-check-format
diff --git a/.github/workflows/py-release.yaml b/.github/workflows/py-release.yaml
new file mode 100644
index 00000000..ba56b0d9
--- /dev/null
+++ b/.github/workflows/py-release.yaml
@@ -0,0 +1,54 @@
+name: py-release.yaml
+
+on:
+ release:
+ types: [published]
+
+env:
+ UV_VERSION: "0.9.x"
+ PYTHON_VERSION: 3.13
+
+jobs:
+ py-release:
+ name: Build and release Python package
+ runs-on: ubuntu-latest
+
+ # Python release tags start with "py/v*"
+ if: startsWith(github.ref, 'refs/tags/py/v')
+
+ environment:
+ name: pypi
+ url: https://pypi.org/project/shinyjson/
+
+ permissions: # for trusted publishing
+ id-token: write
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: ๐ Install uv
+ uses: astral-sh/setup-uv@v3
+ with:
+ version: ${{ env.UV_VERSION }}
+
+ - name: ๐ Set up Python ${{ env.PYTHON_VERSION }}
+ run: uv python install ${{ env.PYTHON_VERSION }}
+
+ - name: ๐ฆ Install the project
+ run: uv sync --python ${{ env.PYTHON_VERSION }} --all-extras --all-groups
+
+ - name: ๐ Check types
+ run: make py-check-types
+
+ - name: ๐ Check formatting
+ run: make py-check-format
+
+ - name: ๐งช Check tests
+ run: make py-check-tests
+
+ - name: ๐งณ Build package
+ run: uv build
+
+ # TODO: https://pypi.org/manage/project/shinyjson/settings/publishing/
+ - name: ๐ข Publish release on PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.github/workflows/quartodoc.yaml b/.github/workflows/quartodoc.yaml
new file mode 100644
index 00000000..ad416bd3
--- /dev/null
+++ b/.github/workflows/quartodoc.yaml
@@ -0,0 +1,64 @@
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - "docs/**/*"
+ - "pkg-py/**/*"
+ - .github/workflows/quartodoc.yaml
+ workflow_dispatch:
+
+name: quartodoc.yaml
+
+permissions:
+ contents: write
+
+env:
+ UV_VERSION: "0.9.x"
+ PYTHON_VERSION: 3.13
+ QUARTO_VERSION: 1.7.31
+
+jobs:
+ docs-publish:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # full history needed for correct versioning of py pkg
+
+ - name: ๐ต Set up Quarto
+ uses: quarto-dev/quarto-actions/setup@v2
+ with:
+ version: ${{ env.QUARTO_VERSION }}
+
+ - name: ๐ Install uv
+ uses: astral-sh/setup-uv@v6.1.0
+ with:
+ version: ${{ env.UV_VERSION }}
+
+ - name: ๐ Set up Python ${{ env.PYTHON_VERSION }}
+ run: uv python install ${{ env.PYTHON_VERSION }}
+
+ - name: ๐ฆ Install shinyjson and dependencies
+ run: uv sync --python ${{ env.PYTHON_VERSION }} --all-extras --all-groups
+
+ - name: ๐ Activate venv
+ run: |
+ source .venv/bin/activate
+ echo "$VIRTUAL_ENV/bin" >> $GITHUB_PATH
+ echo "VIRTUAL_ENV=$VIRTUAL_ENV" >> $GITHUB_ENV
+
+ - name: ๐ญ Update Python docs
+ run: |
+ make py-docs-api
+ quarto render pkg-py/docs
+
+ - name: ๐ข Deploy to GitHub pages
+ if: github.event_name != 'pull_request'
+ uses: JamesIves/github-pages-deploy-action@v4.5.0
+ with:
+ clean: false
+ folder: docs
+ branch: gh-pages
diff --git a/.github/workflows/verify-js-built.yaml b/.github/workflows/verify-js-built.yaml
new file mode 100644
index 00000000..41983b0b
--- /dev/null
+++ b/.github/workflows/verify-js-built.yaml
@@ -0,0 +1,53 @@
+name: verify-js-built.yaml
+
+on:
+ push:
+ branches: ["main", "rc-*"]
+ paths:
+ - "js/**/*"
+ - .github/workflows/verify-js-built.yaml
+ pull_request:
+ paths:
+ - "js/**/*"
+ - .github/workflows/verify-js-built.yaml
+
+jobs:
+ verify_js_built:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: ๐ Extract node version from .nvmrc
+ id: nvm
+ working-directory: js
+ run: |
+ echo "version=$(cat .nvmrc)" >> $GITHUB_OUTPUT
+
+ - name: ๐ก Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "${{ steps.nvm.outputs.version }}"
+
+ - name: ๐ Install dependencies
+ run: make js-setup-ci
+ env:
+ NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
+
+ - name: ๐ญ Build
+ run: make js-build
+
+ - name: ๐ง Check for uncommitted changes
+ run: |
+ if [[ `git status --porcelain` ]]; then
+ git diff
+ echo "Uncommitted changes found. Please commit any changes that result from 'make js-setup-ci js-build'."
+ exit 1
+ else
+ echo "No uncommitted changes found."
+ fi
+ working-directory: js
diff --git a/.gitignore b/.gitignore
index b7faf403..803b31b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,207 +1,14 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[codz]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py.cover
-.hypothesis/
-.pytest_cache/
-cover/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-.pybuilder/
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-# For a library or package, you might want to ignore these files since the code is
-# intended to run in multiple environments; otherwise, check them in:
-# .python-version
-
-# pipenv
-# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-# However, in case of collaboration, if having platform-specific dependencies or dependencies
-# having no cross-platform support, pipenv may install dependencies that don't work, or not
-# install all needed dependencies.
-#Pipfile.lock
-
-# UV
-# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-#uv.lock
-
-# poetry
-# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
-#poetry.lock
-#poetry.toml
-
-# pdm
-# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
-# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
-# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
-#pdm.lock
-#pdm.toml
-.pdm-python
-.pdm-build/
-
-# pixi
-# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
-#pixi.lock
-# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
-# in the .venv directory. It is recommended not to include this directory in version control.
-.pixi
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.envrc
+.Rproj.user
+.Rhistory
+.RData
+.Ruserdata
+.Renviron
.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
-
-# PyCharm
-# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-# and can be added to the global gitignore or merged into this file. For a more nuclear
-# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
-
-# Abstra
-# Abstra is an AI-powered process automation framework.
-# Ignore directories containing user credentials, local state, and settings.
-# Learn more at https://abstra.io/docs
-.abstra/
-
-# Visual Studio Code
-# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
-# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
-# and can be added to the global gitignore or merged into this file. However, if you prefer,
-# you could uncomment the following to ignore the entire vscode folder
-# .vscode/
-
-# Ruff stuff:
-.ruff_cache/
-
-# PyPI configuration file
-.pypirc
-
-# Cursor
-# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
-# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
-# refer to https://docs.cursor.com/context/ignore-files
-.cursorignore
-.cursorindexingignore
-
-# Marimo
-marimo/_static/
-marimo/_lsp/
-__marimo__/
+docs/r
+docs/py
+_dev
+uv.lock
+.coverage
+README.html
+README_files
+shiny_bookmarks
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..12e7953b
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+# MIT License
+
+Copyright (c) 2024 Posit Software, PBC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..3ff7758e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,236 @@
+# Use qvm to manage quarto
+QUARTO_VERSION ?= 1.7.31
+QUARTO_PATH = ~/.local/share/qvm/versions/v${QUARTO_VERSION}/bin/quarto
+PATH_PKG_R := pkg-r
+PATH_PKG_PY := pkg-py
+PATH_PKG_JS := js
+
+.PHONY: install-quarto
+install-quarto:
+ @echo "๐ต Installing quarto"
+ @if ! [ -z $(command -v qvm)]; then \
+ @echo "Error: qvm is not installed. Please visit https://github.com/dpastoor/qvm/releases/ to install it." >&2 \
+ exit 1; \
+ fi
+ qvm install v${QUARTO_VERSION}
+ @echo "๐น Updating .vscode/settings.json"
+ @awk -v path="${QUARTO_PATH}" '/"quarto.path":/ {gsub(/"quarto.path": ".*"/, "\"quarto.path\": \"" path "\"")} 1' .vscode/settings.json > .vscode/settings.json.tmp && mv .vscode/settings.json.tmp .vscode/settings.json
+ @echo "๐น Updating .github/workflows/quartodoc.yaml"
+ @awk -v ver="${QUARTO_VERSION}" '/QUARTO_VERSION:/ {gsub(/QUARTO_VERSION: .*/, "QUARTO_VERSION: " ver)} 1' .github/workflows/quartodoc.yaml > .github/workflows/quartodoc.yaml.tmp && mv .github/workflows/quartodoc.yaml.tmp .github/workflows/quartodoc.yaml
+
+
+.PHONY: docs
+docs: r-docs-render py-docs-render ## [docs] Build the documentation
+
+.PHONY: docs-preview
+docs-preview: ## [docs] Preview the documentation
+ @npx http-server docs -p 8080
+
+.PHONY: js-setup
+js-setup: ## [js] Install JS dependencies
+ @echo "๐ Setup JS dependencies"
+ cd $(PATH_PKG_JS) && npm install
+
+.PHONY: js-setup-ci
+js-setup-ci: ## [js] Install JS dependencies as CI
+ @echo "๐ Setup JS dependencies"
+ cd $(PATH_PKG_JS) && npm ci
+
+.PHONY: js-lint
+js-lint: ## [js] Lint JS code
+ @echo "๐ Linting JS code"
+ cd $(PATH_PKG_JS) && npm run lint
+
+.PHONY: js-build
+js-build: ## [js] Build JS code
+ @echo "๐งณ Building JS code"
+ cd $(PATH_PKG_JS) && npm run build
+
+.PHONY: js-build-watch
+js-build-watch: ## [js] Build JS code in watch mode
+ @echo "๐งณ Building JS code in watch mode"
+ cd $(PATH_PKG_JS) && npm run watch
+
+.PHONY: update-dist
+update-dist: js-build r-update-dist py-update-dist ## Update shinyjson web assets in all packages
+
+.PHONY: r-setup
+r-setup: ## [r] Install R dependencies
+ @echo "๐ Updating R dependencies"
+ cd $(PATH_PKG_R) && Rscript -e "pak::local_install_dev_deps()"
+
+.PHONY: r-check
+r-check: r-check-format r-check-tests r-check-package ## [r] All R checks
+
+.PHONY: r-document
+r-document: ## [r] Document package
+ @echo "๐ Documenting R package"
+ cd $(PATH_PKG_R) && Rscript -e "devtools::document()"
+
+.PHONY: r-format
+r-format: ## [r] Format R code
+ air format $(PATH_PKG_R)/
+
+.PHONY: r-check-package
+r-check-package: ## [r] Check package
+ @echo ""
+ @echo "๐ Running R CMD Check"
+ cd $(PATH_PKG_R) && Rscript -e "devtools::check(document = FALSE)"
+
+.PHONY: r-check-tests
+r-check-tests: ## [r] Check tests
+ @echo ""
+ @echo "๐งช Running R tests"
+ cd $(PATH_PKG_R) && Rscript -e "devtools::test()"
+
+.PHONY: r-check-format
+r-check-format: ## [r] Check format
+ @echo ""
+ @echo "๐ Checking R format"
+ air format --check $(PATH_PKG_R)/
+
+.PHONY: r-update-dist
+r-update-dist: ## [r] Update shinyjson web assets
+ @echo ""
+ @echo "๐ Updating shinyjson web assets"
+ if [ -d $(PATH_PKG_R)/inst/lib/shiny ]; then \
+ rm -rf $(PATH_PKG_R)/inst/lib/shiny; \
+ fi
+ mkdir -p $(PATH_PKG_R)/inst/lib/shiny
+ cp -r $(PATH_PKG_JS)/dist/shinyjson $(PATH_PKG_R)/inst/lib/shiny/
+ (git rev-parse HEAD) > "$(PATH_PKG_R)/inst/lib/shiny/GIT_VERSION"
+
+.PHONY: r-docs-render
+r-docs-render: ## [r] Build R docs
+ @echo "๐ Rendering R docs with pkgdown"
+ cd $(PATH_PKG_R) && Rscript -e "pkgdown::build_site()"
+
+.PHONY: r-docs-preview
+r-docs-preview: ## [r] Build R docs
+ @echo "๐ Rendering R docs with pkgdown"
+ cd $(PATH_PKG_R) && Rscript -e "pkgdown::preview_site()"
+
+.PHONY: py-setup
+py-setup: ## [py] Setup python environment
+ uv sync --all-extras --all-groups --upgrade
+
+.PHONY: py-check
+py-check: py-check-format py-check-types py-check-tests ## [py] Run python checks
+
+.PHONY: py-check-tox
+py-check-tox: ## [py] Run python checks across versions with tox
+ @echo ""
+ @echo "๐ Running tests and type checking with tox for Python 3.10--3.14"
+ uv run tox run-parallel
+
+.PHONY: py-check-tests
+py-check-tests: ## [py] Run python tests
+ @echo ""
+ @echo "๐งช Running tests with pytest"
+ uv run playwright install
+ uv run pytest
+
+.PHONY: py-check-types
+py-check-types: ## [py] Run python type checks
+ @echo ""
+ @echo "๐ Checking types with pyright"
+ uv run pyright
+
+.PHONY: py-check-format
+py-check-format:
+ @echo ""
+ @echo "๐ Checking format with ruff"
+ uv run ruff check pkg-py --config pyproject.toml
+
+.PHONY: py-format
+py-format: ## [py] Format python code
+ uv run ruff check --fix pkg-py --config pyproject.toml
+ uv run ruff format pkg-py --config pyproject.toml
+
+.PHONY: py-coverage
+py-coverage: ## [py] Generate coverage report
+ @echo "๐ Generating coverage report"
+ uv run coverage run -m pytest
+ uv run coverage report
+
+.PHONY: py-coverage-report
+py-coverage-report: py-coverage ## [py] Generate coverage report and open it in browser
+ uv run coverage html
+ @echo ""
+ @echo "๐ก Serving coverage report at http://localhost:8081/"
+ @npx http-server htmlcov --silent -p 8081
+
+.PHONY: py-update-snaps
+py-update-snaps: ## [py] Update python test snapshots
+ @echo "๐ธ Updating pytest snapshots"
+ uv run pytest --snapshot-update
+
+.PHONY: py-docs
+py-docs: py-docs-api py-docs-render ## [py] Build python docs
+
+.PHONY: py-docs-render
+py-docs-render: ## [py] Render python docs
+ @echo "๐ Rendering python docs with quarto"
+ @$(eval export IN_QUARTODOC=true)
+ ${QUARTO_PATH} render pkg-py/docs
+
+.PHONY: py-docs-preview
+py-docs-preview: ## [py] Preview python docs
+ @echo "๐ Rendering python docs with quarto"
+ @$(eval export IN_QUARTODOC=true)
+ ${QUARTO_PATH} preview pkg-py/docs
+
+.PHONY: py-docs-api
+py-docs-api: ## [py] Update python API docs
+ @echo "๐ Generating python docs with quartodoc"
+ @$(eval export IN_QUARTODOC=true)
+ cd pkg-py/docs && uv run quartodoc build
+ cd pkg-py/docs && uv run quartodoc interlinks
+
+.PHONY: py-docs-api-watch
+py-docs-api-watch: ## [py] Update python docs
+ @echo "๐ Generating python docs with quartodoc"
+ @$(eval export IN_QUARTODOC=true)
+ uv run quartodoc build --config pkg-py/docs/_quarto.yml --watch
+
+.PHONY: py-docs-clean
+py-docs-clean: ## [py] Clean python docs
+ @echo "๐งน Cleaning python docs"
+ rm -r pkg-py/docs/api
+ find pkg-py/docs/py -name '*.quarto_ipynb' -delete
+
+.PHONY: py-build
+py-build: ## [py] Build python package
+ @echo "๐งณ Building python package"
+ @[ -d dist ] && rm -r dist || true
+ uv build
+
+.PHONY: py-update-dist
+py-update-dist: ## [py] Update shinyjson web assets
+ @echo ""
+ @echo "๐ Updating shinyjson web assets"
+ if [ -d $(PATH_PKG_PY)/src/shinyjson/www ]; then \
+ rm -rf $(PATH_PKG_PY)/src/shinyjson/www; \
+ fi
+ mkdir -p $(PATH_PKG_PY)/src/shinyjson/www
+ cp -r $(PATH_PKG_JS)/dist/shinyjson $(PATH_PKG_PY)/src/shinyjson/www/
+ (git rev-parse HEAD) > "$(PATH_PKG_PY)/src/shinyjson/www/GIT_VERSION"
+
+.PHONY: help
+help: ## Show help messages for make targets
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; { \
+ printf "\033[32m%-18s\033[0m", $$1; \
+ if ($$2 ~ /^\[docs\]/) { \
+ printf "\033[37m[docs]\033[0m%s\n", substr($$2, 7); \
+ } else if ($$2 ~ /^\[py\]/) { \
+ printf " \033[31m[py]\033[0m%s\n", substr($$2, 5); \
+ } else if ($$2 ~ /^\[r\]/) { \
+ printf " \033[34m[r]\033[0m%s\n", substr($$2, 4); \
+ } else if ($$2 ~ /^\[js\]/) { \
+ printf " \033[33m[js]\033[0m%s\n", substr($$2, 5); \
+ } else { \
+ printf " %s\n", $$2; \
+ } \
+ }'
+
+.DEFAULT_GOAL := help
diff --git a/README.md b/README.md
index 91c6cac0..cc3e67a0 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,13 @@
# shinyjson
-Shiny UI using vercel-labs/json-render
+
+
+[](https://github.com/posit-dev/shinyjson/actions/workflows/R-CMD-check.yaml)
+[](https://pypi.org/project/shinyjson/)
+[](https://github.com/posit-dev/shinyjson/actions/workflows/py-check.yaml)
+
+
+Shiny UI using vercel-labs/json-render.
+
+* ๐ [shinyjson for Python](https://posit-dev.github.io/shinyjson/py/)
+
+* ๐ [shinyjson for R](https://posit-dev.github.io/shinyjson/r/)
diff --git a/js/.eslintrc.js b/js/.eslintrc.js
new file mode 100644
index 00000000..7a389616
--- /dev/null
+++ b/js/.eslintrc.js
@@ -0,0 +1,31 @@
+/* eslint-disable no-undef */
+module.exports = {
+ env: {
+ browser: true,
+ es2021: true,
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:react/recommended",
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:react-hooks/recommended",
+ ],
+ ignorePatterns: ["dist/*"],
+ overrides: [],
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ },
+ plugins: ["react", "@typescript-eslint"],
+ rules: {
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ },
+ settings: {
+ react: {
+ version: "detect",
+ },
+ },
+}
diff --git a/js/.gitignore b/js/.gitignore
new file mode 100644
index 00000000..506fcd25
--- /dev/null
+++ b/js/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+esbuild-metadata.json
diff --git a/js/.nvmrc b/js/.nvmrc
new file mode 100644
index 00000000..3f430af8
--- /dev/null
+++ b/js/.nvmrc
@@ -0,0 +1 @@
+v18
diff --git a/js/.prettierrc b/js/.prettierrc
new file mode 100644
index 00000000..6879b154
--- /dev/null
+++ b/js/.prettierrc
@@ -0,0 +1,14 @@
+{
+ "organizeImportsSkipDestructiveCodeActions": true,
+ "singleQuote": false,
+ "semi": false,
+ "trailingComma": "all",
+ "overrides": [
+ {
+ "files": "**/*.scss",
+ "options": {
+ "printWidth": 150
+ }
+ }
+ ]
+}
diff --git a/js/build.ts b/js/build.ts
new file mode 100644
index 00000000..5b81b2b5
--- /dev/null
+++ b/js/build.ts
@@ -0,0 +1,143 @@
+import { BuildOptions, build, type Metafile } from "esbuild"
+import { sassPlugin } from "esbuild-sass-plugin"
+import * as fs from "node:fs/promises"
+
+// Parse command line arguments
+const args = Object.fromEntries(
+ process.argv
+ .filter((arg) => arg.startsWith("--"))
+ .map((arg) => {
+ const [key, value] = arg.substring(2).split("=")
+ return [key, value || "true"]
+ }),
+)
+
+const minify = args.minify !== "false"
+const metafile = args.metafile !== "false"
+
+if (!minify) console.log("Disabling minification")
+if (!metafile) console.log("Disabling metafile generation")
+
+const outDir = "dist"
+
+const allEsbuildMetadata: Array = []
+
+function mergeMetadatas(metadatas: Array): Metafile {
+ // Merge all the metafile objects together
+ const mergedMetadata: Metafile = {
+ inputs: {},
+ outputs: {},
+ }
+
+ metadatas.forEach((metafile) => {
+ Object.entries(metafile.inputs).forEach(([key, value]) => {
+ if (
+ mergedMetadata.inputs[key] &&
+ JSON.stringify(mergedMetadata.inputs[key]) !== JSON.stringify(value)
+ ) {
+ // It's very possible for multiple MetaFile objects to refer to the same input.
+ // But if that input has different values in the different Metafile objects,
+ // that could cause inaccuracies when we merge them. I think it's possible they
+ // could have different values if tree-shaking is enabled -- this will detect
+ // those cases and warn the user, and if it happens we can figure out how to
+ // handle it.
+ console.error(
+ `Different values found for key in metadata: ${key}. Overwriting.`,
+ )
+ }
+ mergedMetadata.inputs[key] = value
+ })
+ Object.entries(metafile.outputs).forEach(([key, value]) => {
+ if (mergedMetadata.outputs[key]) {
+ console.error(`Duplicate key found in metadata: ${key}. Overwriting.`)
+ }
+ mergedMetadata.outputs[key] = value
+ })
+ })
+
+ return mergedMetadata
+}
+
+async function bundle_helper(
+ options: BuildOptions,
+): Promise | undefined> {
+ try {
+ const result = await build({
+ format: "esm",
+ bundle: true,
+ minify,
+ // No need to clean up old source maps, as `minify==false` only during `npm run watch-fast`
+ // GHA will run `npm run build` which will minify
+ sourcemap: minify,
+ metafile,
+ outdir: outDir,
+ ...options,
+ })
+
+ for (const [output_file_stub] of Object.entries(
+ options.entryPoints as Record,
+ )) {
+ console.log(`Building ${output_file_stub}.js completed successfully!`)
+ }
+
+ if (result.metafile) {
+ allEsbuildMetadata.push(result.metafile)
+ }
+
+ return result
+ } catch (error) {
+ console.error("Build failed:", error)
+ process.exit(1) // Exit with error code to fail CI/CD pipelines
+ }
+}
+
+interface EntryConfig {
+ name: string
+ jsEntry?: string
+ sassEntry?: string
+}
+
+async function bundleEntry({
+ name,
+ jsEntry,
+ sassEntry,
+}: EntryConfig): Promise {
+ const tasks = []
+
+ if (jsEntry) {
+ tasks.push(
+ bundle_helper({
+ entryPoints: { [name]: jsEntry },
+ }),
+ )
+ }
+
+ if (sassEntry) {
+ tasks.push(
+ bundle_helper({
+ entryPoints: { [name]: sassEntry },
+ plugins: [sassPlugin({ type: "css", sourceMap: false })],
+ }),
+ )
+ }
+
+ await Promise.all(tasks)
+}
+
+const entries: EntryConfig[] = [
+ {
+ name: "shinyjson/shinyjson",
+ jsEntry: "src/shinyjson/shinyjson.ts",
+ sassEntry: "src/shinyjson/shinyjson.scss",
+ },
+]
+
+;(async () => {
+ await Promise.all(entries.map(bundleEntry))
+
+ if (metafile && allEsbuildMetadata.length > 0) {
+ const mergedMetadata = mergeMetadatas(allEsbuildMetadata)
+ await fs.writeFile("esbuild-metadata.json", JSON.stringify(mergedMetadata))
+ console.log("Metadata file written to esbuild-metadata.json")
+ }
+})()
diff --git a/js/eslint.config.js b/js/eslint.config.js
new file mode 100644
index 00000000..b4b709fe
--- /dev/null
+++ b/js/eslint.config.js
@@ -0,0 +1,65 @@
+import { defineConfig, globalIgnores } from "eslint/config"
+import globals from "globals"
+import { fixupConfigRules, fixupPluginRules } from "@eslint/compat"
+import tsParser from "@typescript-eslint/parser"
+import react from "eslint-plugin-react"
+import typescriptEslint from "@typescript-eslint/eslint-plugin"
+
+import { fileURLToPath } from "url"
+import { dirname } from "path"
+import { FlatCompat } from "@eslint/eslintrc"
+import js from "@eslint/js"
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
+
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+ recommendedConfig: js.configs.recommended,
+ allConfig: js.configs.all,
+})
+
+export default defineConfig([
+ {
+ languageOptions: {
+ globals: {
+ ...globals.browser,
+ },
+
+ parser: tsParser,
+ ecmaVersion: "latest",
+ sourceType: "module",
+ parserOptions: {},
+ },
+
+ extends: fixupConfigRules(
+ compat.extends(
+ "eslint:recommended",
+ "plugin:react/recommended",
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:react-hooks/recommended",
+ "plugin:prettier/recommended",
+ "prettier",
+ ),
+ ),
+
+ plugins: {
+ react: fixupPluginRules(react),
+ "@typescript-eslint": fixupPluginRules(typescriptEslint),
+ },
+
+ rules: {
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "prettier/prettier": "error",
+ },
+
+ settings: {
+ react: {
+ version: "detect",
+ },
+ },
+ },
+ globalIgnores(["dist/*"]),
+])
diff --git a/js/package.json b/js/package.json
new file mode 100644
index 00000000..9257699d
--- /dev/null
+++ b/js/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "shinyjson",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "description": "Shiny UI using vercel-labs/json-render",
+ "homepage": "https://posit-dev.github.io/shinyjson/",
+ "bugs": {
+ "url": "https://github.com/posit-dev/shinyjson/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/posit-dev/shinyjson.git"
+ },
+ "license": "MIT",
+ "scripts": {
+ "lint": "tsc -noEmit && eslint .",
+ "bundle": "tsx build.ts",
+ "build": "npm run lint && npm run bundle",
+ "watch": "npx nodemon --exec 'npm run build' --ext '*' --ignore dist/ --ignore esbuild-metadata.json",
+ "build-fast": "tsx build.ts --minify=false",
+ "watch-fast": "npx nodemon --exec 'npm run build-fast' --ext '*' --ignore dist/ --ignore esbuild-metadata.json"
+ },
+ "dependencies": {
+ "lit": "^3.3.0"
+ },
+ "devDependencies": {
+ "@eslint/compat": "^1.2.9",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "^9.27.0",
+ "@types/node": "^22.15.21",
+ "@types/rstudio-shiny": "git+https://git@github.com/rstudio/shiny.git#v1.10.0",
+ "@typescript-eslint/eslint-plugin": "^8.32.1",
+ "@typescript-eslint/parser": "^8.32.1",
+ "esbuild": "^0.25.4",
+ "esbuild-sass-plugin": "^3.3.1",
+ "eslint-config-prettier": "^10.1.5",
+ "eslint-plugin-prettier": "^5.4.0",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "globals": "^16.1.0",
+ "prettier": "^3.5.3",
+ "tsx": "^4.19.4",
+ "typescript": "^5.8.3"
+ }
+}
diff --git a/js/src/shinyjson/shinyjson.scss b/js/src/shinyjson/shinyjson.scss
new file mode 100644
index 00000000..672dd012
--- /dev/null
+++ b/js/src/shinyjson/shinyjson.scss
@@ -0,0 +1,4 @@
+// Hello, World! - shinyjson
+.shinyjson {
+ display: block;
+}
diff --git a/js/src/shinyjson/shinyjson.ts b/js/src/shinyjson/shinyjson.ts
new file mode 100644
index 00000000..fd386162
--- /dev/null
+++ b/js/src/shinyjson/shinyjson.ts
@@ -0,0 +1,4 @@
+// Hello, World! - shinyjson
+export function helloWorld(): string {
+ return "Hello, World!"
+}
diff --git a/js/tsconfig.json b/js/tsconfig.json
new file mode 100644
index 00000000..0721a336
--- /dev/null
+++ b/js/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "module": "ES2022",
+ "lib": ["ES2022", "dom", "dom.iterable"],
+ "jsx": "react",
+ "noImplicitAny": true,
+ "removeComments": true,
+ "preserveConstEnums": true,
+ "sourceMap": true,
+ "moduleResolution": "node",
+ "noEmit": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "target": "ES2022",
+ "allowJs": true,
+ "resolveJsonModule": true,
+ "moduleDetection": "force",
+ "isolatedModules": true,
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+ "experimentalDecorators": true,
+ "useDefineForClassFields": false,
+ },
+ "include": ["**/*.tsx", "**/*.ts"],
+}
diff --git a/pkg-py/.gitignore b/pkg-py/.gitignore
new file mode 100644
index 00000000..1f82c9f6
--- /dev/null
+++ b/pkg-py/.gitignore
@@ -0,0 +1,3 @@
+src/shinyjson/__version.py
+
+__pycache__/
diff --git a/pkg-py/CHANGELOG.md b/pkg-py/CHANGELOG.md
new file mode 100644
index 00000000..d3c03caa
--- /dev/null
+++ b/pkg-py/CHANGELOG.md
@@ -0,0 +1,7 @@
+# Changelog
+
+All notable changes to the shinyjson Python package will be documented in this file.
+
+## Unreleased
+
+- Initial release of shinyjson.
diff --git a/pkg-py/README.md b/pkg-py/README.md
new file mode 100644
index 00000000..586145d9
--- /dev/null
+++ b/pkg-py/README.md
@@ -0,0 +1,21 @@
+# shinyjson
+
+> Shiny UI using vercel-labs/json-render.
+
+## Installation
+
+```bash
+pip install shinyjson
+```
+
+## Usage
+
+```python
+from shinyjson import hello_world
+
+print(hello_world())
+```
+
+## Documentation
+
+- [shinyjson for Python](https://posit-dev.github.io/shinyjson/py/)
diff --git a/pkg-py/src/shinyjson/__init__.py b/pkg-py/src/shinyjson/__init__.py
new file mode 100644
index 00000000..2380ed66
--- /dev/null
+++ b/pkg-py/src/shinyjson/__init__.py
@@ -0,0 +1,11 @@
+"""shinyjson: Shiny UI using vercel-labs/json-render."""
+
+
+def hello_world() -> str:
+ """Return a Hello, World! greeting."""
+ return "Hello, World!"
+
+
+__all__ = [
+ "hello_world",
+]
diff --git a/pkg-py/src/shinyjson/py.typed b/pkg-py/src/shinyjson/py.typed
new file mode 100644
index 00000000..e69de29b
diff --git a/pkg-r/.Rbuildignore b/pkg-r/.Rbuildignore
new file mode 100644
index 00000000..6574a819
--- /dev/null
+++ b/pkg-r/.Rbuildignore
@@ -0,0 +1,17 @@
+^.*\.Rproj$
+^\.Rproj\.user$
+^LICENSE\.md$
+^scripts$
+^\.github$
+^README\.Rmd$
+^_pkgdown\.yml$
+^docs$
+^pkgdown$
+^vignettes*
+^_dev$
+^cran-comments\.md$
+^CRAN-SUBMISSION$
+^_dev$
+^revdep$
+^shiny_bookmarks$
+^vignettes/articles$
diff --git a/pkg-r/DESCRIPTION b/pkg-r/DESCRIPTION
new file mode 100644
index 00000000..7b566423
--- /dev/null
+++ b/pkg-r/DESCRIPTION
@@ -0,0 +1,26 @@
+Package: shinyjson
+Title: Shiny UI using vercel-labs/json-render
+Version: 0.1.0.9000
+Authors@R: c(
+ person("Joe", "Cheng", , "joe@posit.co", role = "aut"),
+ person("Carson", "Sievert", , "carson@posit.co", role = "aut"),
+ person("Garrick", "Aden-Buie", , "garrick@adenbuie.com", role = c("aut", "cre"),
+ comment = c(ORCID = "0000-0002-7111-0077")),
+ person("Barret", "Schloerke", , "barret@posit.co", role = "aut",
+ comment = c(ORCID = "0000-0001-9986-114X")),
+ person("Posit Software, PBC", role = c("cph", "fnd"),
+ comment = c(ROR = "03wc8by49"))
+ )
+Description: Provides a Shiny UI using vercel-labs/json-render.
+License: MIT + file LICENSE
+URL: https://posit-dev.github.io/shinyjson/r/,
+ https://github.com/posit-dev/shinyjson
+BugReports: https://github.com/posit-dev/shinyjson/issues
+Imports:
+ htmltools
+Suggests:
+ testthat (>= 3.0.0)
+Config/testthat/edition: 3
+Encoding: UTF-8
+Roxygen: list(markdown = TRUE)
+RoxygenNote: 7.3.3
diff --git a/pkg-r/LICENSE b/pkg-r/LICENSE
new file mode 100644
index 00000000..43f736a4
--- /dev/null
+++ b/pkg-r/LICENSE
@@ -0,0 +1,2 @@
+YEAR: 2024
+COPYRIGHT HOLDER: Posit Software, PBC
diff --git a/pkg-r/LICENSE.md b/pkg-r/LICENSE.md
new file mode 100644
index 00000000..12e7953b
--- /dev/null
+++ b/pkg-r/LICENSE.md
@@ -0,0 +1,21 @@
+# MIT License
+
+Copyright (c) 2024 Posit Software, PBC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/pkg-r/NAMESPACE b/pkg-r/NAMESPACE
new file mode 100644
index 00000000..da44dada
--- /dev/null
+++ b/pkg-r/NAMESPACE
@@ -0,0 +1,3 @@
+# Generated by roxygen2: do not edit by hand
+
+export(hello_world)
diff --git a/pkg-r/NEWS.md b/pkg-r/NEWS.md
new file mode 100644
index 00000000..c78599c2
--- /dev/null
+++ b/pkg-r/NEWS.md
@@ -0,0 +1,3 @@
+# shinyjson (development version)
+
+- Initial release of shinyjson.
diff --git a/pkg-r/R/hello_world.R b/pkg-r/R/hello_world.R
new file mode 100644
index 00000000..72bc88fc
--- /dev/null
+++ b/pkg-r/R/hello_world.R
@@ -0,0 +1,9 @@
+# Hello, World! - shinyjson
+
+#' Say Hello, World!
+#'
+#' @return A character string "Hello, World!"
+#' @export
+hello_world <- function() {
+ "Hello, World!"
+}
diff --git a/pkg-r/R/shinyjson-package.R b/pkg-r/R/shinyjson-package.R
new file mode 100644
index 00000000..0cc9debe
--- /dev/null
+++ b/pkg-r/R/shinyjson-package.R
@@ -0,0 +1,2 @@
+#' @keywords internal
+"_PACKAGE"
diff --git a/pkg-r/README.md b/pkg-r/README.md
new file mode 100644
index 00000000..596bb8f8
--- /dev/null
+++ b/pkg-r/README.md
@@ -0,0 +1,21 @@
+# shinyjson
+
+> Shiny UI using vercel-labs/json-render.
+
+## Installation
+
+```r
+pak::pak("posit-dev/shinyjson/pkg-r")
+```
+
+## Usage
+
+```r
+library(shinyjson)
+
+hello_world()
+```
+
+## Documentation
+
+- [shinyjson for R](https://posit-dev.github.io/shinyjson/r/)
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..caf7d0ad
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,168 @@
+[project]
+name = "shinyjson"
+description = "Shiny UI using vercel-labs/json-render."
+readme = "pkg-py/README.md"
+requires-python = ">=3.10"
+license = { text = "MIT" }
+dependencies = [
+ "htmltools>=0.6.0",
+ "shiny>=1.4.0",
+]
+dynamic = ["version"]
+authors = [
+ {name = "Joe Cheng", email = "joe@posit.co"},
+ {name = "Carson Sievert", email = "carson@posit.co"},
+ {name = "Garrick Aden-Buie", email = "garrick@posit.co"}
+]
+
+[project.urls]
+Homepage = "https://posit-dev.github.io/shinyjson/"
+Documentation = "https://posit-dev.github.io/shinyjson/py/"
+Repository = "https://github.com/posit-dev/shinyjson"
+Issues = "https://github.com/posit-dev/shinyjson/issues/"
+Changelog = "https://github.com/posit-dev/shinyjson/blob/main/pkg-py/CHANGELOG.md"
+
+[project.optional-dependencies]
+providers = [
+ "anthropic;python_version>='3.11'",
+ "chatlas[mcp]>=0.12.0",
+ "pydantic",
+ "google-generativeai",
+ "langchain-core>=1.0.0",
+ "ollama>=0.4.0",
+ "openai",
+ "tokenizers",
+]
+
+[dependency-groups]
+test = [
+ "coverage>=7.8.2",
+ "faicons",
+ "ipyleaflet",
+ "pandas",
+ "plotly",
+ "pyright>=1.1.398",
+ "pytest>=6.2.4",
+ "playwright>=1.43.0",
+ "pytest-playwright>=0.3.0",
+ "shinylive",
+ "shinywidgets",
+ "tox-uv>=1",
+]
+dev = [
+ "quartodoc>=0.10.0",
+ "ruff>=0.6.5",
+ {include-group = "test"},
+]
+
+[tool.uv]
+upgrade-package = ["shinyjson"]
+
+[build-system]
+requires = ["hatchling", "hatch-vcs"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build]
+skip-excluded-dirs = true
+
+[tool.hatch.build.targets.wheel]
+packages = ["pkg-py/src/shinyjson"]
+include = ["py.typed"]
+
+[tool.hatch.build.targets.sdist]
+include = ["/pkg-py/src/shinyjson"]
+
+[tool.hatch.version]
+source = "vcs"
+tag-pattern = "^py/(?:[\\w-]+-)?(?P[vV]?\\d+(?:\\.\\d+){0,2}[^\\+]*)(?:\\+.*)?$"
+
+[tool.hatch.version.raw-options]
+git_describe_command = "git describe --dirty --tags --long --match 'py/v*'"
+
+[tool.hatch.build.hooks.vcs]
+version-file = "pkg-py/src/shinyjson/__version.py"
+
+[tool.pyright]
+include = ["pkg-py"]
+exclude = ["pkg-py/_dev"]
+
+[tool.tox]
+legacy_tox_ini = """
+[tox]
+env_list = type-py3{10,11,12,13,14}, py3{10,11,12,13,14}
+isolated_build = True
+
+[testenv]
+package = wheel
+wheel_build_env = .pkg
+commands = pytest
+
+[testenv:type]
+deps =
+ pyright
+ pytest
+commands = pyright
+"""
+
+[tool.ruff]
+src = ["pkg-py"]
+exclude = [
+ "__version.py",
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".git-rewrite",
+ ".hg",
+ ".ipynb_checkpoints",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pyenv",
+ ".pytest_cache",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ ".vscode",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "build",
+ "dist",
+ "node_modules",
+ "site-packages",
+ "venv",
+ "_dev",
+]
+
+line-length = 80
+indent-width = 4
+
+target-version = "py310"
+
+[tool.ruff.lint]
+select = ['E', 'F', 'W', 'A', 'PLC', 'PLE', 'PLW', 'I']
+ignore = [
+ "A001", # Variable shadowing Python builtin, e.g. `input`
+ "A002", # Argument shadowing Python builtin, e.g. `id`
+ "A004", # Import shadowing Python builtin, e.g. `input`
+ "E501",
+ "PLC0415", # Import outside top-level
+]
+
+# Allow fix for all enabled rules (when `--fix`) is provided.
+fixable = ["ALL"]
+unfixable = []
+
+# Allow unused variables when underscore-prefixed.
+dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
+
+[tool.ruff.format]
+quote-style = "double"
+indent-style = "space"
+skip-magic-trailing-comma = false
+line-ending = "auto"
+docstring-code-format = true
+docstring-code-line-length = "dynamic"
diff --git a/shinyjson.Rproj b/shinyjson.Rproj
new file mode 100644
index 00000000..270314b8
--- /dev/null
+++ b/shinyjson.Rproj
@@ -0,0 +1,21 @@
+Version: 1.0
+
+RestoreWorkspace: Default
+SaveWorkspace: Default
+AlwaysSaveHistory: Default
+
+EnableCodeIndexing: Yes
+UseSpacesForTab: Yes
+NumSpacesForTab: 2
+Encoding: UTF-8
+
+RnwWeave: Sweave
+LaTeX: pdfLaTeX
+
+AutoAppendNewline: Yes
+StripTrailingWhitespace: Yes
+
+BuildType: Package
+PackageUseDevtools: Yes
+PackageInstallArgs: --no-multiarch --with-keep.source
+PackageRoxygenize: rd,collate,namespace