From 00060a9426d206bbdf572ec848239949b505fc26 Mon Sep 17 00:00:00 2001 From: Pawel Rucki <12943682+pawelru@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:27:06 +0200 Subject: [PATCH] Initialize the package (#1) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .Rbuildignore | 23 ++ .github/CODEOWNERS | 1 + .github/CODE_OF_CONDUCT.md | 133 +++++++ .github/CONTRIBUTING.md | 136 +++++++ .github/ISSUE_TEMPLATE/bug.yml | 51 +++ .github/ISSUE_TEMPLATE/config.yml | 10 + .github/ISSUE_TEMPLATE/feature.yml | 27 ++ .github/ISSUE_TEMPLATE/question.yml | 27 ++ .github/ISSUE_TEMPLATE/release.yaml | 122 ++++++ .github/PULL_REQUEST_TEMPLATE.md | 3 + .github/workflows/check.yaml | 92 +++++ .github/workflows/cla.yaml | 18 + .github/workflows/docs.yaml | 44 +++ .github/workflows/post-release.yaml | 15 + .github/workflows/release.yaml | 42 +++ .github/workflows/scheduled.yaml | 34 ++ .gitignore | 36 ++ .lintr | 3 + .pre-commit-config.yaml | 88 +++++ DESCRIPTION | 43 +++ LICENSE | 13 + NAMESPACE | 13 + NEWS.md | 3 + R/parse_url.R | 27 ++ R/tag_examplesShinylive.R | 226 +++++++++++ README.md | 49 +++ SECURITY.md | 25 ++ _pkgdown.yml | 27 ++ inst/WORDLIST | 8 + man/create_shinylive_url.Rd | 21 ++ man/tag-examplesShinylive.Rd | 129 +++++++ roxy.shinylive.Rproj | 17 + tests/testthat.R | 3 + .../testthat/_snaps/tag_examplesShinylive.md | 14 + tests/testthat/setup-options.R | 20 + tests/testthat/test-parse_url.R | 7 + tests/testthat/test-tag_examplesShinylive.R | 354 ++++++++++++++++++ 37 files changed, 1904 insertions(+) create mode 100644 .Rbuildignore create mode 100644 .github/CODEOWNERS create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.yml create mode 100644 .github/ISSUE_TEMPLATE/question.yml create mode 100644 .github/ISSUE_TEMPLATE/release.yaml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/check.yaml create mode 100644 .github/workflows/cla.yaml create mode 100644 .github/workflows/docs.yaml create mode 100644 .github/workflows/post-release.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/scheduled.yaml create mode 100644 .gitignore create mode 100644 .lintr create mode 100644 .pre-commit-config.yaml create mode 100644 DESCRIPTION create mode 100644 LICENSE create mode 100644 NAMESPACE create mode 100644 NEWS.md create mode 100644 R/parse_url.R create mode 100644 R/tag_examplesShinylive.R create mode 100644 SECURITY.md create mode 100644 _pkgdown.yml create mode 100644 inst/WORDLIST create mode 100644 man/create_shinylive_url.Rd create mode 100644 man/tag-examplesShinylive.Rd create mode 100644 roxy.shinylive.Rproj create mode 100644 tests/testthat.R create mode 100644 tests/testthat/_snaps/tag_examplesShinylive.md create mode 100644 tests/testthat/setup-options.R create mode 100644 tests/testthat/test-parse_url.R create mode 100644 tests/testthat/test-tag_examplesShinylive.R diff --git a/.Rbuildignore b/.Rbuildignore new file mode 100644 index 0000000..0ce030a --- /dev/null +++ b/.Rbuildignore @@ -0,0 +1,23 @@ +^renv$ +^renv\.lock$ +CODE_OF_CONDUCT.md +SECURITY.md +^.*\.Rproj$ +^\.Rproj\.user$ +^_pkgdown\.yml$ +^vignettes/hello\.Rmd$ +^docs$ +^\.github$ +README.* +^\.lintr$ +^staged_dependencies\.yaml$ +coverage.* +^\.pre-commit-config\.yaml$ +^codemeta\.json$ +init.sh +workflows.md +images +^pkgdown$ +^.revdeprefs\.yaml$ +^revdep$ +^\.covrignore$ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..536b996 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @pawelru diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..45d257b --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..8cef653 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,136 @@ +# Contribution Guidelines + +πŸ™ Thank you for taking the time to contribute! + +Your input is deeply valued, whether an issue, a pull request, or even feedback, regardless of size, content or scope. + +## Table of contents + +[πŸ‘Ά Getting started](#getting-started) + +[πŸ“” Code of Conduct](#code-of-conduct) + +[πŸ—ƒ License](#license) + +[πŸ“œ Issues](#issues) + +[🚩 Pull requests](#pull-requests) + +[πŸ’» Coding guidelines](#coding-guidelines) + +[πŸ† Recognition model](#recognition-model) + +[❓ Questions](#questions) + +## Getting started + +Please refer the project [documentation][docs] for a brief introduction. Please also see other [articles][articles] within the project documentation for additional information. + +## Code of Conduct + +A [Code of Conduct](CODE_OF_CONDUCT.md) governs this project. Participants and contributors are expected to follow the rules outlined therein. + +## License + +All your contributions will be covered by this project's [license][license]. + +## Issues + +We use GitHub to track issues, feature requests, and bugs. Before submitting a new issue, please check if the issue has already been reported. If the issue already exists, please upvote the existing issue πŸ‘. + +For new feature requests, please elaborate on the context and the benefit the feature will have for users, developers, or other relevant personas. + +## Pull requests + +### GitHub Flow + +This repository uses the [GitHub Flow](https://docs.github.com/en/get-started/quickstart/github-flow) model for collaboration. To submit a pull request: + +1. Create a branch + + Please see the [branch naming convention](#branch-naming-convention) below. If you don't have write access to this repository, please fork it. + +2. Make changes + + Make sure your code + * passes all checks imposed by GitHub Actions + * is well documented + * is well tested with unit tests sufficiently covering the changes introduced + +3. Create a pull request (PR) + + In the pull request description, please link the relevant issue (if any), provide a detailed description of the change, and include any assumptions. + +4. Address review comments, if any + +5. Post approval + + Merge your PR if you have write access. Otherwise, the reviewer will merge the PR on your behalf. + +6. Pat yourself on the back + + Congratulations! πŸŽ‰ + You are now an official contributor to this project! We are grateful for your contribution. + +### Branch naming convention + +Suppose your changes are related to a current issue in the current project; please name your branch as follows: `_`. Please use underscore (`_`) as a delimiter for word separation. For example, `420_fix_ui_bug` would be a suitable branch name if your change is resolving and UI-related bug reported in issue number `420` in the current project. + +If your change affects multiple repositories, please name your branches as follows: `__`. For example, `69_awesomeproject_fix_spelling_error` would reference issue `69` reported in project `awesomeproject` and aims to resolve one or more spelling errors in multiple (likely related) repositories. + +### `monorepo` and `staged.dependencies` + +Sometimes you might need to change upstream dependent package(s) to be able to submit a meaningful change. We are using [`staged.dependencies`](https://github.com/openpharma/staged.dependencies) functionality to simulate a `monorepo` behavior. The dependency configuration is already specified in this project's `staged_dependencies.yaml` file. You need to name the feature branches appropriately. _This is the only exception from the branch naming convention described above_. + +Please refer to the [staged.dependencies package documentation](https://openpharma.github.io/staged.dependencies/) for more details. + +## Coding guidelines + +This repository follows some unified processes and standards adopted by its maintainers to ensure software development is carried out consistently within teams and cohesively across other repositories. + +### Style guide + +This repository follows the standard [`tidyverse` style guide](https://style.tidyverse.org/) and uses [`lintr`](https://github.com/r-lib/lintr) for lint checks. Customized lint configurations are available in this repository's `.lintr` file. + +### Dependency management + +Lightweight is the right weight. This repository follows [tinyverse](https://www.tinyverse.org/) recommedations of limiting dependencies to minimum. + +### Dependency version management + +If the code is not compatible with all (!) historical versions of a given dependenct package, it is required to specify minimal version in the `DESCRIPTION` file. In particular: if the development version requires (imports) the development version of another package - it is required to put `abc (>= 1.2.3.9000)`. + +### Recommended development environment & tools + +#### R & package versions + +We continuously test our packages against the newest R version along with the most recent dependencies from CRAN and BioConductor. We recommend that your working environment is also set up in the same way. You can find the details about the R version and packages used in the `R CMD check` GitHub Action execution log - there is a step that prints out the R `sessionInfo()`. + +If you discover bugs on older R versions or with an older set of dependencies, please create the relevant bug reports. + +#### `pre-commit` + +We highly recommend that you use the [`pre-commit`](https://pre-commit.com/) tool combined with [`R hooks for pre-commit`](https://github.com/lorenzwalthert/precommit) to execute some of the checks before committing and pushing your changes. + +Pre-commit hooks are already available in this repository's `.pre-commit-config.yaml` file. + +## Recognition model + +As mentioned previously, all contributions are deeply valued and appreciated. While all contribution data is available as part of the [repository insights][insights], to recognize a _significant_ contribution and hence add the contributor to the package authors list, the following rules are enforced: + +* Minimum 5% of lines of code authored* (determined by `git blame` query) OR +* Being at the top 5 contributors in terms of number of commits OR lines added OR lines removed* + +*Excluding auto-generated code, including but not limited to `roxygen` comments or `renv.lock` files. + +The package maintainer also reserves the right to adjust the criteria to recognize contributions. + +## Questions + +If you have further questions regarding the contribution guidelines, please contact the package/repository maintainer. + + +[docs]: https://insightsengineering.github.io/r.pkg.template/index.html +[articles]: https://insightsengineering.github.io/r.pkg.template/main/articles/index.html +[license]: https://insightsengineering.github.io/r.pkg.template/main/LICENSE-text.html +[insights]: https://github.com/insightsengineering/r.pkg.template/pulse diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..cf47384 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,51 @@ +--- +name: 🐞 Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: dropdown + id: r-version + attributes: + label: Which version(s) of R were you using? + multiple: true + options: + - "3.6.x" + - "4.1.x" + - "4.2.x" + - "Other" + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: R + - type: checkboxes + id: code-of-conduct + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://insightsengineering.github.io/r.pkg.template/CODE_OF_CONDUCT.html) + options: + - label: I agree to follow this project's Code of Conduct. + required: true + - type: checkboxes + id: contributor-guidelines + attributes: + label: Contribution Guidelines + description: By submitting this issue, you agree to follow our [Contribution Guidelines](https://insightsengineering.github.io/r.pkg.template/CONTRIBUTING.html) + options: + - label: I agree to follow this project's Contribution Guidelines. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..d17075b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +--- +blank_issues_enabled: true + +contact_links: + - name: We are hiring! + url: https://careers.gene.com/ + about: Genentech and Roche are hiring! + - name: Pharmaverse + url: https://pharmaverse.org/ + about: Related projects @ Pharmaverse.org diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..5efe065 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,27 @@ +--- +name: ✨ Feature Request +description: Request or propose a new feature +title: "[Feature Request]: <title>" +labels: ["enhancement"] +body: + - type: textarea + attributes: + label: Feature description + validations: + required: true + - type: checkboxes + id: code-of-conduct + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://insightsengineering.github.io/r.pkg.template/CODE_OF_CONDUCT.html) + options: + - label: I agree to follow this project's Code of Conduct. + required: true + - type: checkboxes + id: contributor-guidelines + attributes: + label: Contribution Guidelines + description: By submitting this issue, you agree to follow our [Contribution Guidelines](https://insightsengineering.github.io/r.pkg.template/CONTRIBUTING.html) + options: + - label: I agree to follow this project's Contribution Guidelines. + required: true diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..b3e46c1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,27 @@ +--- +name: ❓ Question +description: Question about usage or documentation +title: "[Question]: <title>" +labels: ["question"] +body: + - type: textarea + attributes: + label: What is your question? + validations: + required: true + - type: checkboxes + id: code-of-conduct + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://insightsengineering.github.io/r.pkg.template/CODE_OF_CONDUCT.html) + options: + - label: I agree to follow this project's Code of Conduct. + required: true + - type: checkboxes + id: contributor-guidelines + attributes: + label: Contribution Guidelines + description: By submitting this issue, you agree to follow our [Contribution Guidelines](https://insightsengineering.github.io/r.pkg.template/CONTRIBUTING.html) + options: + - label: I agree to follow this project's Contribution Guidelines. + required: true diff --git a/.github/ISSUE_TEMPLATE/release.yaml b/.github/ISSUE_TEMPLATE/release.yaml new file mode 100644 index 0000000..73bb11d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release.yaml @@ -0,0 +1,122 @@ +--- +name: πŸš€ Release +description: Template for package release +title: "[Release]: <version>" +labels: ["release"] +assignees: + - KlaudiaBB + - cicdguy +body: + - type: markdown + attributes: + value: | + ⚠️ Please do not link or mention any internal references in this issue. This includes internal URLs, intellectual property and references. + - type: textarea + id: blocked-by + attributes: + label: Blocked by + description: Any PRs or issues that this release is blocked by. + placeholder: Add a list of blocking PRs or issues here. + value: | + ### PRs + + - [ ] PR 1 + + ### Issues + + - [ ] Issue 1 + validations: + required: true + - type: textarea + id: pre-release + attributes: + label: Pre-release + description: Pre-requisites that must be fulfilled before initiating the release process. + placeholder: Add your list of pre-requisites here. + value: | + - [ ] Make sure that high priority bugs (label "priority" + "bug") have been resolved before going into the release. + - [ ] Review old/hanging PRs before going into the release. + - [ ] Revisit R-package's lifecycle badges (Optional). + - [ ] Release Manager: Discuss package dependencies, create a plan to sequentially close release activities and submit groups of packages for internal validation (Applicable only for regulatory release). + - [ ] Check Validation Pipeline dry-run results for the package. + - [ ] Make sure all relevant integration tests are green 2-3 days before the release. Look carefully through logs (check for warnings and notes). + - [ ] Inform about the soft code freeze, decide what gets merged in before starting release activities. + - type: textarea + id: release + attributes: + label: Release + description: The steps to be taken in order to create a release. + placeholder: Steps to create a release. + value: | + ### Prepare the release + + - [ ] Create a new release candidate branch + `git checkout -b release-candidate-vX.Y.Z` + - [ ] Update NEWS.md file: make sure it reflects a holistic summary of what has changed in the package, check README. + - [ ] Remove the additional fields (`Remotes`) from the DESCRIPTION file where applicable. + - [ ] Make sure that the minimum dependency versions are updated in the DESCRIPTION file for the package. + - [ ] Increase versioned dependency on {package name} to >=X.Y.Z. + - [ ] Commit your changes and create the PR on GitHub (add "[skip vbump]" in the PR title). Add all updates, commit, and push changes: + `# Make the necessary modifications to your files + # Stage the changes + git add <files your modified> + # Commit the changes + git commit -m "[skip vbump] <Your commit message>" + git push origin release-candidate-vX.Y.Z` + + ### Test the release + + - [ ] Execute the manual tests on Shiny apps that are deployed on various hosting providers (Posit connect and shinyapps.io) - track the results in GitHub issue (Applicable only for frameworks that use Shiny). + - [ ] Monitor integration tests, if integration fails, create priority issues on the board. + - [ ] Execute UAT tests (Optional). + + ### Validation loop + + Note: This section is applicable only for regulatory packages. + + - [ ] Tag the update(s) as a release candidate vX.Y.Z-rc<iteration-number> (e.g. v0.5.3-rc1) on the release candidate branch (release-candidate-vX.Y.Z). + `# Create rc tag for submission for internal validation + git tag vX.Y.Z-rc<iteration number> + git push origin vX.Y.Z-rc<iteration number>` + - [ ] Submit the package for internal validation. + - [ ] Address any feedback (internal validation/user testing), retag the package as a release candidate vX.Y.Z-rc(n+1). Repeat the submission for internal validation if necessary. + - [ ] Get the package validated. + + ### Tag the release + + - [ ] If the additional fields were removed, add them back in a separate PR, and then merge the PR back to main (add "[skip vbump]" in the PR title). If nothing was removed just merge the PR you created in the "Prepare the release" section to `main`. Note the commit hash of the merged commit. **Note:** additional commits might be added to the `main` branch by a bot or an automation - we do **NOT** want to tag this commit. + + #### Make sure of the following before continuing with the release: + + - [ ] CI checks are passing in GH. + - [ ] Shiny apps are deployable and there are no errors/warnings (Applicable only for frameworks that use Shiny). + + - [ ] Create a git tag with the final version set to vX.Y.Z on the main branch. In order to do this: + 1. Checkout the commit hash. + `git checkout <commit hash>` + 2. Tag the hash with the release version (vX.Y.Z). + `git tag vX.Y.Z` + 3. Push the tag to make the final release. + `git push origin vX.Y.Z` + - [ ] Update downstream package dependencies to (>=X.Y.Z) in {package name}. + Note: Once the release tag is created, the package is automatically published to internal repositories. + - type: textarea + id: post-release + attributes: + label: Post-release + description: The list of activities to be completed after the release. + placeholder: The steps that must be taken after the release. + value: | + - [ ] Make sure that the package is published to internal repositories (Validated and/or Non-Validated repository). + - [ ] Review and update installation instructions for the package if needed. + - [ ] Make sure internal documentation/documentation catalogs are up to date. + - [ ] Notify the IDR team to start post-release/clean-up activities. + - [ ] Announce the release on ________. + - type: textarea + id: decision-tree + attributes: + label: Decision tree + description: Any decision tree(s) that would aid release management + placeholder: Any decision tree(s) that would aid release management. + value: | + Click [here](https://github.com/insightsengineering/.github/blob/main/.github/ISSUE_TEMPLATE/RELEASE_DECISION_TREE.md) to see the release decision tree. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..81c84dc --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,3 @@ +# Pull Request + +<!-- Please describe your pull request here --> diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml new file mode 100644 index 0000000..6c25abe --- /dev/null +++ b/.github/workflows/check.yaml @@ -0,0 +1,92 @@ +--- +name: Check πŸ›  + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + branches: + - main + push: + branches: + - main + workflow_dispatch: + +jobs: + audit: + name: Audit Dependencies πŸ•΅οΈβ€β™‚οΈ + uses: insightsengineering/r.pkg.template/.github/workflows/audit.yaml@main + r-cmd: + name: R CMD Check 🧬 + uses: insightsengineering/r.pkg.template/.github/workflows/build-check-install.yaml@main + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + with: + additional-env-vars: | + _R_CHECK_CRAN_INCOMING_REMOTE_=false + additional-r-cmd-check-params: --as-cran + r-cmd-non-cran: + name: R CMD Check (non-CRAN) 🧬 + uses: insightsengineering/r.pkg.template/.github/workflows/build-check-install.yaml@main + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + with: + additional-env-vars: | + NOT_CRAN=true + concurrency-group: non-cran + unit-test-report-directory: unit-test-report-non-cran + coverage: + name: Coverage πŸ“” + uses: insightsengineering/r.pkg.template/.github/workflows/test-coverage.yaml@main + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + linter: + if: github.event_name != 'push' + name: SuperLinter πŸ¦Έβ€β™€οΈ + uses: insightsengineering/r.pkg.template/.github/workflows/linter.yaml@main + roxygen: + name: Roxygen πŸ…Ύ + uses: insightsengineering/r.pkg.template/.github/workflows/roxygen.yaml@main + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + with: + auto-update: true + gitleaks: + name: gitleaks πŸ’§ + uses: insightsengineering/r.pkg.template/.github/workflows/gitleaks.yaml@main + spelling: + name: Spell Check πŸ†Ž + uses: insightsengineering/r.pkg.template/.github/workflows/spelling.yaml@main + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + links: + if: github.event_name != 'push' + name: Check URLs 🌐 + uses: insightsengineering/r.pkg.template/.github/workflows/links.yaml@main + vbump: + name: Version Bump πŸ€œπŸ€› + if: github.event_name == 'push' + uses: insightsengineering/r.pkg.template/.github/workflows/version-bump.yaml@main + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + version: + name: Version Check 🏁 + uses: insightsengineering/r.pkg.template/.github/workflows/version.yaml@main + licenses: + name: License Check πŸƒ + uses: insightsengineering/r.pkg.template/.github/workflows/licenses.yaml@main + style: + if: github.event_name != 'push' + name: Style Check πŸ‘— + uses: insightsengineering/r.pkg.template/.github/workflows/style.yaml@main + with: + auto-update: true + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + grammar: + if: github.event_name != 'push' + name: Grammar Check πŸ”€ + uses: insightsengineering/r.pkg.template/.github/workflows/grammar.yaml@main diff --git a/.github/workflows/cla.yaml b/.github/workflows/cla.yaml new file mode 100644 index 0000000..b674b0b --- /dev/null +++ b/.github/workflows/cla.yaml @@ -0,0 +1,18 @@ +name: CLA πŸ” + +on: + issue_comment: + types: + - created + # For PRs that originate from forks + pull_request_target: + types: + - opened + - closed + - synchronize + +jobs: + CLA: + name: CLA πŸ“ + uses: insightsengineering/.github/.github/workflows/cla.yaml@main + secrets: inherit diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000..9dbcfd0 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,44 @@ +--- +name: Docs πŸ“š + +on: + push: + branches: + - main + paths: + - "inst/templates/**" + - "_pkgdown.*" + - DESCRIPTION + - "**.md" + - "**.Rmd" + - "man/**" + - "LICENSE.*" + - NAMESPACE + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + branches: + - main + paths: + - "inst/templates/**" + - "_pkgdown.*" + - DESCRIPTION + - "**.md" + - "**.Rmd" + - "man/**" + - "LICENSE.*" + - NAMESPACE + workflow_dispatch: + +jobs: + docs: + name: Pkgdown Docs πŸ“š + uses: insightsengineering/r.pkg.template/.github/workflows/pkgdown.yaml@main + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + with: + default-landing-page: main + additional-unit-test-report-directories: unit-test-report-non-cran diff --git a/.github/workflows/post-release.yaml b/.github/workflows/post-release.yaml new file mode 100644 index 0000000..c5fb822 --- /dev/null +++ b/.github/workflows/post-release.yaml @@ -0,0 +1,15 @@ +--- +name: Post release ✨ + +on: + release: + types: ["released"] + +jobs: + vbump: + name: Version Bump πŸ€œπŸ€› + uses: insightsengineering/r.pkg.template/.github/workflows/version-bump.yaml@main + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + with: + vbump-after-release: true diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..cf7732a --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,42 @@ +--- +name: Release 🎈 + +on: + push: + tags: + - "v*" + workflow_dispatch: + +jobs: + build: + name: Build package 🎁 + needs: release + uses: insightsengineering/r.pkg.template/.github/workflows/build-check-install.yaml@main + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + with: + skip-r-cmd-check: true + skip-r-cmd-install: true + docs: + name: Pkgdown Docs πŸ“š + needs: release + uses: insightsengineering/r.pkg.template/.github/workflows/pkgdown.yaml@main + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + with: + default-landing-page: latest-tag + validation: + name: R Package Validation report πŸ“ƒ + needs: release + uses: insightsengineering/r.pkg.template/.github/workflows/validation.yaml@main + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + release: + name: Create release πŸŽ‰ + uses: insightsengineering/r.pkg.template/.github/workflows/release.yaml@main + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + wasm: + name: Build WASM packages πŸ§‘β€πŸ­ + needs: release + uses: insightsengineering/r.pkg.template/.github/workflows/wasm.yaml@main diff --git a/.github/workflows/scheduled.yaml b/.github/workflows/scheduled.yaml new file mode 100644 index 0000000..d0cc504 --- /dev/null +++ b/.github/workflows/scheduled.yaml @@ -0,0 +1,34 @@ +--- +name: On-demand πŸ§‘β€πŸ”¬ + +on: + schedule: + - cron: '45 3 * * 0' + workflow_dispatch: + +jobs: + dependency-test: + strategy: + fail-fast: false + matrix: + test-strategy: ["min_cohort", "min_isolated", "release", "max"] + uses: insightsengineering/r.pkg.template/.github/workflows/verdepcheck.yaml@main + name: Dependency Test - ${{ matrix.test-strategy }} πŸ”’ + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + GCHAT_WEBHOOK: ${{ secrets.GCHAT_WEBHOOK }} + with: + strategy: ${{ matrix.test-strategy }} + additional-env-vars: | + PKG_SYSREQS_DRY_RUN=true + branch-cleanup: + name: Branch Cleanup 🧹 + uses: insightsengineering/r.pkg.template/.github/workflows/branch-cleanup.yaml@main + secrets: + REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + revdepcheck: + name: revdepcheck ↩️ + uses: insightsengineering/r.pkg.template/.github/workflows/revdepcheck.yaml@main + rhub: + name: R-hub 🌐 + uses: insightsengineering/r.pkg.template/.github/workflows/rhub.yaml@main diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6526a76 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +.DS_Store +.httr-oauth +.project +.RData +.Rhistory +.Rproj.user +.Ruserdata +.settings/** +*.html +*.Rcheck +*.rprof +*.sas.txt +*~ +/.project +devel/* +doc +docs +inst/outputs/* +logs +Meta +packrat/lib*/ +temp +temp_w +templates/ +tmp.* +vignettes/*.html +vignettes/*.md +vignettes/*.R +coverage.* +.vscode/ +node_modules/ +package-lock.json +package.json +tests/testthat/_snaps/**/*.new.md +tests/testthat/_snaps/**/*.new.svg +inst/doc diff --git a/.lintr b/.lintr new file mode 100644 index 0000000..ec50cf4 --- /dev/null +++ b/.lintr @@ -0,0 +1,3 @@ +linters: linters_with_defaults( + line_length_linter = line_length_linter(120) + ) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..78fcce0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,88 @@ +--- +# All available hooks: https://pre-commit.com/hooks.html +# R specific hooks: https://github.com/lorenzwalthert/precommit +repos: + - repo: https://github.com/lorenzwalthert/precommit + rev: v0.3.2.9007 + hooks: + - id: style-files + args: [--style_pkg=styler, --style_fun=tidyverse_style] + - id: roxygenize + additional_dependencies: + - glue + - jsonlite + - roxygen2 + - lzstring + - roxygen2 + - stringr + # codemeta must be above use-tidy-description when both are used + # - id: codemeta-description-updated + - id: use-tidy-description + - id: spell-check + exclude: > + (?x)^( + data/.*| + inst/.*| + (.*/|)\.Rprofile| + (.*/|)\.Renviron| + (.*/|)\.gitignore| + (.*/|)NAMESPACE| + (.*/|)DESCRIPTION| + (.*/|)WORDLIST| + (.*/|)LICENSE| + (.*/|)\.Rbuildignore| + (.*/|)\.lintr| + (.*/|)_pkgdown.y[a]?ml| + (.*/|)\.covrignore| + (.*/|)staged_dependencies.y[a]?ml| + (.*/|)\.pre-commit-.*| + \.github/.*| + .*\.[rR]| + .*\.Rproj| + .*\.py| + .*\.png| + .*\.feather| + .*\.rds| + .*\.Rds| + .*\.sh| + .*\.RData + )$ + - id: lintr + - id: readme-rmd-rendered + - id: parsable-R + - id: no-browser-statement + - id: deps-in-desc + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.0.0-alpha.6 + hooks: + - id: prettier + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + args: ["--maxkb=200"] + - id: end-of-file-fixer + exclude: '\.Rd' + - id: trailing-whitespace + exclude: '\.Rd' + - id: check-yaml + - id: no-commit-to-branch + - id: mixed-line-ending + args: ["--fix=lf"] + - id: detect-aws-credentials + args: ["--allow-missing-credentials"] + - id: detect-private-key + - id: forbid-new-submodules + - id: check-symlinks + - repo: local + hooks: + - id: forbid-to-commit + name: Don't commit common R artifacts + entry: Cannot commit .Rhistory, .RData, .Rds or .rds. + language: fail + files: '\.Rhistory|\.RData|\.Rds|\.rds$' + # `exclude: <regex>` to allow committing specific files. + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.33.0 + hooks: + - id: markdownlint diff --git a/DESCRIPTION b/DESCRIPTION new file mode 100644 index 0000000..be734ad --- /dev/null +++ b/DESCRIPTION @@ -0,0 +1,43 @@ +Type: Package +Package: roxy.shinylive +Title: A Roxygen2 Extension for Shinylive +Version: 0.0.0.9001 +Authors@R: c( + person("Pawel Rucki", , , "pawel.rucki@roche.com", role = c("aut", "cre")), + person("F. Hoffmann-La Roche AG", role = c("cph", "fnd")) + ) +Description: An extension for roxygen2 that allows to auto-create links to shinylive from examples in the documentation. +License: Apache License 2.0 | file LICENSE +URL: https://github.com/insightsengineering/roxy.shinylive/ +BugReports: https://github.com/insightsengineering/roxy.shinylive/issues +Depends: + R (>= 4.0) +Imports: + glue, + jsonlite (>= 0.9.4), + lzstring, + roxygen2 (>= 7.1.1), + stringr (>= 0.4) +Suggests: + pkgdown (>= 1.2.0), + testthat (>= 3.0.4), + withr (>= 2.4.3) +Config/Needs/verdepcheck: + tidyverse/glue, + jeroen/jsonlite, + lzstring=parmsam/lzstring-r, + r-lib/roxygen2, + tidyverse/stringr, + r-lib/pkgdown, + r-lib/testthat, + r-lib/withr +Config/Needs/website: insightsengineering/nesttemplate +Encoding: UTF-8 +Language: en-US +LazyData: true +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.3.2 +Config/testthat/edition: 3 +Collate: + 'tag_examplesShinylive.R' + 'parse_url.R' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fb8e419 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2024 F. Hoffmann-La Roche AG + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/NAMESPACE b/NAMESPACE new file mode 100644 index 0000000..e9e45e9 --- /dev/null +++ b/NAMESPACE @@ -0,0 +1,13 @@ +# Generated by roxygen2: do not edit by hand + +S3method(format,rd_section_examplesShinylive) +S3method(roxygen2::roxy_tag_parse,roxy_tag_examplesShinylive) +S3method(roxygen2::roxy_tag_rd,roxy_tag_examplesShinylive) +export(create_shinylive_url) +importFrom(glue,glue_data) +importFrom(jsonlite,toJSON) +importFrom(jsonlite,unbox) +importFrom(lzstring,compressToEncodedURIComponent) +importFrom(roxygen2,rd_section) +importFrom(roxygen2,warn_roxy_tag) +importFrom(stringr,str_trim) diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..538886c --- /dev/null +++ b/NEWS.md @@ -0,0 +1,3 @@ +# roxy.shinylive 0.0.0.9001 + +- Initialize the package. diff --git a/R/parse_url.R b/R/parse_url.R new file mode 100644 index 0000000..dfe3904 --- /dev/null +++ b/R/parse_url.R @@ -0,0 +1,27 @@ +#' Creates Shinylive url for the app code. +#' +#' @importFrom jsonlite unbox toJSON +#' @importFrom lzstring compressToEncodedURIComponent +#' +#' @param code (`character(1)`) A string with app code. +#' @return (`character(1)`) Shinylive app url. +#' +#' @export +#' +#' @examples +#' code <- "this is your app code as a string" +#' create_shinylive_url(code) +create_shinylive_url <- function(code) { + stopifnot(is.character(code) && length(code) == 1) + + # implementation based on "Create ShinyLive Link" feature of Shiny VSCode extension + # https://github.com/posit-dev/shiny-vscode/blob/80560bf36d516ff89dffe88bd9a28cee9edd4d43/src/shinylive.ts#L499 + files <- list( + name = jsonlite::unbox("app.R"), + content = jsonlite::unbox(code) + ) + files_json <- jsonlite::toJSON(list(files)) + files_lz <- lzstring::compressToEncodedURIComponent(as.character(files_json)) + files_lz <- gsub("/", "-", files_lz) + sprintf("https://shinylive.io/r/app/#code=%s", files_lz) +} diff --git a/R/tag_examplesShinylive.R b/R/tag_examplesShinylive.R new file mode 100644 index 0000000..1ffe962 --- /dev/null +++ b/R/tag_examplesShinylive.R @@ -0,0 +1,226 @@ +#' Custom `examplesShinylive` tag. +#' +#' This function generates a new "Examples in Shinylive" section in the documentation. This section contains URL to +#' the application in Shinylive as well as an iframe with the application. +#' If no code is provided then the code is taken from the following `@examples` or `@examplesIf` tag. +#' +#' The application code must be executable inside Shinylive. If the application code includes functions from your +#' package, you must add `library(<package>)` beforehand. For more information, refer to the Decoration section +#' on how to use and decorate existing examples. +#' +#' Note: All the packages used in the application code need to be installable in WebR. +#' See [this article](https://docs.r-wasm.org/webr/latest/packages.html) for more details. +#' +#' @section Decoration: +#' +#' To avoid repetition between the `@examplesShinylive` and `@examples` sections contents, +#' there are special string literals to be used inside `@examplesShinylive` tag content +#' that allow you to access the content(s) of the `@examples` or `@examplesIf` tags. +#' These literals should be used as expressions embraced with `{{ }}`, which are then interpolated using +#' `glue::glue_data(..., .open = "{{", .close = "}}")`. +#' +#' The following keywords are available: +#' * `"{{ next_example }}"` - (the default if empty) "raw" element of the next example +#' * `"{{ prev_example }}"` - "raw" element of the previous example +#' * `"{{ tags_examples }}"` - a list of `@examples` or `@examplesIf` tags +#' * `"{{ examples }}"` - a list of "raw" elements from `tags_examples` list elements +#' +#' This allows you to access and decorate existing example code to create executable application code for Shinylive. +#' Refer to the examples section for possible use cases. +#' +#' @name tag-examplesShinylive +#' +#' @usage +#' #' @examplesShinylive${1:# example code (optional)} +#' +#' @examples +#' # As a part of documentation: +#' +#' # basic example: +#' #' (docs) +#' #' @examplesShinylive +#' #' @examples +#' #' (example code) +#' +#' # using keywords - `{{ next_example }}`: +#' #' (docs) +#' #' @examplesShinylive +#' #' foo <- 1 +#' #' {{ next_example }} +#' #' bar <- 2 +#' #' @examples +#' #' (example code) +#' +#' # using keywords - `{{ prev_example }}`: +#' #' (docs) +#' #' bar <- 2 +#' #' @examples +#' #' (example code) +#' #' @examplesShinylive +#' #' foo <- 1 +#' #' {{ prev_example }} +#' +#' # A typical example would be: +#' #' (docs) +#' #' @examplesShinylive +#' #' library(<package>) +#' #' interactive <- function() TRUE +#' #' {{ next_example }} +#' #' @examples +#' #' app <- ... +#' #' if (interactive()) { +#' #' shinyApp(app$ui, app$server) +#' #' } +#' +#' # multiple apps: +#' #' (docs) +#' #' @examplesShinylive +#' #' @examples +#' #' (example app 1) +#' #' @examplesShinylive +#' #' @examples +#' #' (example app 2) +#' +#' # skip parts of example code: +#' #' (docs) +#' #' @examples +#' #' (example code - skipped) +#' #' @examplesShinylive +#' #' @examples +#' #' (example code - included) +#' +#' # multiple apps with keywords: +#' #' (docs) +#' #' @examplesShinylive +#' #' x <- 1 +#' #' {{ next_example }} +#' #' @examples +#' #' (example app 1) +#' #' @examplesShinylive +#' #' y <- 1 +#' #' {{ next_example }} +#' #' @examples +#' #' (example app 2) +#' +#' # combining multiple examples: +#' #' (docs) +#' #' @examples +#' #' (app pre-requisites) +#' #' @examples +#' #' (example app) +#' #' @examplesShinylive +#' #' {{ paste0(examples, collapse = ", ") }} +#' +#' # identical to the above example but with a different approach: +#' #' (docs) +#' #' @examples +#' #' (app pre-requisites) +#' #' @examples +#' #' (example app) +#' #' @examplesShinylive +#' #' {{ paste0(lapply(tags_examples, `[[`, "raw"), collapse = ", ") }} +NULL + +#' @noRd +#' @exportS3Method roxygen2::roxy_tag_parse roxy_tag_examplesShinylive +#' @importFrom glue glue_data +#' @importFrom stringr str_trim +#' @importFrom roxygen2 warn_roxy_tag +roxy_tag_parse.roxy_tag_examplesShinylive <- function(x) { + if (stringr::str_trim(x$raw) == "") { + x$raw <- "{{ next_example }}" + } + + # not elegant but this is the most efficient way to access sibling tags + tokens <- get("tokens", envir = parent.frame(3L)) + + tags_examples <- Filter(function(x) x$tag %in% c("examples", "examplesIf"), tokens) + + examples <- lapply(tags_examples, `[[`, "raw") + + next_example <- Reduce( + function(x, y) `if`(x$line < y$line, x, y), + Filter(function(y) y$line > x$line, tags_examples) + )$raw + + prev_example <- Reduce( + function(x, y) `if`(x$line > y$line, x, y), + Filter(function(y) y$line < x$line, tags_examples) + )$raw + + x$raw <- try( + as.character( + glue::glue_data( + .x = list( + tags_examples = tags_examples, + examples = examples, + next_example = next_example, + prev_example = prev_example + ), + x$raw, + .open = "{{", + .close = "}}" + ) + ), + silent = TRUE + ) + + if (inherits(x$raw, "try-error")) { + roxygen2::warn_roxy_tag(x, "failed to interpolate the content") + return(NULL) + } + + if (is.null(x$raw) || length(x$raw) == 0) { + roxygen2::warn_roxy_tag(x, "requires a value") + return(NULL) + } + x$val <- create_shinylive_url(x$raw) # nolint: object_usage_linter. + x +} + +#' @noRd +#' @exportS3Method roxygen2::roxy_tag_rd roxy_tag_examplesShinylive +#' @importFrom roxygen2 rd_section +roxy_tag_rd.roxy_tag_examplesShinylive <- function(x, base_path, env) { + roxygen2::rd_section("examplesShinylive", x$val) +} + +#' @noRd +#' @exportS3Method format rd_section_examplesShinylive +format.rd_section_examplesShinylive <- function(x, ...) { + iframe_style <- paste0( + "style=\"", + paste( + "height: 800px", + "width: 100\\%", + "border: 1px solid rgba(0,0,0,0.175)", + "border-radius: .375rem", + "position: relative", + "z-index: 1", + sep = "; " + ), + "\"" + ) + # If in pkgdown website - increase the width + jscode <- " +$(function() { + var if_pkgdown = [...document.scripts].filter(x => x.src.includes(\"pkgdown.js\")).length > 0; + if (if_pkgdown) { + $(\"iframe.iframe_shinylive\").css(\"width\", \"140\\%\"); + } +});" + paste0( + "\\section{Examples in Shinylive}{\n", + "\\describe{\n", + paste0( + " \\item{example-", seq_along(x$value), "}{\n", + " \\href{", x$value, "}{Open in Shinylive}\n", + " \\if{html}{\\out{<script type=\"text/javascript\">", gsub("\n", "", jscode), "</script>}}\n", + " \\if{html}{\\out{<iframe class=\"iframe_shinylive\" src=\"", x$value, "\" ", iframe_style, "></iframe>}}\n", # nolint: line_length_linter. + " }\n", + collapse = "" + ), + "}\n", + "}\n" + ) +} diff --git a/README.md b/README.md index c579f1d..f829102 100644 --- a/README.md +++ b/README.md @@ -1 +1,50 @@ # roxy.shinylive + +## Overview + +This package provides a `roxygen2` extension that automatically takes the code from the `@examples` tag that follows and crate an URL to the shinylive service. During the documentation build, a new section is added to the function manual that contains aforementioned link as well as iframe to the application itself. + +## Install + +```r +pak::pak("insightsengineering/roxy.shinylive") +``` + +## Usage + +In your `DESCRIPTION` file, add the following: + +```yaml +Roxygen: list(markdown = TRUE, packages = c("roxy.shinylive")) +``` + +Then in your package documentation: + +```r +#' (docs) +#' @examplesShinylive +#' @examples +#' (example code with a Shiny app) +``` + +Which would produce a following output in your documentation: + +```Rd +\section{Examples in Shinylive}{ +\describe{ + \item{example-1}{ + \href{https://shinylive.io/r/app/#code=...}{Open in Shinylive} + \if{html}{\out{<script type="text/javascript">(custom JS)</script>}} + \if{html}{\out{<iframe src="https://shinylive.io/r/app/#code=..."></iframe>}} + } + \item{example-2}{ + \href{https://shinylive.io/r/app/#code=...}{Open in Shinylive} + \if{html}{\out{<script type="text/javascript">(custom JS)</script>}} + \if{html}{\out{<iframe src="https://shinylive.io/r/app/#code=..."></iframe>}} + } + ... +} +} +``` + +See the package documentation for more details. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..98ab70f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,25 @@ +# Security Policy + +## Reporting Security Issues + +If you believe you have found a security vulnerability in any of the repositories in this organization, please report it to us through coordinated disclosure. + +**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** + +Instead, please send an email to vulnerability.management[@]roche.com. + +Please include as much of the information listed below as you can to help us better understand and resolve the issue: + +* The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) +* Full paths of source file(s) related to the manifestation of the issue +* The location of the affected source code (tag/branch/commit or direct URL) +* Any special configuration required to reproduce the issue +* Step-by-step instructions to reproduce the issue +* Proof-of-concept or exploit code (if possible) +* Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +## Data Security Standards (DSS) + +Please make sure that while reporting issues in the form a bug, feature, or pull request, *all* sensitive information such as [PII](https://www.dhs.gov/privacy-training/what-personally-identifiable-information), [PHI](https://www.hhs.gov/hipaa/for-professionals/security/laws-regulations/index.html), and [PCI](https://www.pcisecuritystandards.org/pci_security/standards_overview) is completely removed from any text and attachments, including pictures and videos. diff --git a/_pkgdown.yml b/_pkgdown.yml new file mode 100644 index 0000000..60ef829 --- /dev/null +++ b/_pkgdown.yml @@ -0,0 +1,27 @@ +--- +url: https://insightsengineering.github.io/roxy.shinylive/ + +template: + package: nesttemplate + +navbar: + right: + - icon: fa-github + href: https://github.com/insightsengineering/roxy.shinylive + +articles: + - title: Articles + navbar: ~ + contents: + - matches("*") + +reference: + - title: Tags + desc: Functions for working with tags + contents: + - starts_with("tag-") + + - title: Other + desc: Other functions + contents: + - create_shinylive_url diff --git a/inst/WORDLIST b/inst/WORDLIST new file mode 100644 index 0000000..3e45b60 --- /dev/null +++ b/inst/WORDLIST @@ -0,0 +1,8 @@ +Roxygen +Shinylive +WebR +iframe +installable +roxy +roxygen +shinylive diff --git a/man/create_shinylive_url.Rd b/man/create_shinylive_url.Rd new file mode 100644 index 0000000..8b312cb --- /dev/null +++ b/man/create_shinylive_url.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/parse_url.R +\name{create_shinylive_url} +\alias{create_shinylive_url} +\title{Creates Shinylive url for the app code.} +\usage{ +create_shinylive_url(code) +} +\arguments{ +\item{code}{(\code{character(1)}) A string with app code.} +} +\value{ +(\code{character(1)}) Shinylive app url. +} +\description{ +Creates Shinylive url for the app code. +} +\examples{ +code <- "this is your app code as a string" +create_shinylive_url(code) +} diff --git a/man/tag-examplesShinylive.Rd b/man/tag-examplesShinylive.Rd new file mode 100644 index 0000000..aa14bdd --- /dev/null +++ b/man/tag-examplesShinylive.Rd @@ -0,0 +1,129 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tag_examplesShinylive.R +\name{tag-examplesShinylive} +\alias{tag-examplesShinylive} +\title{Custom \code{examplesShinylive} tag.} +\usage{ +#' @examplesShinylive${1:# example code (optional)} +} +\description{ +This function generates a new "Examples in Shinylive" section in the documentation. This section contains URL to +the application in Shinylive as well as an iframe with the application. +If no code is provided then the code is taken from the following \verb{@examples} or \verb{@examplesIf} tag. +} +\details{ +The application code must be executable inside Shinylive. If the application code includes functions from your +package, you must add \verb{library(<package>)} beforehand. For more information, refer to the Decoration section +on how to use and decorate existing examples. + +Note: All the packages used in the application code need to be installable in WebR. +See \href{https://docs.r-wasm.org/webr/latest/packages.html}{this article} for more details. +} +\section{Decoration}{ + + +To avoid repetition between the \verb{@examplesShinylive} and \verb{@examples} sections contents, +there are special string literals to be used inside \verb{@examplesShinylive} tag content +that allow you to access the content(s) of the \verb{@examples} or \verb{@examplesIf} tags. +These literals should be used as expressions embraced with \code{{{ }}}, which are then interpolated using +\code{glue::glue_data(..., .open = "{{", .close = "}}")}. + +The following keywords are available: +\itemize{ +\item \code{"{{ next_example }}"} - (the default if empty) "raw" element of the next example +\item \code{"{{ prev_example }}"} - "raw" element of the previous example +\item \code{"{{ tags_examples }}"} - a list of \verb{@examples} or \verb{@examplesIf} tags +\item \code{"{{ examples }}"} - a list of "raw" elements from \code{tags_examples} list elements +} + +This allows you to access and decorate existing example code to create executable application code for Shinylive. +Refer to the examples section for possible use cases. +} + +\examples{ +# As a part of documentation: + +# basic example: +#' (docs) +#' @examplesShinylive +#' @examples +#' (example code) + +# using keywords - `{{ next_example }}`: +#' (docs) +#' @examplesShinylive +#' foo <- 1 +#' {{ next_example }} +#' bar <- 2 +#' @examples +#' (example code) + +# using keywords - `{{ prev_example }}`: +#' (docs) +#' bar <- 2 +#' @examples +#' (example code) +#' @examplesShinylive +#' foo <- 1 +#' {{ prev_example }} + +# A typical example would be: +#' (docs) +#' @examplesShinylive +#' library(<package>) +#' interactive <- function() TRUE +#' {{ next_example }} +#' @examples +#' app <- ... +#' if (interactive()) { +#' shinyApp(app$ui, app$server) +#' } + +# multiple apps: +#' (docs) +#' @examplesShinylive +#' @examples +#' (example app 1) +#' @examplesShinylive +#' @examples +#' (example app 2) + +# skip parts of example code: +#' (docs) +#' @examples +#' (example code - skipped) +#' @examplesShinylive +#' @examples +#' (example code - included) + +# multiple apps with keywords: +#' (docs) +#' @examplesShinylive +#' x <- 1 +#' {{ next_example }} +#' @examples +#' (example app 1) +#' @examplesShinylive +#' y <- 1 +#' {{ next_example }} +#' @examples +#' (example app 2) + +# combining multiple examples: +#' (docs) +#' @examples +#' (app pre-requisites) +#' @examples +#' (example app) +#' @examplesShinylive +#' {{ paste0(examples, collapse = ", ") }} + +# identical to the above example but with a different approach: +#' (docs) +#' @examples +#' (app pre-requisites) +#' @examples +#' (example app) +#' @examplesShinylive +#' {{ paste0(lapply(tags_examples, `[[`, "raw"), collapse = ", ") }} +} diff --git a/roxy.shinylive.Rproj b/roxy.shinylive.Rproj new file mode 100644 index 0000000..aaa62a5 --- /dev/null +++ b/roxy.shinylive.Rproj @@ -0,0 +1,17 @@ +Version: 1.0 + +RestoreWorkspace: No +SaveWorkspace: No +AlwaysSaveHistory: Default + +EnableCodeIndexing: Yes +Encoding: UTF-8 + +AutoAppendNewline: Yes +StripTrailingWhitespace: Yes +LineEndingConversion: Posix + +BuildType: Package +PackageUseDevtools: Yes +PackageInstallArgs: --no-multiarch --with-keep.source +PackageRoxygenize: rd,collate,namespace diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..b31aad1 --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,3 @@ +pkg_name <- "roxy.shinylive" +library(pkg_name, character.only = TRUE) +testthat::test_check(pkg_name) diff --git a/tests/testthat/_snaps/tag_examplesShinylive.md b/tests/testthat/_snaps/tag_examplesShinylive.md new file mode 100644 index 0000000..21a9616 --- /dev/null +++ b/tests/testthat/_snaps/tag_examplesShinylive.md @@ -0,0 +1,14 @@ +# examplesShinylive tag - errors - missing @examples + + Code + block <- roxygen2::parse_text(text)[[1]] + Message + x <text>:8: @examplesShinylive requires a value. + +# examplesShinylive tag - keywords - error when parsing with glue + + Code + block <- roxygen2::parse_text(text)[[1]] + Message + x <text>:8: @examplesShinylive failed to interpolate the content. + diff --git a/tests/testthat/setup-options.R b/tests/testthat/setup-options.R new file mode 100644 index 0000000..78be1f9 --- /dev/null +++ b/tests/testthat/setup-options.R @@ -0,0 +1,20 @@ +# `opts_partial_match_old` is left for exclusions due to partial matching in dependent packages (i.e. not fixable here) +# it might happen that it is not used right now, but it is left for possible future use +# use with: `withr::with_options(opts_partial_match_old, { ... })` inside the test +opts_partial_match_old <- list( + warnPartialMatchDollar = getOption("warnPartialMatchDollar"), + warnPartialMatchArgs = getOption("warnPartialMatchArgs"), + warnPartialMatchAttr = getOption("warnPartialMatchAttr") +) +opts_partial_match_new <- list( + warnPartialMatchDollar = TRUE, + warnPartialMatchArgs = TRUE, + warnPartialMatchAttr = TRUE +) + +if (isFALSE(getFromNamespace("on_cran", "testthat")()) && requireNamespace("withr", quietly = TRUE)) { + withr::local_options( + opts_partial_match_new, + .local_envir = testthat::teardown_env() + ) +} diff --git a/tests/testthat/test-parse_url.R b/tests/testthat/test-parse_url.R new file mode 100644 index 0000000..febc86e --- /dev/null +++ b/tests/testthat/test-parse_url.R @@ -0,0 +1,7 @@ +test_that("create_shinylive_url works as expected", { + app_code <- "x <- 1" + expect_identical( + create_shinylive_url(app_code), + "https://shinylive.io/r/app/#code=NobwRAdghgtgpmAXGKAHVA6ASmANGAYwHsIAXOMpMADwAIAeAWloEYwBfAXSA" + ) +}) diff --git a/tests/testthat/test-tag_examplesShinylive.R b/tests/testthat/test-tag_examplesShinylive.R new file mode 100644 index 0000000..ace91b5 --- /dev/null +++ b/tests/testthat/test-tag_examplesShinylive.R @@ -0,0 +1,354 @@ +test_that("examplesShinylive tag - errors - missing @examples", { + text <- " + #' This is a title + #' + #' This is the description. + #' + #' @param x,y A number + #' @export + #' @examplesShinylive + f <- function(x, y) x + y + " + expect_snapshot( + block <- roxygen2::parse_text(text)[[1]] + ) + expect_false(roxygen2::block_has_tags(block, "examplesShinylive")) +}) + +test_that("examplesShinylive tag - single occurrence", { + text <- " + #' This is a title + #' + #' This is the description. + #' + #' @param x,y A number + #' @export + #' @examplesShinylive + #' @examples + #' f(1, 2) + f <- function(x, y) x + y + " + expect_silent(block <- roxygen2::parse_text(text)[[1]]) + expect_true(roxygen2::block_has_tags(block, "examplesShinylive")) + expect_length( + roxygen2::block_get_tags(block, "examplesShinylive"), + 1 + ) + expect_identical( + roxygen2::block_get_tag(block, "examplesShinylive")$raw, + "\nf(1, 2)" + ) + expect_identical( + roxygen2::block_get_tag_value(block, "examplesShinylive"), + "https://shinylive.io/r/app/#code=NobwRAdghgtgpmAXGKAHVA6ASmANGAYwHsIAXOMpMAHQgDMAKARlwAIAmASjAF8BdIA" + ) +}) + +test_that("examplesShinylive tag - multiple occurrences", { + text <- " + #' This is a title + #' + #' This is the description. + #' + #' @param x,y A number + #' @examplesShinylive + #' @examples + #' f(1, 2) + #' @examplesShinylive + #' @examples + #' f(1, 3) + f <- function(x, y) x + y + " + expect_silent(block <- roxygen2::parse_text(text)[[1]]) + expect_true(roxygen2::block_has_tags(block, "examplesShinylive")) + expect_length( + roxygen2::block_get_tags(block, "examplesShinylive"), + 2 + ) + expect_identical( + roxygen2::block_get_tags(block, "examplesShinylive")[[1]]$raw, + "\nf(1, 2)" + ) + expect_identical( + roxygen2::block_get_tags(block, "examplesShinylive")[[2]]$raw, + "\nf(1, 3)" + ) + expect_identical( + roxygen2::block_get_tags(block, "examplesShinylive")[[1]]$val, + "https://shinylive.io/r/app/#code=NobwRAdghgtgpmAXGKAHVA6ASmANGAYwHsIAXOMpMAHQgDMAKARlwAIAmASjAF8BdIA" + ) + expect_identical( + roxygen2::block_get_tags(block, "examplesShinylive")[[2]]$val, + "https://shinylive.io/r/app/#code=NobwRAdghgtgpmAXGKAHVA6ASmANGAYwHsIAXOMpMAHQgDMAKARlwAIBmASjAF8BdIA" + ) +}) + +test_that("examplesShinylive tag - don't use previous example code", { + text <- " + #' This is a title + #' + #' This is the description. + #' + #' @param x,y A number + #' @export + #' + #' @examples + #' x <- 'this is excluded' + #' @examplesShinylive + #' @examples + #' f(1, 2) + f <- function(x, y) x + y + " + expect_silent(block <- roxygen2::parse_text(text)[[1]]) + expect_true(roxygen2::block_has_tags(block, "examplesShinylive")) + expect_length( + roxygen2::block_get_tags(block, "examplesShinylive"), + 1 + ) + expect_identical( + roxygen2::block_get_tag(block, "examplesShinylive")$raw, + "\nf(1, 2)" + ) + expect_identical( + roxygen2::block_get_tag_value(block, "examplesShinylive"), + "https://shinylive.io/r/app/#code=NobwRAdghgtgpmAXGKAHVA6ASmANGAYwHsIAXOMpMAHQgDMAKARlwAIAmASjAF8BdIA" + ) +}) + +test_that("examplesShinylive tag - keywords - {{next_example}}", { + text <- " + #' This is a title + #' + #' This is the description. + #' + #' @param x,y A number + #' @export + #' + #' @examplesShinylive + #' {{ next_example }} + #' @examples + #' f(1, 2) + f <- function(x, y) x + y + " + expect_silent(block <- roxygen2::parse_text(text)[[1]]) + expect_true(roxygen2::block_has_tags(block, "examplesShinylive")) + expect_length( + roxygen2::block_get_tags(block, "examplesShinylive"), + 1 + ) + expect_identical( + roxygen2::block_get_tag(block, "examplesShinylive")$raw, + "\nf(1, 2)" + ) + expect_identical( + roxygen2::block_get_tag_value(block, "examplesShinylive"), + "https://shinylive.io/r/app/#code=NobwRAdghgtgpmAXGKAHVA6ASmANGAYwHsIAXOMpMAHQgDMAKARlwAIAmASjAF8BdIA" + ) +}) + +test_that("examplesShinylive tag - keywords - {{prev_example}}", { + text <- " + #' This is a title + #' + #' This is the description. + #' + #' @param x,y A number + #' @export + #' + #' @examples + #' f(1, 2) + #' @examplesShinylive + #' {{ prev_example }} + f <- function(x, y) x + y + " + expect_silent(block <- roxygen2::parse_text(text)[[1]]) + expect_true(roxygen2::block_has_tags(block, "examplesShinylive")) + expect_length( + roxygen2::block_get_tags(block, "examplesShinylive"), + 1 + ) + expect_identical( + roxygen2::block_get_tag(block, "examplesShinylive")$raw, + "\nf(1, 2)" + ) + expect_identical( + roxygen2::block_get_tag_value(block, "examplesShinylive"), + "https://shinylive.io/r/app/#code=NobwRAdghgtgpmAXGKAHVA6ASmANGAYwHsIAXOMpMAHQgDMAKARlwAIAmASjAF8BdIA" + ) +}) + +test_that("examplesShinylive tag - keywords - {{examples}}", { + text <- " + #' This is a title + #' + #' This is the description. + #' + #' @param x,y A number + #' @export + #' + #' @examples + #' f(1, 2) + #' @examplesShinylive + #' {{ examples[[1]] }} + f <- function(x, y) x + y + " + expect_silent(block <- roxygen2::parse_text(text)[[1]]) + expect_true(roxygen2::block_has_tags(block, "examplesShinylive")) + expect_length( + roxygen2::block_get_tags(block, "examplesShinylive"), + 1 + ) + expect_identical( + roxygen2::block_get_tag(block, "examplesShinylive")$raw, + "\nf(1, 2)" + ) + expect_identical( + roxygen2::block_get_tag_value(block, "examplesShinylive"), + "https://shinylive.io/r/app/#code=NobwRAdghgtgpmAXGKAHVA6ASmANGAYwHsIAXOMpMAHQgDMAKARlwAIAmASjAF8BdIA" + ) +}) + +test_that("examplesShinylive tag - keywords - {{tags_examples}}", { + text <- " + #' This is a title + #' + #' This is the description. + #' + #' @param x,y A number + #' @export + #' + #' @examples + #' f(1, 2) + #' @examplesShinylive + #' {{ tags_examples[[1]]$raw }} + f <- function(x, y) x + y + " + expect_silent(block <- roxygen2::parse_text(text)[[1]]) + expect_true(roxygen2::block_has_tags(block, "examplesShinylive")) + expect_length( + roxygen2::block_get_tags(block, "examplesShinylive"), + 1 + ) + expect_identical( + roxygen2::block_get_tag(block, "examplesShinylive")$raw, + "\nf(1, 2)" + ) + expect_identical( + roxygen2::block_get_tag_value(block, "examplesShinylive"), + "https://shinylive.io/r/app/#code=NobwRAdghgtgpmAXGKAHVA6ASmANGAYwHsIAXOMpMAHQgDMAKARlwAIAmASjAF8BdIA" + ) +}) + +test_that("examplesShinylive tag - keywords - error when parsing with glue", { + text <- " + #' This is a title + #' + #' This is the description. + #' + #' @param x,y A number + #' @export + #' @examplesShinylive + #' {{ keyword_not_found }} + f <- function(x, y) x + y + " + expect_snapshot( + block <- roxygen2::parse_text(text)[[1]] + ) + expect_false(roxygen2::block_has_tags(block, "examplesShinylive")) +}) + +test_that("examplesShinylive tag - decorate using {{next_example}} keyword", { + text <- " + #' This is a title + #' + #' This is the description. + #' + #' @param x,y A number + #' @export + #' + #' @example + #' x <- 'this is excluded' + #' @examplesShinylive + #' x1 <- 1 # this is included + #' {{ next_example }} + #' x2 <- 2 # this is included + #' @examples + #' f(1, 2) + f <- function(x, y) x + y + " + expect_silent(block <- roxygen2::parse_text(text)[[1]]) + expect_true(roxygen2::block_has_tags(block, "examplesShinylive")) + expect_length( + roxygen2::block_get_tags(block, "examplesShinylive"), + 1 + ) + expect_identical( + roxygen2::block_get_tag(block, "examplesShinylive")$raw, + "x1 <- 1 # this is included\n\nf(1, 2)\nx2 <- 2 # this is included" + ) + expect_identical( + roxygen2::block_get_tag_value(block, "examplesShinylive"), + "https://shinylive.io/r/app/#code=NobwRAdghgtgpmAXGKAHVA6ASmANGAYwHsIAXOMpMADwEYACAHgFp6GBie0gCwEsBnegKEQCAGwCuAEzhSAOhAUAzABS1c9AEwBKBdU1NWBzj2FnRkmVLABfALpA" # nolint: line_length_linter. + ) +}) + + + +test_that("format returns Rd parsable to HTML", { + testthat::skip_if_not_installed("pkgdown") + + text <- " + #' This is a title + #' + #' This is the description. + #' + #' @param x,y A number + #' @export + #' @examplesShinylive + #' @examples + #' f(1, 2) + f <- function(x, y) x + y + " + + topic <- roxygen2::roc_proc_text(roxygen2::rd_roclet(), text)[[1]] + rd_code <- capture.output(topic$get_section("examplesShinylive")) + suppressWarnings(expect_no_warning(html_code <- pkgdown::rd2html(rd_code), message = ".*unexpected END_OF_INPUT.*")) + expect_gt(length(html_code), 0) +}) + +test_that("format returns Rd parsable to tidy HTML", { + testthat::skip_if_not_installed("pkgdown") + testthat::skip_if_not_installed("withr") + testthat::skip_if_not( + nzchar(Sys.which("tidy")), + "tidy is not installed" + ) + + text <- " + #' This is a title + #' + #' This is the description. + #' + #' @param x,y A number + #' @export + #' @examplesShinylive + #' @examples + #' f(1, 2) + f <- function(x, y) x + y + " + + topic <- roxygen2::roc_proc_text(roxygen2::rd_roclet(), text)[[1]] + rd_code <- capture.output(topic$get_section("examplesShinylive")) + html_code <- paste0(suppressWarnings(pkgdown::rd2html(rd_code)), collapse = "\n") + withr::with_tempfile("x", { + writeLines(html_code, x) + # https://github.com/wch/r-source/blob/7450caaef0076c8b43dfdcc0deab1dbe646b8fc4/src/library/tools/R/htmltools.R#L19 + tidy_res <- suppressWarnings(system2( + "tidy", + c("-language en", "-qe", x), + stdout = TRUE, stderr = TRUE + )) + tidy_res <- grep("line 1 column 1", tidy_res, value = TRUE, invert = TRUE) + testthat::expect_setequal(tidy_res, character(0)) + }) +})