├── .eggignore ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── documentation_issue.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request.md ├── release-template.yml └── workflows │ ├── lint.yml │ ├── ship.yml │ └── test.yml ├── .gitignore ├── Drakefile.ts ├── LICENSE ├── README.md ├── benchmarks ├── README.md └── commands │ └── update │ ├── deps.ts │ └── update_benchmark.ts ├── deps.ts ├── eggs.ts ├── src ├── api │ ├── common.ts │ ├── fetch.ts │ ├── module.ts │ ├── module_test.ts │ └── post.ts ├── commands.ts ├── commands │ ├── info.ts │ ├── init.ts │ ├── install.ts │ ├── link.ts │ ├── publish.ts │ ├── update.ts │ └── upgrade.ts ├── context │ ├── config.ts │ ├── config_test.ts │ ├── context.ts │ ├── files.ts │ ├── ignore.ts │ └── ignore_test.ts ├── keyfile.ts ├── schema.json ├── utilities │ ├── environment.ts │ ├── json.ts │ ├── log.ts │ └── types.ts └── version.ts └── test └── deps.ts /.eggignore: -------------------------------------------------------------------------------- 1 | extends .gitignore 2 | .*ignore 3 | 4 | # Extra typescript files 5 | test/ 6 | *_test.ts 7 | Drakefile.ts 8 | 9 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | ### Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | ### Our Standards 13 | 14 | Examples of behaviour that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behaviour by participants include: 24 | 25 | - The use of sexualised language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ### Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behaviour and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behaviour. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ### Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ### Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behaviour may be 58 | reported by contacting the project team at support@nest.land. All complaints 59 | will be reviewed and investigated and will result in a response that is deemed 60 | necessary and appropriate to the circumstances. The project team is obligated to 61 | maintain confidentiality with regard to the reporter of an incident. Further 62 | details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ### Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 71 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 |

2 | GitHub issues 3 | GitHub closed issues 4 | GitHub contributors 5 |

6 |

7 | Runtime 8 | language 9 |

10 | 11 | # Contributing / Developing 12 | 13 | Contributions are welcome. Fork this repository and issue a pull request with 14 | your changes. 15 | 16 | Please add new tests for new functionality, adapt the existing ones if needed, 17 | and make sure that `deno test` succeeds. 18 | 19 | ### Prerequisites 20 | 21 | You need to have [Git](https://git-scm.com/downloads) and 22 | [deno](https://deno.land) installed on your system. 23 | 24 | ### Setting up Dev 25 | 26 | Just execute these commands: 27 | 28 | ```shell 29 | git clone https://github.com/nestdotland/eggs.git 30 | cd eggs/ 31 | ``` 32 | 33 | This project uses drake to manage project scripts. Run it with Deno: 34 | 35 | ```sh 36 | deno run -A Drakefile.ts 37 | # A shell alias shortcut can be set to run the default drakefile: 38 | alias drake="deno run -A Drakefile.ts" 39 | ``` 40 | 41 | ### Versioning 42 | 43 | We use [SemVer](http://semver.org/) for versioning. For the versions available, 44 | see the [link to tags on this repository](/tags). 45 | 46 | ### Tests 47 | 48 | ```sh 49 | drake test 50 | ``` 51 | 52 | ### Style guide 53 | 54 | ```sh 55 | drake format 56 | drake lint 57 | ``` 58 | 59 | Make sure to use 60 | [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 61 | 62 | ### Pull request 63 | 64 | **Please PR to the `dev` branch!** Then follow the 65 | [pull request template](.github/PULL_REQUEST_TEMPLATE/pull_request.md). 66 | 67 | ### Deploying / Publishing 68 | 69 | Submit a pull request after running `drake dev` to ensure it runs correctly. The 70 | module is automatically published to nest.land when a new release is published 71 | on github. 72 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Submit a bug report to help us improve 4 | labels: 'type: bug, status: needs triage' 5 | --- 6 | 7 | 25 | 26 | ## 🐛 Bug Report 27 | 28 | 29 | 30 | ## To Reproduce 31 | 32 | 33 | 34 | 1. Step 1... 35 | 2. Step 2... 36 | 3. Step 3... 37 | 38 | ## Expected behavior 39 | 40 | 45 | 46 | ## Actual Behavior 47 | 48 | 54 | 55 | ## Environment 56 | 57 | 58 | 59 | - Eggs version: 60 | - Deno version: 61 | - Operating system and version: 62 | 63 | ## Additional context 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 📚 Documentation Issue 3 | about: Report an issue related to documentation 4 | labels: 'meta: documentation, status: needs triage' 5 | --- 6 | 7 | ## 📚 Documentation Issue 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature Request/Idea 3 | about: Suggest a feature or idea for this project 4 | labels: 'type: feature, status: needs triage' 5 | --- 6 | 7 | 16 | 17 | ## 🚀 Feature 18 | 19 | 20 | 21 | ## Motivation 22 | 23 | 24 | 25 | ## Implementation 26 | 27 | 28 | 29 | ## Alternatives 30 | 31 | 32 | 33 | ## Additional context 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Description 6 | 7 | Please include a summary of the change and which issue is fixed. Please also 8 | include relevant motivation and context. List any dependencies that are required 9 | for this change. 10 | 11 | Fixes # (issue) 12 | 13 | ## Type of change 14 | 15 | Please delete options that are not relevant. 16 | 17 | - [ ] Bug fix (non-breaking change which fixes an issue) 18 | - [ ] New feature (non-breaking change which adds functionality) 19 | - [ ] Breaking change (fix or feature that would cause existing functionality to 20 | not work as expected) 21 | - [ ] This change requires a documentation update 22 | 23 | # How Has This Been Tested? 24 | 25 | Please describe the tests that you ran to verify your changes. Provide 26 | instructions so we can reproduce. Please also list any relevant details for your 27 | test configuration 28 | 29 | - [ ] Test A 30 | - [ ] Test B 31 | 32 | **Test Configuration**: 33 | 34 | - OS: 35 | - Version: 36 | 37 | # Checklist: 38 | 39 | - [ ] My code follows the style guidelines of this project 40 | - [ ] I have performed a self-review of my own code 41 | - [ ] I have commented my code, particularly in hard-to-understand areas 42 | - [ ] I have made corresponding changes to the documentation 43 | - [ ] My changes generate no new warnings 44 | - [ ] I have added tests that prove my fix is effective or that my feature works 45 | - [ ] New and existing unit tests pass locally with my changes 46 | -------------------------------------------------------------------------------- /.github/release-template.yml: -------------------------------------------------------------------------------- 1 | name-template: '$RESOLVED_VERSION' 2 | 3 | tag-template: '$RESOLVED_VERSION' 4 | 5 | categories: 6 | - title: '🚀 Features' 7 | labels: 'type: feature' 8 | 9 | - title: '🐛 Bug Fixes' 10 | labels: 'type: bugfix' 11 | 12 | change-template: '- #$NUMBER $TITLE (@$AUTHOR)' 13 | 14 | version-resolver: 15 | major: 16 | labels: 17 | - 'semver: major' 18 | 19 | minor: 20 | labels: 21 | - 'semver: minor' 22 | 23 | patch: 24 | labels: 25 | - 'semver: patch' 26 | 27 | default: patch 28 | 29 | template: | 30 | ## CHANGELOG 31 | 32 | $CHANGES 33 | 34 | ### Install / Upgrade 35 | 36 | **Using Deno** 37 | 38 | ```sh 39 | deno install -Afq --unstable -n eggs https://x.nest.land/eggs@$RESOLVED_VERSION/eggs.ts 40 | ``` 41 | 42 | **Using eggs** 43 | 44 | ```sh 45 | eggs upgrade 46 | ``` 47 | 48 | Special thanks to $CONTRIBUTORS for their contributions to this release! 49 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | fmt: 7 | name: deno fmt 8 | 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 60 11 | 12 | steps: 13 | - name: Setup repo 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup Deno 17 | uses: maximousblk/setup-deno@v1 18 | 19 | - name: Setup Drake 20 | run: deno run -A --unstable Drakefile.ts setup-github-actions 21 | 22 | - name: Check formatting 23 | run: drake check-format 24 | 25 | lint: 26 | name: deno lint 27 | 28 | runs-on: ubuntu-latest 29 | timeout-minutes: 60 30 | 31 | steps: 32 | - name: Setup repo 33 | uses: actions/checkout@v2 34 | 35 | - name: Setup Deno 36 | uses: maximousblk/setup-deno@v1 37 | 38 | - name: Setup Drake 39 | run: deno run -A --unstable Drakefile.ts setup-github-actions 40 | 41 | - name: Run linter 42 | run: drake lint 43 | -------------------------------------------------------------------------------- /.github/workflows/ship.yml: -------------------------------------------------------------------------------- 1 | name: Ship 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | 11 | env: 12 | NESTAPIKEY: ${{ secrets.NESTAPIKEY }} 13 | 14 | steps: 15 | - name: Setup repo 16 | uses: actions/checkout@v2 17 | 18 | - name: Setup Deno 19 | uses: maximousblk/setup-deno@v1 20 | 21 | - name: Setup Drake 22 | run: deno run -A --unstable Drakefile.ts setup-github-actions 23 | 24 | - name: Get eggs version 25 | run: drake get-version 26 | 27 | - name: Draft release 28 | id: draft_release 29 | uses: release-drafter/release-drafter@v5 30 | with: 31 | config-name: release-template.yml 32 | version: ${{ env.EGGS_VERSION }} 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Publish eggs 37 | run: drake ship 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 9 * * *' 8 | 9 | jobs: 10 | stable: 11 | name: Deno Stable 12 | 13 | runs-on: ${{ matrix.os }} 14 | timeout-minutes: 60 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [macOS-latest, windows-latest, ubuntu-latest] 20 | 21 | env: 22 | NESTAPIKEY: ${{ secrets.NESTAPIKEY }} 23 | 24 | steps: 25 | - name: Setup repo 26 | uses: actions/checkout@v2 27 | 28 | - name: Setup Deno 29 | uses: maximousblk/setup-deno@v1 30 | 31 | - name: Setup Drake 32 | run: deno run -A --unstable Drakefile.ts setup-github-actions 33 | 34 | - name: Run tests 35 | run: drake test 36 | 37 | - name: Eggs release test 38 | run: drake dry-ship 39 | 40 | - name: Upload debug file 41 | uses: actions/upload-artifact@v2 42 | if: failure() 43 | with: 44 | name: eggs-debug 45 | path: eggs-debug.log 46 | 47 | canary: 48 | name: Deno Canary 49 | 50 | runs-on: ${{ matrix.os }} 51 | timeout-minutes: 60 52 | 53 | strategy: 54 | fail-fast: false 55 | matrix: 56 | os: [macOS-latest, windows-latest, ubuntu-latest] 57 | 58 | steps: 59 | - name: Setup repo 60 | uses: actions/checkout@v2 61 | 62 | - name: Setup Deno 63 | uses: maximousblk/setup-deno@v1 64 | with: 65 | deno-version: canary 66 | 67 | - name: Setup Drake 68 | run: deno run -A --unstable Drakefile.ts setup-github-actions 69 | 70 | - name: Run tests 71 | run: drake test 72 | 73 | - name: Eggs release test 74 | run: drake dry-ship 75 | 76 | - name: Upload debug file 77 | uses: actions/upload-artifact@v2 78 | if: failure() 79 | with: 80 | name: eggs-debug 81 | path: eggs-debug.log 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editors files 2 | .idea/ 3 | .vscode/ 4 | 5 | # Nest.land - eggs 6 | nestkey.json 7 | eggs-debug.log 8 | 9 | # Benchmarks 10 | benchmarks/** 11 | !benchmarks/README.md 12 | -------------------------------------------------------------------------------- /Drakefile.ts: -------------------------------------------------------------------------------- 1 | import { desc, run, sh, task } from "https://x.nest.land/drake@1.5.0/mod.ts"; 2 | import { version } from "./src/version.ts"; 3 | import { join } from "./deps.ts"; 4 | 5 | const encoder = new TextEncoder(); 6 | 7 | desc("Run tests."); 8 | task("test", [], async function () { 9 | await sh(`deno test -A --unstable`); 10 | }); 11 | 12 | desc("Format source files."); 13 | task("format", [], async function () { 14 | await sh(`deno fmt`); 15 | }); 16 | 17 | desc("Format source files."); 18 | task("check-format", [], async function () { 19 | await sh(`deno fmt --check`); 20 | }); 21 | 22 | desc("Lint source files."); 23 | task("lint", [], async function () { 24 | await sh(`deno lint --unstable`); 25 | }); 26 | 27 | desc("Links the nest.land API key."); 28 | task("link", [], async function () { 29 | await sh( 30 | `deno run -A --unstable eggs.ts link ${ 31 | Deno.env.get("NESTAPIKEY") || 32 | "null" 33 | } -Do`, 34 | ); 35 | }); 36 | 37 | desc("Reports the details of what would have been published."); 38 | task("dry-publish", [], async function () { 39 | await sh( 40 | `deno run -A --unstable eggs.ts publish eggs -DYod --entry eggs.ts --version ${version}-dev --no-check --check-installation`, 41 | ); 42 | }); 43 | 44 | desc("Publishes eggs to the nest.land registry."); 45 | task("publish", [], async function () { 46 | await sh( 47 | `deno run -A --unstable eggs.ts publish eggs -DYo --entry eggs.ts --version ${version} --no-check --check-installation`, 48 | ); 49 | }); 50 | 51 | desc("Reports the details of what would have been shipped."); 52 | task("dry-ship", ["link", "dry-publish"]); 53 | 54 | desc("Ship eggs to nest.land."); 55 | task("ship", ["link", "publish"]); 56 | 57 | task("get-version", [], async function () { 58 | console.log(`Eggs version: ${version}`); 59 | const env = encoder.encode(`\nEGGS_VERSION=${version}\n`); 60 | const GITHUB_ENV = Deno.env.get("GITHUB_ENV"); 61 | if (!GITHUB_ENV) throw new Error("Unable to get Github env"); 62 | await Deno.writeFile(GITHUB_ENV, env, { append: true }); 63 | }); 64 | 65 | task("setup-github-actions", [], async function () { 66 | const process = Deno.run({ 67 | cmd: ["deno", "install", "-A", "--unstable", "-n", "drake", "Drakefile.ts"], 68 | }); 69 | await process.status(); 70 | process.close(); 71 | // https://github.com/denoland/setup-deno/issues/5 72 | const home = Deno.env.get("HOME") ?? // for linux / mac 73 | Deno.env.get("USERPROFILE") ?? // for windows 74 | "/"; 75 | const path = encoder.encode(join(home, ".deno", "bin")); 76 | const GITHUB_PATH = Deno.env.get("GITHUB_PATH"); 77 | if (!GITHUB_PATH) throw new Error("Unable to get Github path"); 78 | await Deno.writeFile(GITHUB_PATH, path, { append: true }); 79 | }); 80 | 81 | desc("Development tools. Should ideally be run before each commit."); 82 | task("dev", ["format", "lint", "test", "dry-publish"]); 83 | 84 | run(); 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 nest.land Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | logo 9 | 10 | 11 |

Eggs CLI

12 |

13 | The CLI used to publish and update modules in nest.land. 14 |

15 |

16 | 17 | nest.land badge 18 | 19 | Eggs lint 23 | Eggs test 27 | Eggs ship 31 | 32 | Discord 36 | 37 |

38 |

39 | 40 | # Contents 41 | 42 | - [Contents](#contents) 43 | - [Installation](#installation) 44 | - [List Of Commands](#list-of-commands) 45 | - [Link](#link) 46 | - [Init](#init) 47 | - [Publish](#publish) 48 | - [Update](#update) 49 | - [Install](#install) 50 | - [Upgrade](#upgrade) 51 | - [Contributing](#contributing) 52 | 53 | ## Installation 54 | 55 | **Note: You need to upgrade to Deno v1.6.0 or newer in order to use our CLI.** 56 | 57 | ```shell script 58 | deno install -Afq --unstable https://x.nest.land/eggs@0.3.10/eggs.ts 59 | ``` 60 | 61 | For more information, see the [documentation](https://docs.nest.land/). 62 | 63 | ## List Of Commands 64 | 65 | ### Link 66 | 67 | Before publishing a package to our registry, you'll need to get an API key. 68 | Visit [nest.land](https://nest.land/#start) to generate one. 69 | 70 | Then, use `link` to add it to the CLI: 71 | 72 | ```shell script 73 | eggs link 74 | ``` 75 | 76 | Alternatively, you can manually create a `.nest-api-key` file at your user home 77 | directory. 78 | 79 | ### Init 80 | 81 | To publish a package, you need to create an `egg.json` file at the root of your 82 | project as well. To do this easily, type: 83 | 84 | ```shell script 85 | eggs init 86 | ``` 87 | 88 | Note: If you'd like to specify a version that you'll publish to, you can include 89 | a `version` variable in `egg.json`. 90 | 91 | ### Publish 92 | 93 | After you've filled in the information located in `egg.json`, you can publish 94 | your package to our registry with this command: 95 | 96 | ```shell script 97 | eggs publish 98 | ``` 99 | 100 | You'll receive a link to your package on our registry, along with an import URL 101 | for others to import your package from the Arweave blockchain! 102 | 103 | Note: It may take some time for the transaction to process in Arweave. Until 104 | then, we upload your files to our server, where they are served for 20 minutes 105 | to give the transaction time to process. 106 | 107 | ### Update 108 | 109 | You can easily update your dependencies and global scripts with the `update` 110 | command. 111 | 112 | ```shell script 113 | eggs update [deps] 114 | ``` 115 | 116 | Your dependencies are by default checked in the `deps.ts` file (current working 117 | directory). You can change this with `--file` 118 | 119 | ```shell script 120 | eggs update # default to deps.ts 121 | eggs update --file dependencies.ts 122 | ``` 123 | 124 | In regular mode, all your dependencies are updated. You can choose which ones 125 | will be modified by adding them as arguments. 126 | 127 | ```shell script 128 | eggs update # Updates everything 129 | eggs update http fs eggs # Updates only http, fs, eggs 130 | ``` 131 | 132 | Several registries are supported. The current ones are: 133 | 134 | - x.nest.land 135 | - deno.land/x 136 | - deno.land/std 137 | - raw.githubusercontent.com 138 | - denopkg.com 139 | 140 | If you want to add a registry, open an issue by specifying the registry url and 141 | we'll add it. 142 | 143 | An example of updated file: 144 | 145 | ```ts 146 | import * as colors from "https://deno.land/std@v0.55.0/fmt/colors.ts"; 147 | import * as bcrypt from "https://deno.land/x/bcrypt@v0.2.0/mod.ts"; 148 | import * as eggs from "https://x.nest.land/eggs@v0.1.0/mod.ts"; 149 | import * as http from "https://deno.land/std/http/mod.ts"; 150 | ``` 151 | 152 | After `eggs update`: 153 | 154 | ```ts 155 | import * as colors from "https://deno.land/std@0.58.0/fmt/colors.ts"; 156 | import * as bcrypt from "https://deno.land/x/bcrypt@v0.2.1/mod.ts"; 157 | import * as eggs from "https://x.nest.land/eggs@0.3.0/mod.ts"; 158 | import * as http from "https://deno.land/std/http/mod.ts"; 159 | ``` 160 | 161 | ### Install 162 | 163 | Just like `deno install`, you can install scripts globally with eggs. By 164 | installing it this way, you will be notified if an update is available for your 165 | script. 166 | 167 | The verification is smart, it can't be done more than once a day. To install a 168 | script, simply replace `deno` with `eggs`. 169 | 170 | ```shell script 171 | deno install --allow-write --allow-read -n [NAME] https://x.nest.land/[MODULE]@[VERSION]/cli.ts 172 | ``` 173 | 174 | Becomes 175 | 176 | ```shell script 177 | eggs install --allow-write --allow-read -n [NAME] https://x.nest.land/[MODULE]@[VERSION]/cli.ts 178 | ``` 179 | 180 | The supported registries are the same as for the update command. 181 | 182 | ### Upgrade 183 | 184 | To upgrade the eggs CLI, use the command shown: 185 | 186 | ```shell script 187 | eggs upgrade 188 | ``` 189 | 190 | ## Contributing 191 | 192 | All contributions are welcome! If you can think of a command or feature that 193 | might benefit nest.land, fork this repository and make a pull request from your 194 | branch with the additions. Make sure to use 195 | [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 196 | 197 | [Contribution guide](.github/CONTRIBUTING.md) 198 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | Documentation for benchmarks of CLI commands. 4 | 5 | # Contents 6 | 7 | - [Commands](#commands) 8 | - [Update](#update) 9 | 10 | # Commands 11 | 12 | ## Update 13 | 14 | **Cautions** 15 | 16 | - There may be a time where the deps are too out of date. They need to be out of 17 | date for `update` to update them, so just bump them up to the release before 18 | the latest std 19 | 20 | **How it Works** 21 | 22 | - This benchmark uses a separate `deps.ts` file. The reason for this is so we 23 | can have a 'realworld' example, where many dependencies are used, and from 24 | various registries 25 | 26 | - All but 1 dependency are out of date, to fully represent the test, as the 27 | `update` would update all the out of date dependencies 28 | 29 | - This benchmark shows the execution time for running `update` against 14 30 | separate dependencies, all varying from: 31 | - deno.land/std/ 32 | - deno.land/x/ 33 | - x.nest.land 34 | 35 | **How to Run** 36 | 37 | ``` 38 | cd nest.land/eggs/benchmarks/commands/update 39 | deno run --allow-run update_benchmark.ts 40 | ``` 41 | 42 | **Result** 43 | 44 | Ran at 12/06/2018 (12th of June) 45 | 46 | Ran on a MacBook Pro, 2018, 2.3GHz Intel Core i5, 8GB RAM 47 | 48 | ``` 49 | Edwards-MacBook-Pro:update edwardbebbington$ deno run --allow-run update_benchmark.ts 50 | Compile file:///Users/edwardbebbington/Development/docker/environments/nest.land/eggs/benchmarks/commands/update/update_benchmark.ts 51 | running 1 benchmark ... 52 | benchmark updateCommand ... 53 | 3968ms 54 | benchmark result: DONE. 1 measured; 0 filtered 55 | ``` 56 | -------------------------------------------------------------------------------- /benchmarks/commands/update/deps.ts: -------------------------------------------------------------------------------- 1 | // std 2 | export { 3 | bench, 4 | runBenchmarks, 5 | } from "https://x.nest.land/std@0.61.0/testing/bench.ts"; 6 | import * as _archive from "https://x.nest.land/std@0.61.0/archive/tar.ts"; 7 | import * as _denoSync from "https://x.nest.land/std@0.61.0/async/mod.ts"; 8 | import * as _bytes from "https://x.nest.land/std@0.61.0/bytes/mod.ts"; 9 | import * as _dateTime from "https://x.nest.land/std@0.61.0/datetime/mod.ts"; 10 | import * as _colors from "https://x.nest.land/std@0.61.0/fmt/colors.ts"; 11 | import * as _sha512 from "https://x.nest.land/std@0.61.0/hash/sha512.ts"; 12 | import * as _http from "https://x.nest.land/std@0.61.0/http/mod.ts"; 13 | import * as _io from "https://x.nest.land/std@0.61.0/io/mod.ts"; 14 | import * as _log from "https://x.nest.land/std@0.61.0/log/mod.ts"; 15 | 16 | // deno land 3rd party 17 | import * as _dmm from "https://deno.land/x/dmm@v1.0.2/mod.ts"; 18 | import * as _drash from "https://deno.land/x/drash@v1.0.2/mod.ts"; 19 | import * as _bcrypt from "https://deno.land/x/bcrypt@v0.2.0/mod.ts"; 20 | -------------------------------------------------------------------------------- /benchmarks/commands/update/update_benchmark.ts: -------------------------------------------------------------------------------- 1 | import { bench, runBenchmarks } from "./deps.ts"; 2 | 3 | bench(async function updateCommand(b): Promise { 4 | b.start(); 5 | const p = await Deno.run({ 6 | cmd: [ 7 | "deno", 8 | "run", 9 | "--allow-net", 10 | "--allow-read", 11 | "--allow-write", 12 | "../../../src/main.ts", 13 | "update", 14 | ], 15 | stdout: "null", 16 | }); 17 | await p.status(); 18 | b.stop(); 19 | }); 20 | 21 | runBenchmarks(); 22 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | /**************** std ****************/ 2 | export { 3 | basename, 4 | dirname, 5 | extname, 6 | fromFileUrl, 7 | globToRegExp, 8 | isAbsolute, 9 | join, 10 | relative, 11 | resolve, 12 | } from "https://x.nest.land/std@0.113.0/path/mod.ts"; 13 | 14 | export { 15 | exists, 16 | existsSync, 17 | expandGlob, 18 | expandGlobSync, 19 | walkSync, 20 | } from "https://x.nest.land/std@0.113.0/fs/mod.ts"; 21 | 22 | export * as log from "https://x.nest.land/std@0.113.0/log/mod.ts"; 23 | 24 | export { LogRecord } from "https://x.nest.land/std@0.113.0/log/logger.ts"; 25 | 26 | export type { LevelName } from "https://x.nest.land/std@0.113.0/log/levels.ts"; 27 | export { LogLevels } from "https://x.nest.land/std@0.113.0/log/levels.ts"; 28 | 29 | export { BaseHandler } from "https://x.nest.land/std@0.113.0/log/handlers.ts"; 30 | 31 | export * from "https://x.nest.land/std@0.113.0/fmt/colors.ts"; 32 | 33 | export { 34 | assert, 35 | assertEquals, 36 | assertMatch, 37 | } from "https://x.nest.land/std@0.113.0/testing/asserts.ts"; 38 | 39 | export { 40 | parse as parseYaml, 41 | stringify as stringifyYaml, 42 | } from "https://x.nest.land/std@0.113.0/encoding/yaml.ts"; 43 | 44 | /**************** cliffy ****************/ 45 | export { 46 | Command, 47 | CompletionsCommand, 48 | HelpCommand, 49 | } from "https://x.nest.land/cliffy@0.20.1/command/mod.ts"; 50 | 51 | export { string as stringType } from "https://x.nest.land/cliffy@0.20.1/flags/types/string.ts"; 52 | 53 | export { 54 | Checkbox, 55 | Confirm, 56 | Input, 57 | List, 58 | Select, 59 | } from "https://x.nest.land/cliffy@0.20.1/prompt/mod.ts"; 60 | 61 | export type { ITypeInfo } from "https://x.nest.land/cliffy@0.20.1/flags/types.ts"; 62 | 63 | /**************** semver ****************/ 64 | export * as semver from "https://deno.land/x/semver@v1.4.0/mod.ts"; 65 | 66 | /**************** base64 ****************/ 67 | export * as base64 from "https://denopkg.com/chiefbiiko/base64@v0.2.0/mod.ts"; 68 | 69 | /**************** hatcher ****************/ 70 | export { 71 | latestVersion, 72 | NestLand, 73 | parseURL, 74 | UpdateNotifier, 75 | } from "https://x.nest.land/hatcher@0.10.2/mod.ts"; 76 | 77 | export { isVersionUnstable } from "https://x.nest.land/hatcher@0.10.2/lib/utilities/utils.ts"; 78 | 79 | export { install as installHatcher } from "https://x.nest.land/hatcher@0.10.2/lib/cli.ts"; 80 | 81 | /**************** analyzer ****************/ 82 | export type { DependencyTree } from "https://x.nest.land/analyzer@0.0.6/deno/tree.ts"; 83 | export { dependencyTree } from "https://x.nest.land/analyzer@0.0.6/deno/tree.ts"; 84 | 85 | /**************** wait ****************/ 86 | export { Spinner, wait } from "https://deno.land/x/wait@0.1.12/mod.ts"; 87 | -------------------------------------------------------------------------------- /eggs.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Command, 3 | CompletionsCommand, 4 | HelpCommand, 5 | log, 6 | NestLand, 7 | UpdateNotifier, 8 | } from "./deps.ts"; 9 | import type { DefaultOptions } from "./src/commands.ts"; 10 | import { linkCommand } from "./src/commands/link.ts"; 11 | import { initCommand } from "./src/commands/init.ts"; 12 | import { publishCommand } from "./src/commands/publish.ts"; 13 | import { updateCommand } from "./src/commands/update.ts"; 14 | import { installCommand } from "./src/commands/install.ts"; 15 | import { upgradeCommand } from "./src/commands/upgrade.ts"; 16 | import { infoCommand } from "./src/commands/info.ts"; 17 | 18 | import { version } from "./src/version.ts"; 19 | 20 | import { 21 | errorOccurred, 22 | handleError, 23 | setupLog, 24 | writeLogFile, 25 | } from "./src/utilities/log.ts"; 26 | 27 | const commands = { 28 | link: linkCommand, 29 | init: initCommand, 30 | publish: publishCommand, 31 | update: updateCommand, 32 | install: installCommand, 33 | upgrade: upgradeCommand, 34 | info: infoCommand, 35 | }; 36 | 37 | await setupLog(); 38 | 39 | const notifier = new UpdateNotifier({ 40 | name: "eggs", 41 | registry: NestLand, 42 | currentVersion: version, 43 | }); 44 | 45 | const checkForUpdates = notifier.checkForUpdates(); 46 | 47 | const eggs = new Command() 48 | .throwErrors() 49 | .name("eggs") 50 | .version(version) 51 | .description( 52 | "nest.land - A module registry and CDN for Deno, on the permaweb", 53 | ) 54 | .option("-D, --debug", "Print additional information.", { global: true }) 55 | .option( 56 | "-o, --output-log", 57 | "Create a log file after command completion.", 58 | { global: true }, 59 | ) 60 | .action(() => { 61 | eggs.showHelp(); 62 | }) 63 | .command("help", new HelpCommand()) 64 | .command("completions", new CompletionsCommand()) 65 | .command("link", linkCommand) 66 | .command("init", initCommand) 67 | .command("publish", publishCommand) 68 | .command("update", updateCommand) 69 | .command("install", installCommand) 70 | .command("info", infoCommand) 71 | .command("upgrade", upgradeCommand); 72 | 73 | try { 74 | const { options } = await eggs.parse(Deno.args); 75 | 76 | if (options.outputLog) { 77 | await writeLogFile(); 78 | } 79 | await notification(); 80 | 81 | if (errorOccurred) { 82 | Deno.exit(1); 83 | } 84 | 85 | Deno.exit(); 86 | } catch (err) { 87 | if ( 88 | err.message.match( 89 | /^(Unknown option:|Unknown command:|Option --|Missing value for option:|Missing argument\(s\):)/, 90 | ) 91 | ) { 92 | const command = Deno.args[0] as keyof typeof commands; 93 | if (command in commands) { 94 | commands[command].showHelp(); 95 | } else { 96 | eggs.showHelp(); 97 | } 98 | log.error(err.message); 99 | } else { 100 | await handleError(err); 101 | } 102 | 103 | await notification(); 104 | Deno.exit(1); 105 | } 106 | 107 | async function notification() { 108 | await checkForUpdates; 109 | notifier.notify("eggs upgrade"); 110 | } 111 | -------------------------------------------------------------------------------- /src/api/common.ts: -------------------------------------------------------------------------------- 1 | import { envENDPOINT } from "../utilities/environment.ts"; 2 | 3 | export const ENDPOINT = envENDPOINT(); 4 | 5 | // TODO(@qu4k): develop mock api 6 | let _MOCK = false; 7 | 8 | export function enableMockApi() { 9 | _MOCK = true; 10 | } 11 | 12 | export async function apiFetch( 13 | input: string, 14 | init?: RequestInit, 15 | ): Promise { 16 | return await fetch(input, init); 17 | } 18 | -------------------------------------------------------------------------------- /src/api/fetch.ts: -------------------------------------------------------------------------------- 1 | import { IModule, Module } from "./module.ts"; 2 | import { apiFetch, ENDPOINT } from "./common.ts"; 3 | 4 | export async function fetchResource(query: string): Promise { 5 | // TODO(@qu4k): add test resource 6 | try { 7 | const response = await apiFetch(`${ENDPOINT}${query}`); 8 | if (!response || !response.ok) return undefined; 9 | const value = await response.json(); 10 | return value as T; 11 | } catch { 12 | return undefined; 13 | } 14 | } 15 | 16 | export async function fetchModule(name: string): Promise { 17 | const module: IModule | undefined = await fetchResource( 18 | `/api/package/${name}`, 19 | ); 20 | if (!module) return undefined; 21 | return new Module(module); 22 | } 23 | -------------------------------------------------------------------------------- /src/api/module.ts: -------------------------------------------------------------------------------- 1 | import { semver } from "../../deps.ts"; 2 | 3 | export interface IModule { 4 | name: string; 5 | owner: string; 6 | description: string; 7 | repository?: string; 8 | latestVersion?: string; 9 | latestStableVersion?: string; 10 | packageUploadNames: string[]; 11 | } 12 | 13 | export class Module implements IModule { 14 | name: string; 15 | owner: string; 16 | description: string; 17 | repository?: string; 18 | latestVersion?: string; 19 | latestStableVersion?: string; 20 | packageUploadNames: string[]; 21 | 22 | constructor(module: IModule) { 23 | this.name = module.name; 24 | this.owner = module.owner; 25 | this.description = module.description; 26 | this.repository = module.repository; 27 | this.latestVersion = module.latestVersion; 28 | this.latestStableVersion = module.latestStableVersion; 29 | this.packageUploadNames = module.packageUploadNames; 30 | } 31 | 32 | getLatestVersion(): string { 33 | function vn(n: string): string { 34 | return n.split("@")[1]; 35 | } 36 | 37 | function cmp(a: string, b: string): number { 38 | return -(semver.compare(vn(a), vn(b))); 39 | } 40 | 41 | let latest: string | undefined; 42 | 43 | if (this.packageUploadNames.length > 0) { 44 | const sorted = this.packageUploadNames.sort(cmp); 45 | latest = vn(sorted[0]); 46 | } 47 | 48 | if (!latest && this.latestVersion) { 49 | latest = vn(this.latestVersion); 50 | } else if (!latest && this.latestStableVersion) { 51 | latest = vn(this.latestStableVersion); 52 | } 53 | 54 | return latest ?? "0.0.0"; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/api/module_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "../../test/deps.ts"; 2 | import { Module } from "./module.ts"; 3 | 4 | Deno.test({ 5 | name: "internal | module | versioning", 6 | fn(): void { 7 | const eggs = new Module({ 8 | name: "eggs", 9 | owner: "nest-land", 10 | description: "The CLI used to publish and update packages in nest.land.", 11 | latestVersion: undefined, 12 | latestStableVersion: "eggs@0.1.8", 13 | packageUploadNames: [ 14 | "eggs@0.1.7", 15 | "eggs@0.1.8", 16 | "eggs@0.1.9-rc1", 17 | ], 18 | }); 19 | 20 | const mazeGenerator = new Module({ 21 | name: "maze_generator", 22 | owner: "TheWizardBear", 23 | description: "A module for generating mazes", 24 | latestVersion: "maze_generator@0.1.0-alpha.0", 25 | latestStableVersion: undefined, 26 | packageUploadNames: [ 27 | "maze_generator@0.0.8", 28 | "maze_generator@0.1.0-alpha.0", 29 | ], 30 | }); 31 | 32 | assertEquals(eggs.getLatestVersion(), "0.1.9-rc1"); 33 | assertEquals(mazeGenerator.getLatestVersion(), "0.1.0-alpha.0"); 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/api/post.ts: -------------------------------------------------------------------------------- 1 | import { apiFetch, ENDPOINT } from "./common.ts"; 2 | 3 | type StringMap = { 4 | [key: string]: string; 5 | }; 6 | 7 | export async function postResource( 8 | query: string, 9 | headers: StringMap, 10 | data: Record, 11 | ): Promise { 12 | // TODO(@qu4k): add test resource 13 | try { 14 | const response = await apiFetch(`${ENDPOINT}${query}`, { 15 | method: "POST", 16 | headers: { 17 | "Content-Type": "application/json", 18 | ...headers, 19 | }, 20 | body: JSON.stringify(data), 21 | }); 22 | 23 | if (!response || !response.ok) return undefined; 24 | const value = await response.json(); 25 | return value as T; 26 | } catch { 27 | return undefined; 28 | } 29 | } 30 | 31 | export interface PublishResponse { 32 | token: string; 33 | name: string; 34 | version: string; 35 | owner: string; 36 | } 37 | 38 | export interface PublishModule extends Record { 39 | name: string; 40 | description?: string; 41 | repository?: string; 42 | version: string; 43 | unlisted?: boolean; 44 | upload: boolean; 45 | entry?: string; 46 | latest?: boolean; 47 | stable?: boolean; 48 | } 49 | 50 | export async function postPublishModule( 51 | key: string, 52 | module: PublishModule, 53 | ): Promise { 54 | const response: PublishResponse | undefined = await postResource( 55 | "/api/publish", 56 | { "Authorization": key }, 57 | module, 58 | ); 59 | return response; 60 | } 61 | 62 | export interface PiecesResponse { 63 | name: string; 64 | files: { [x: string]: string }; 65 | } 66 | 67 | export async function postPieces( 68 | uploadToken: string, 69 | pieces: StringMap, 70 | ): Promise { 71 | const response: PiecesResponse | undefined = await postResource( 72 | "/api/piece", 73 | { "X-UploadToken": uploadToken }, 74 | { 75 | pieces, 76 | end: true, 77 | }, 78 | ); 79 | return response; 80 | } 81 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | export interface DefaultOptions { 2 | debug?: boolean; 3 | outputLog?: boolean; 4 | } 5 | -------------------------------------------------------------------------------- /src/commands/info.ts: -------------------------------------------------------------------------------- 1 | import { 2 | blue, 3 | bold, 4 | Command, 5 | cyan, 6 | DependencyTree, 7 | dependencyTree, 8 | gray, 9 | green, 10 | italic, 11 | log, 12 | magenta, 13 | parseURL, 14 | red, 15 | resolve, 16 | yellow, 17 | } from "../../deps.ts"; 18 | import type { DefaultOptions } from "../commands.ts"; 19 | import { version } from "../version.ts"; 20 | import { setupLog, spinner } from "../utilities/log.ts"; 21 | 22 | const format = { 23 | redundant: gray("..."), 24 | circular: red("circular import"), 25 | local: bold("local"), 26 | nestLand: magenta("nest.land"), 27 | denoLand: cyan("deno.land"), 28 | github: blue("github.com"), 29 | denopkgCom: green("denopkg.com"), 30 | skypack: magenta("cdn.skypack.dev"), 31 | jspm: yellow("jspm.dev"), 32 | }; 33 | 34 | /** Info Command. */ 35 | export async function info(options: Options, file?: string) { 36 | await setupLog(options.debug); 37 | 38 | if (file) { 39 | const path = file.match(/https?:\/\//) ? file : resolve(Deno.cwd(), file); 40 | 41 | const wait = spinner.info("Scanning dependency tree..."); 42 | 43 | let importsFound = 0; 44 | let importsResolved = 0; 45 | 46 | const progress = () => 47 | wait.text = `${importsResolved} / ${importsFound} imports`; 48 | const onImportFound = (count: number) => { 49 | importsFound = count; 50 | progress(); 51 | }; 52 | const onImportResolved = (count: number) => { 53 | importsResolved = count; 54 | progress(); 55 | }; 56 | 57 | const deps = await dependencyTree( 58 | path, 59 | { 60 | fullTree: options.full, 61 | onImportFound, 62 | onImportResolved, 63 | }, 64 | ); 65 | wait.stop(); 66 | log.debug("Dependency tree", deps.tree[0]); 67 | prettyTree(deps.tree[0].path, deps.tree[0].imports, "", true, options); 68 | 69 | log.info(""); 70 | log.info(`Found ${deps.count} dependencies.`); 71 | if (deps.circular) { 72 | log.warning("This dependency tree contains circular imports!"); 73 | } 74 | } else { 75 | const info = Deno.run({ 76 | cmd: ["deno", "info"], 77 | stderr: "inherit", 78 | stdout: "inherit", 79 | }); 80 | 81 | await info.status(); 82 | info.close(); 83 | } 84 | } 85 | 86 | function prettyTree( 87 | name: string, 88 | tree: DependencyTree, 89 | indent: string, 90 | last: boolean, 91 | options: Options, 92 | ) { 93 | let line = indent; 94 | if (last) { 95 | line += "└─" + (tree.length > 0 ? "┬" : "─"); 96 | indent += " "; 97 | } else { 98 | line += "├─" + (tree.length > 0 ? "┬" : "─"); 99 | indent += "│ "; 100 | } 101 | 102 | console.log(`${line} ${options.raw ? name : beautifyDependency(name)}`); 103 | 104 | for (let i = 0; i < tree.length; i++) { 105 | const { path, imports } = tree[i]; 106 | prettyTree(path, imports, indent, i === tree.length - 1, options); 107 | } 108 | } 109 | 110 | function formatVersion(version: string) { 111 | if (version === "" || version === undefined) { 112 | return red(italic("latest")); 113 | } 114 | return italic(version); 115 | } 116 | 117 | function formatPath(path: string) { 118 | return gray(italic(path)); 119 | } 120 | 121 | function beautifyDependency(dep: string) { 122 | if (dep.match(/^\[Error/)) { 123 | return red(dep); 124 | } 125 | if (dep.match(/^\[Redundant/)) { 126 | return format.redundant; 127 | } 128 | if (dep.match(/^\[Circular/)) { 129 | return format.circular; 130 | } 131 | if (dep.match(/^file:\/\/\//)) { 132 | return `${format.local} ${gray(italic(dep.split("file:///")[1]))}`; 133 | } 134 | try { 135 | const { registry, name, version, owner, relativePath } = parseURL(dep); 136 | switch (registry) { 137 | case "x.nest.land": 138 | return `${format.nestLand} ${bold(name)} ${formatVersion(version)} ${ 139 | formatPath(relativePath) 140 | }`; 141 | 142 | case "deno.land": 143 | return `${format.denoLand} ${bold(name)} ${formatVersion(version)} ${ 144 | formatPath(relativePath) 145 | }`; 146 | 147 | case "raw.githubusercontent.com": 148 | return `${format.github} ${bold(`${owner}/${name}`)} ${ 149 | formatVersion(version) 150 | } ${formatPath(relativePath)}`; 151 | 152 | case "denopkg.com": 153 | return `${format.denopkgCom} ${bold(`${owner}/${name}`)} ${ 154 | formatVersion(version) 155 | } ${formatPath(relativePath)}`; 156 | 157 | case "cdn.skypack.dev": 158 | return `${format.skypack} ${bold(name)} ${formatVersion(version)}`; 159 | 160 | case "jspm.dev": 161 | return `${format.jspm} ${bold(name)} ${formatVersion(version)}`; 162 | 163 | default: 164 | return dep; 165 | } 166 | } catch { 167 | return dep; 168 | } 169 | } 170 | 171 | export interface Options extends DefaultOptions { 172 | full: boolean; 173 | raw: boolean; 174 | } 175 | export type Arguments = [string]; 176 | 177 | export const infoCommand = new Command() 178 | .version(version) 179 | .arguments("[file:string]") 180 | .option( 181 | "-f, --full", 182 | "Displays the complete tree, without hiding redundant imports. Not recommended for large trees.", 183 | ) 184 | .option( 185 | "-r, --raw", 186 | "Displays the raw URLs, without modifying them.", 187 | ) 188 | .description( 189 | "Displays the dependency tree of a file in a more readable, colorful way. Useful when you have imports that redirect urls (like nest.land).", 190 | ) 191 | .action(info); 192 | -------------------------------------------------------------------------------- /src/commands/init.ts: -------------------------------------------------------------------------------- 1 | import { 2 | basename, 3 | Command, 4 | Confirm, 5 | Input, 6 | List, 7 | log, 8 | Select, 9 | } from "../../deps.ts"; 10 | import { 11 | Config, 12 | ConfigFormat, 13 | configFormat, 14 | defaultConfig, 15 | readConfig, 16 | writeConfig, 17 | } from "../context/config.ts"; 18 | import type { semver } from "../../deps.ts"; 19 | import { validateURL, validateVersion } from "../utilities/types.ts"; 20 | import { fetchModule } from "../api/fetch.ts"; 21 | import type { DefaultOptions } from "../commands.ts"; 22 | import { version as eggsVersion } from "../version.ts"; 23 | import { setupLog } from "../utilities/log.ts"; 24 | 25 | /** Init Command. 26 | * `init` creates (or overrides) configuration in 27 | * the cwd with an interactive prompt. */ 28 | export async function init(options: Options) { 29 | await setupLog(options.debug); 30 | 31 | let currentConfig: Partial = {}; 32 | 33 | const configPath = await defaultConfig(); 34 | if (configPath) { 35 | log.warning("An egg config file already exists..."); 36 | const override = await Confirm.prompt("Do you want to override it?"); 37 | if (!override) return; 38 | currentConfig = await readConfig(configPath); 39 | } 40 | 41 | const name: string = await Input.prompt({ 42 | message: "Name:", 43 | default: currentConfig.name || basename(Deno.cwd()), 44 | minLength: 2, 45 | maxLength: 40, 46 | }); 47 | 48 | const existing = await fetchModule(name); 49 | 50 | const entry: string | undefined = await Input.prompt({ 51 | message: "Entry file:", 52 | default: currentConfig.entry ?? "./mod.ts", 53 | }); 54 | 55 | const description: string | undefined = await Input.prompt({ 56 | message: "Description:", 57 | default: currentConfig.description ?? existing?.description ?? "", 58 | }); 59 | 60 | const homepage: string | undefined = await Input.prompt({ 61 | message: "Module homepage:", 62 | default: currentConfig.homepage ?? existing?.repository ?? "", 63 | validate: (value: string) => value === "" || validateURL(value), 64 | }); 65 | 66 | let releaseType: string | null | undefined = await Select.prompt({ 67 | message: "Automatic semver increment:", 68 | options: [ 69 | { name: "disabled", value: "none" }, 70 | Select.separator("--------"), 71 | { name: "patch", value: "patch" }, 72 | { name: "minor", value: "minor" }, 73 | { name: "major", value: "major" }, 74 | Select.separator("--------"), 75 | { name: "pre", value: "pre" }, 76 | { name: "prepatch", value: "prepatch" }, 77 | { name: "preminor", value: "preminor" }, 78 | { name: "premajor", value: "premajor" }, 79 | { name: "prerelease", value: "prerelease" }, 80 | ], 81 | default: "none", 82 | keys: { 83 | previous: ["up", "8", "u", "k"], 84 | next: ["down", "2", "d", "j"], 85 | }, 86 | }); 87 | if (releaseType === "none") releaseType = null; 88 | 89 | const version: string | undefined = await Input.prompt({ 90 | message: "Version:", 91 | default: currentConfig.version ?? existing?.getLatestVersion() ?? "", 92 | validate: (value: string) => value === "" || validateVersion(value), 93 | }); 94 | 95 | const unstable: boolean | undefined = await Confirm.prompt({ 96 | message: "Is this an unstable version?", 97 | default: currentConfig.unstable ?? false, 98 | }); 99 | 100 | const unlisted: boolean | undefined = await Confirm.prompt({ 101 | message: "Should this module be hidden in the gallery?", 102 | default: currentConfig.unlisted ?? false, 103 | }); 104 | 105 | let files: string[] | undefined = await List.prompt({ 106 | message: "Files and relative directories to publish, separated by a comma:", 107 | default: currentConfig.files ?? [], 108 | }); 109 | if (files?.length === 1 && files[0] === "") files = []; 110 | 111 | let ignore: string[] | undefined = await List.prompt({ 112 | message: "Files and relative directories to ignore, separated by a comma:", 113 | default: currentConfig.ignore ?? [], 114 | }); 115 | if (ignore?.length === 1 && ignore[0] === "") ignore = []; 116 | 117 | const check: boolean | undefined = await Confirm.prompt({ 118 | message: "Perform all checks before publication?", 119 | default: currentConfig.check ?? false, 120 | }); 121 | const noCheck = !check; 122 | 123 | let checkFormat: boolean | string | undefined = noCheck && 124 | (await Confirm.prompt({ 125 | message: "Check source files formatting before publication?", 126 | default: !!currentConfig.checkFormat ?? false, 127 | })) 128 | ? await Input.prompt({ 129 | message: "Formatting command (leave blank for default):", 130 | default: typeof currentConfig.checkFormat === "string" 131 | ? currentConfig.checkFormat 132 | : undefined, 133 | }) 134 | : false; 135 | if (checkFormat === "") checkFormat = true; 136 | 137 | let checkTests: boolean | string | undefined = noCheck && 138 | (await Confirm.prompt({ 139 | message: "Test your code before publication?", 140 | default: !!currentConfig.checkTests ?? false, 141 | })) 142 | ? await Input.prompt({ 143 | message: "Testing command (leave blank for default):", 144 | default: typeof currentConfig.checkTests === "string" 145 | ? currentConfig.checkTests 146 | : undefined, 147 | }) 148 | : false; 149 | if (checkTests === "") checkTests = true; 150 | 151 | const checkInstallation: boolean | undefined = noCheck && 152 | (await Confirm.prompt({ 153 | message: "Install module and check for missing files before publication?", 154 | default: currentConfig.checkInstallation ?? false, 155 | })); 156 | 157 | const format = await Select.prompt({ 158 | message: "Config format: ", 159 | default: configPath 160 | ? configFormat(configPath).toUpperCase() 161 | : ConfigFormat.JSON, 162 | options: [ 163 | { name: "YAML", value: ConfigFormat.YAML }, 164 | { name: "JSON", value: ConfigFormat.JSON }, 165 | ], 166 | keys: { 167 | previous: ["up", "8", "u", "k"], 168 | next: ["down", "2", "d", "j"], 169 | }, 170 | }); 171 | 172 | const config: Partial = { 173 | $schema: `https://x.nest.land/eggs@${eggsVersion}/src/schema.json`, 174 | name, 175 | entry, 176 | description, 177 | homepage, 178 | version, 179 | releaseType: releaseType as semver.ReleaseType, 180 | unstable, 181 | unlisted, 182 | files, 183 | ignore, 184 | checkFormat, 185 | checkTests, 186 | checkInstallation, 187 | check, 188 | }; 189 | 190 | log.debug("Config: ", config, format); 191 | 192 | await writeConfig(config, format as ConfigFormat); 193 | 194 | log.info("Successfully created config file."); 195 | } 196 | 197 | export type Options = DefaultOptions; 198 | export type Arguments = []; 199 | 200 | export const initCommand = new Command() 201 | .version(eggsVersion) 202 | .description("Initiates a new module for the nest.land registry.") 203 | .action(init); 204 | -------------------------------------------------------------------------------- /src/commands/install.ts: -------------------------------------------------------------------------------- 1 | import { Command, installHatcher } from "../../deps.ts"; 2 | import type { DefaultOptions } from "../commands.ts"; 3 | import { version } from "../version.ts"; 4 | import { setupLog } from "../utilities/log.ts"; 5 | 6 | export async function install( 7 | options: Options, 8 | ...args: string[] 9 | ): Promise { 10 | await setupLog(options.debug); 11 | 12 | /** help option need to be parsed manually */ 13 | if (["-h", "--help", "help"].includes(args[0])) { 14 | installCommand.showHelp(); 15 | return; 16 | } 17 | 18 | await installHatcher(args); 19 | } 20 | 21 | const desc = `Add update notification to any CLI. 22 | 23 | Installs a script as an executable in the installation root's bin directory. 24 | eggs install --allow-net --allow-read https://x.nest.land/std/http/file_server.ts 25 | eggs install https://x.nest.land/std/examples/colors.ts 26 | 27 | To change the executable name, use -n/--name: 28 | eggs install --allow-net --allow-read -n serve https://x.nest.land/std/http/file_server.ts 29 | 30 | The executable name is inferred by default: 31 | - Attempt to take the file stem of the URL path. The above example would 32 | become 'file_server'. 33 | - If the file stem is something generic like 'main', 'mod', 'index' or 'cli', 34 | and the path has no parent, take the file name of the parent path. Otherwise 35 | settle with the generic name. 36 | 37 | To change the installation root, use --root: 38 | eggs install --allow-net --allow-read --root /usr/local https://x.nest.land/std/http/file_server.ts 39 | 40 | The installation root is determined, in order of precedence: 41 | - --root option 42 | - DENO_INSTALL_ROOT environment variable 43 | - $HOME/.deno 44 | 45 | These must be added to the path manually if required.`; 46 | 47 | export type Options = DefaultOptions; 48 | export type Arguments = string[]; 49 | 50 | export const installCommand = new Command() 51 | .version(version) 52 | .description(desc) 53 | .arguments("[options...:string]") 54 | .option("-A, --allow-all", "Allow all permissions") 55 | .option("--allow-env", "Allow environment access") 56 | .option("--allow-hrtime", "Allow high resolution time measurement") 57 | .option("--allow-net=", "Allow network access") 58 | .option("--allow-plugin", "Allow loading plugins") 59 | .option("--allow-read=", "Allow file system read access") 60 | .option("--allow-run", "Allow running subprocesses") 61 | .option("--allow-write=", "Allow file system write access") 62 | .option( 63 | "--cert ", 64 | "Load certificate authority from PEM encoded file", 65 | ) 66 | .option( 67 | "-f, --force", 68 | "Forcefully overwrite existing installation", 69 | ) 70 | .option( 71 | "-L, --log-level ", 72 | "Set log level [possible values: debug, info]", 73 | ) 74 | .option("-n, --name ", "Executable file name") 75 | .option("-q, --quiet", "Suppress diagnostic output") 76 | .option("--root ", "Installation root") 77 | .option("--unstable", "Enable unstable APIs") 78 | /** Unknown options cannot be parsed */ 79 | .useRawArgs() 80 | .action(install); 81 | -------------------------------------------------------------------------------- /src/commands/link.ts: -------------------------------------------------------------------------------- 1 | import { Command, log } from "../../deps.ts"; 2 | import type { DefaultOptions } from "../commands.ts"; 3 | import { KEY_FILE, writeAPIKey } from "../keyfile.ts"; 4 | import { version } from "../version.ts"; 5 | import { setupLog } from "../utilities/log.ts"; 6 | 7 | /** Link Command. 8 | * Provided a key, the `link` commands creates 9 | * a persistent file on the host os to save 10 | * the API key to. */ 11 | export async function link(options: Options, key: string) { 12 | await setupLog(options.debug); 13 | 14 | log.debug("Key: ", key); 15 | await writeAPIKey(key); 16 | log.info(`Successfully updated ${KEY_FILE} with your key!`); 17 | } 18 | 19 | export type Options = DefaultOptions; 20 | export type Arguments = [string]; 21 | 22 | export const linkCommand = new Command() 23 | .version(version) 24 | .description("Links your nest.land API key to the CLI") 25 | .arguments("") 26 | .action(link); 27 | -------------------------------------------------------------------------------- /src/commands/publish.ts: -------------------------------------------------------------------------------- 1 | import { 2 | basename as _basename, 3 | bold, 4 | Command, 5 | Confirm, 6 | dependencyTree as _dependencyTree, 7 | dim, 8 | dirname as _dirname, 9 | existsSync, 10 | globToRegExp, 11 | gray, 12 | green, 13 | isVersionUnstable, 14 | italic, 15 | join as _join, 16 | log, 17 | relative, 18 | semver, 19 | } from "../../deps.ts"; 20 | import type { DefaultOptions } from "../commands.ts"; 21 | import { releaseType, urlType, versionType } from "../utilities/types.ts"; 22 | 23 | import { ENDPOINT } from "../api/common.ts"; 24 | import { fetchModule } from "../api/fetch.ts"; 25 | import { postPieces, postPublishModule, PublishModule } from "../api/post.ts"; 26 | 27 | import { 28 | Config, 29 | configFormat, 30 | defaultConfig, 31 | writeConfig, 32 | } from "../context/config.ts"; 33 | import { gatherContext } from "../context/context.ts"; 34 | import { extendsIgnore, parseIgnore } from "../context/ignore.ts"; 35 | import type { Ignore } from "../context/ignore.ts"; 36 | import { MatchedFile, matchFiles, readFiles } from "../context/files.ts"; 37 | 38 | import { getAPIKey } from "../keyfile.ts"; 39 | import { version } from "../version.ts"; 40 | import { highlight, setupLog, spinner } from "../utilities/log.ts"; 41 | 42 | function ensureCompleteConfig( 43 | config: Partial, 44 | ignore: Ignore | undefined, 45 | ): config is Config { 46 | let isConfigComplete = true; 47 | 48 | if (!config.name) { 49 | log.error("Your module configuration must provide a module name."); 50 | isConfigComplete = false; 51 | } 52 | 53 | if (!config.version && !config.releaseType) { 54 | log.error( 55 | "Your module configuration must provide a version or release type.", 56 | ); 57 | isConfigComplete = false; 58 | } 59 | 60 | if (!config.files && !ignore && !config.entry) { 61 | log.error( 62 | `Your module configuration must provide files to upload in the form of a ${ 63 | italic("files") 64 | } field and/or ${ 65 | italic("ignore") 66 | } field in the config or in an .eggignore file.`, 67 | ); 68 | isConfigComplete = false; 69 | } 70 | 71 | config.entry ||= "./mod.ts"; 72 | config.description ||= ""; 73 | config.homepage ||= ""; 74 | config.ignore ||= []; 75 | config.unlisted ??= false; 76 | config.check ??= true; 77 | 78 | return isConfigComplete; 79 | } 80 | 81 | function ensureFiles(config: Config, matched: MatchedFile[]): boolean { 82 | if (!existsSync("README.md")) { 83 | log.warning("No README found at project root, continuing without one..."); 84 | } 85 | 86 | config.entry = "./" + relative(Deno.cwd(), config.entry).replace(/\\/g, "/"); 87 | const entryRegExp = globToRegExp(config.entry); 88 | 89 | if (!matched.find((e) => entryRegExp.test(e.path))) { 90 | log.error(`${config.entry} was not found. This file is required.`); 91 | return false; 92 | } 93 | return true; 94 | } 95 | 96 | async function deprecationWarnings(_config: Config) { 97 | // no deprecated feature for the time being :) 98 | } 99 | 100 | function gatherOptions( 101 | options: Options, 102 | name?: string, 103 | ): Partial | undefined { 104 | try { 105 | const cfg: Partial = {}; 106 | // TODO(@oganexon): find a more elegant way to remove undefined fields 107 | name && (cfg.name = name); 108 | options.version && 109 | (cfg.version = versionType( 110 | { name: "version", value: options.version, label: "", type: "" }, 111 | )); 112 | options.releaseType && 113 | (cfg.releaseType = releaseType( 114 | { name: "bump", value: options.releaseType, label: "", type: "" }, 115 | )); 116 | options.description && (cfg.description = options.description); 117 | options.entry && (cfg.entry = options.entry); 118 | options.unstable !== undefined && (cfg.unstable = options.unstable); 119 | options.unlisted !== undefined && (cfg.unlisted = options.unlisted); 120 | options.homepage && 121 | (cfg.homepage = urlType( 122 | { name: "homepage", value: options.homepage, label: "", type: "" }, 123 | )); 124 | options.files && (cfg.files = options.files); 125 | options.ignore && (cfg.ignore = options.ignore); 126 | options.checkFormat !== undefined && 127 | (cfg.checkFormat = options.checkFormat); 128 | options.checkTests !== undefined && (cfg.checkTests = options.checkTests); 129 | options.checkInstallation !== undefined && 130 | (cfg.checkInstallation = options.checkInstallation); 131 | options.check !== undefined && (cfg.check = options.check); 132 | options.yes !== undefined && (cfg.yes = options.yes); 133 | return cfg; 134 | } catch (err) { 135 | log.error(err); 136 | return; 137 | } 138 | } 139 | 140 | async function checkUp( 141 | config: Config, 142 | matched: MatchedFile[], 143 | ): Promise { 144 | if (config.checkFormat ?? config.check) { 145 | const wait = spinner.info("Checking if the source files are formatted..."); 146 | const process = Deno.run( 147 | { 148 | cmd: typeof config.checkFormat === "string" 149 | ? config.checkFormat?.split(" ") 150 | : ["deno", "fmt", "--check"].concat( 151 | matched.map((file) => file.fullPath).filter( 152 | (path) => path.match(/\.(js|jsx|ts|tsx|json)$/), 153 | ), 154 | ), 155 | stderr: "piped", 156 | stdout: "piped", 157 | }, 158 | ); 159 | const status = await process.status(); 160 | const stdout = new TextDecoder("utf-8").decode(await process.output()); 161 | const stderr = new TextDecoder("utf-8").decode( 162 | await process.stderrOutput(), 163 | ); 164 | wait.stop(); 165 | if (status.success) { 166 | log.info("Source files are formatted."); 167 | } else { 168 | log.error("Some source files are not properly formatted."); 169 | log.error(stdout); 170 | log.error(stderr); 171 | return false; 172 | } 173 | } 174 | 175 | if (config.checkTests ?? config.check) { 176 | const wait = spinner.info("Testing your code..."); 177 | const process = Deno.run( 178 | { 179 | cmd: typeof config.checkTests === "string" 180 | ? config.checkTests?.split(" ") 181 | : ["deno", "test", "-A", "--unstable"], 182 | stderr: "piped", 183 | stdout: "piped", 184 | }, 185 | ); 186 | const status = await process.status(); 187 | const stdout = new TextDecoder("utf-8").decode(await process.output()); 188 | const stderr = new TextDecoder("utf-8").decode( 189 | await process.stderrOutput(), 190 | ); 191 | wait.stop(); 192 | if (status.success) { 193 | log.info("Tests passed successfully."); 194 | } else { 195 | if (stdout.match(/^No matching test modules found/)) { 196 | log.info("No matching test modules found, tests skipped."); 197 | } else { 198 | log.error("Some tests were not successful."); 199 | log.error(stdout); 200 | log.error(stderr); 201 | return false; 202 | } 203 | } 204 | } 205 | 206 | // Test disabled: analyzer is still unstable 207 | /* if (config.checkInstallation ?? config.check) { 208 | const wait = spinner.info("Test installation..."); 209 | const tempDir = await Deno.makeTempDir(); 210 | for (let i = 0; i < matched.length; i++) { 211 | const file = matched[i]; 212 | const dir = join(tempDir, dirname(file.path)); 213 | try { 214 | await Deno.mkdir(dir, { recursive: true }); 215 | } catch (err) { 216 | if (!(err instanceof Deno.errors.AlreadyExists)) { 217 | throw err; 218 | } 219 | } 220 | await Deno.copyFile(file.fullPath, join(tempDir, file.path)); 221 | } 222 | const entry = join(tempDir, config.entry); 223 | const deps = await dependencyTree(entry); 224 | await Deno.remove(tempDir, { recursive: true }); 225 | wait.stop(); 226 | if (deps.errors.length === 0) { 227 | log.info("No errors detected when installing the module."); 228 | } else { 229 | log.error( 230 | "Some files could not be resolved during the test installation. They are probably missing, you should include them.", 231 | ); 232 | for (let i = 0; i < deps.errors.length; i++) { 233 | const [path, error] = deps.errors[i]; 234 | const relativePath = path.split(basename(tempDir))[1] || path; 235 | log.error(`${bold(relativePath)} : ${error}`); 236 | } 237 | return false; 238 | } 239 | } */ 240 | 241 | return true; 242 | } 243 | 244 | export async function publish(options: Options, name?: string) { 245 | await setupLog(options.debug); 246 | 247 | const apiKey = await getAPIKey(); 248 | if (!apiKey) { 249 | log.error( 250 | `No API Key file found. You can add one using ${ 251 | italic("eggs link ") 252 | }. You can create one on ${highlight("https://nest.land")}`, 253 | ); 254 | return; 255 | } 256 | 257 | const [gatheredContext, contextIgnore] = await gatherContext(); 258 | log.debug("Options:", options); 259 | const gatheredOptions = gatherOptions(options, name); 260 | if (!gatheredContext || !gatheredOptions) return; 261 | 262 | const egg: Partial = { 263 | ...gatheredContext, 264 | ...gatheredOptions, 265 | }; 266 | 267 | log.debug("Raw config:", egg); 268 | 269 | const ignore = contextIgnore || 270 | egg.ignore && await extendsIgnore(parseIgnore(egg.ignore.join())); 271 | 272 | if (!ensureCompleteConfig(egg, ignore)) return; 273 | 274 | log.debug("Ignore:", ignore); 275 | 276 | const matched = matchFiles(egg, ignore); 277 | if (!matched) return; 278 | const matchedContent = readFiles(matched); 279 | 280 | log.debug("Matched files:", matched); 281 | 282 | if (!ensureFiles(egg, matched)) return; 283 | if (!await checkUp(egg, matched)) return; 284 | await deprecationWarnings(egg); 285 | 286 | log.debug("Config:", egg); 287 | 288 | const existing = await fetchModule(egg.name); 289 | 290 | let latest = "0.0.0"; 291 | if (existing) { 292 | latest = existing.getLatestVersion(); 293 | egg.description = egg.description || existing.description; 294 | egg.homepage = egg.homepage || existing.repository || ""; 295 | } 296 | if (egg.releaseType) { 297 | egg.version = semver.inc(egg.version || latest, egg.releaseType) as string; 298 | } 299 | if ( 300 | existing && 301 | existing.packageUploadNames.indexOf(`${egg.name}@${egg.version}`) !== -1 302 | ) { 303 | log.error( 304 | "This version was already published. Please increment the version in your configuration.", 305 | ); 306 | return; 307 | } 308 | 309 | if (!egg.description) { 310 | log.warning( 311 | "You haven't provided a description for your module, continuing without one...", 312 | ); 313 | } 314 | 315 | const module: PublishModule = { 316 | name: egg.name, 317 | version: egg.version, 318 | description: egg.description, 319 | repository: egg.homepage, 320 | unlisted: egg.unlisted, 321 | stable: !(egg.unstable ?? isVersionUnstable(egg.version)), 322 | upload: true, 323 | latest: semver.compare(egg.version, latest) === 1, 324 | entry: egg.entry.substr(1), 325 | // TODO(@oganexon): make this format consistent between eggs & website 326 | // (here we need to have "/" at the start of the string, where in the website "/" is removed) 327 | }; 328 | 329 | log.info( 330 | `${bold("The resulting module is:")} ${ 331 | Deno.inspect(module, { colors: true }) 332 | .replace(/^\s*{([\s\S]*)\n}\s*$/, "$1") 333 | .replace(/\n\s{2}/g, "\n - ") 334 | }`, 335 | ); 336 | 337 | const filesToPublish = matched.reduce( 338 | (previous, current) => { 339 | return `${previous}\n - ${dim(current.path)} ${ 340 | gray(dim("(" + (current.lstat.size / 1000000).toString() + "MB)")) 341 | }`; 342 | }, 343 | "Files to publish:", 344 | ); 345 | log.info(filesToPublish); 346 | 347 | if (!options.yes) { 348 | const confirmation: boolean = await Confirm.prompt({ 349 | message: "Are you sure you want to publish this module?", 350 | default: false, 351 | }); 352 | 353 | if (!confirmation) { 354 | log.info("Publish cancelled."); 355 | return; 356 | } 357 | } 358 | 359 | if (options.dryRun) { 360 | return; 361 | } 362 | 363 | const uploadResponse = await postPublishModule(apiKey, module); 364 | if (!uploadResponse) { 365 | // TODO(@qu4k): provide better error reporting 366 | throw new Error("Something broke when publishing... "); 367 | } 368 | 369 | const pieceResponse = await postPieces( 370 | uploadResponse.token, 371 | Object.entries(matchedContent).reduce((prev, [key, value]) => { 372 | prev[key.substr(1)] = value; 373 | return prev; 374 | }, {} as Record), 375 | ); 376 | // TODO(@oganexon): same, needs consistency 377 | 378 | if (!pieceResponse) { 379 | // TODO(@qu4k): provide better error reporting 380 | throw new Error("Something broke when sending pieces... "); 381 | } 382 | 383 | const configPath = await defaultConfig(); 384 | if (configPath) { 385 | await writeConfig(egg, configFormat(configPath)); 386 | log.debug("Updated configuration."); 387 | } 388 | 389 | log.info(`Successfully published ${bold(egg.name)}!`); 390 | 391 | const files = Object.entries(pieceResponse.files).reduce( 392 | (previous, current) => { 393 | return `${previous}\n - ${current[0]} -> ${ 394 | bold(`${ENDPOINT}/${egg.name}@${egg.version}${current[0]}`) 395 | }`; 396 | }, 397 | "Files uploaded: ", 398 | ); 399 | log.info(files); 400 | 401 | log.info(""); 402 | log.info( 403 | green( 404 | `You can now find your module on our registry at ${ 405 | highlight(`https://nest.land/package/${egg.name}`) 406 | }`, 407 | ), 408 | ); 409 | log.info(""); 410 | log.info("Now you can showcase your module on our GitHub Discussions!"); 411 | log.info(highlight("https://github.com/nestdotland/nest.land/discussions")); 412 | } 413 | 414 | export interface Options extends DefaultOptions { 415 | dryRun?: boolean; 416 | yes?: boolean; 417 | version?: string; 418 | releaseType?: semver.ReleaseType; 419 | entry?: string; 420 | description?: string; 421 | homepage?: string; 422 | unstable?: boolean; 423 | unlisted?: boolean; 424 | files?: string[]; 425 | ignore?: string[]; 426 | checkFormat?: boolean | string; 427 | checkTests?: boolean | string; 428 | checkInstallation?: boolean; 429 | check?: boolean; 430 | } 431 | export type Arguments = [string]; 432 | 433 | export const publishCommand = new Command() 434 | .description("Publishes your module to the nest.land registry.") 435 | .version(version) 436 | .type("release", releaseType) 437 | .type("version", versionType) 438 | .type("url", urlType) 439 | .arguments("[name: string]") 440 | .option( 441 | "-d, --dry-run", 442 | "No changes will actually be made, reports the details of what would have been published.", 443 | ) 444 | .option( 445 | "-Y, --yes", 446 | "Disable confirmation prompts.", 447 | ) 448 | .option( 449 | "--description ", 450 | "A description of your module that will appear on the gallery.", 451 | ) 452 | .option( 453 | "--release-type ", 454 | "Increment the version by the release type.", 455 | ) 456 | .option("--version ", "Set the version.") 457 | .option( 458 | "--entry ", 459 | "The main file of your project.", 460 | ) 461 | .option("--unstable", "Flag this version as unstable.") 462 | .option("--unlisted", "Hide this module/version on the gallery.") 463 | .option( 464 | "--homepage ", 465 | "A link to your homepage. Usually a repository.", 466 | ) 467 | .option( 468 | "--files ", 469 | "All the files that should be uploaded to nest.land. Supports file globbing.", 470 | ) 471 | .option( 472 | "--ignore ", 473 | "All the files that should be ignored when uploading to nest.land. Supports file globbing.", 474 | ) 475 | .option( 476 | "--check-format [value:string]", 477 | "Automatically format your code before publishing", 478 | ) 479 | .option("--check-tests [value:string]", "Test your code.") 480 | .option( 481 | "--check-installation", 482 | "Simulates a dummy installation and check for missing files in the dependency tree.", 483 | ) 484 | .option( 485 | "--no-check", 486 | `Use this option to not perform any check.`, 487 | ) 488 | .action(publish); 489 | -------------------------------------------------------------------------------- /src/commands/update.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Command, 3 | green, 4 | latestVersion, 5 | log, 6 | parseURL, 7 | semver, 8 | yellow, 9 | } from "../../deps.ts"; 10 | import { version } from "../version.ts"; 11 | import type { DefaultOptions } from "../commands.ts"; 12 | import { setupLog } from "../utilities/log.ts"; 13 | 14 | /** What the constructed dependency objects should contain */ 15 | interface ModuleToUpdate { 16 | line: string; 17 | versionURL: string; 18 | latestRelease: string; 19 | } 20 | 21 | const decoder = new TextDecoder("utf-8"); 22 | 23 | export async function update( 24 | options: Options, 25 | requestedModules: string[], 26 | ): Promise { 27 | await setupLog(options.debug); 28 | 29 | log.debug("Options: ", options); 30 | 31 | /** Gather the path to the user's dependency file using the CLI arguments */ 32 | let pathToDepFile = ""; 33 | try { 34 | pathToDepFile = Deno.realPathSync("./" + options.file); 35 | } catch { 36 | // Dependency file doesn't exist 37 | log.warning( 38 | "No dependency file was found in your current working directory.", 39 | ); 40 | return; 41 | } 42 | 43 | /** Creates an array of strings from each line inside the dependency file. 44 | * Only extracts lines that contain "https://" to strip out non-import lines. */ 45 | const dependencyFileContents: string[] = decoder 46 | .decode(Deno.readFileSync(pathToDepFile)) 47 | .split("\n") 48 | .filter((line) => line.indexOf("https://") > 0); 49 | 50 | if (dependencyFileContents.length === 0) { 51 | log.warning( 52 | "Your dependency file does not contain any imported modules.", 53 | ); 54 | return; 55 | } 56 | 57 | log.debug("Dependency file contents: ", dependencyFileContents); 58 | 59 | /** For each import line in the users dependency file, collate the data ready to be re-written 60 | * if it can be updated. 61 | * Skips the dependency if it is not versioned (no need to try to update it) */ 62 | const dependenciesToUpdate: Array = []; 63 | for (const line of dependencyFileContents) { 64 | const { name, parsedURL, registry, owner, version } = parseURL(line); 65 | 66 | // TODO(@qu4k): edge case: dependency isn't a module, for example: from 67 | // "https://x.nest.land/std@version.ts";, will return -> "version.ts"; 68 | // Issue: "Mandarine.TS" is a module while "version.ts" isn't 69 | 70 | // Now we have the name, ignore dependency if requested dependencies are set and it isn't one requested 71 | if ( 72 | requestedModules.length && requestedModules.indexOf(name) === -1 73 | ) { 74 | log.debug(name, "was not requested."); 75 | continue; 76 | } 77 | 78 | // Get latest release 79 | const latestRelease = await latestVersion(registry, name, owner); 80 | 81 | // Basic safety net 82 | 83 | if (!version || !semver.valid(version)) { 84 | log.debug("Invalid version", name, version); 85 | continue; 86 | } 87 | 88 | if (!latestRelease || !semver.valid(latestRelease)) { 89 | log.warning( 90 | `Warning: could not find the latest version of ${name}.`, 91 | ); 92 | continue; 93 | } 94 | 95 | if (semver.eq(version, latestRelease)) { 96 | log.debug(name, "is already up to date!"); 97 | continue; 98 | } 99 | 100 | // Collate the dependency 101 | dependenciesToUpdate.push({ 102 | line, 103 | versionURL: parsedURL, 104 | latestRelease, 105 | }); 106 | 107 | log.info(`${name} ${yellow(version)} → ${green(latestRelease)}`); 108 | } 109 | 110 | // If no modules are needed to update then exit 111 | if (dependenciesToUpdate.length === 0) { 112 | log.info("Your dependencies are already up to date!"); 113 | return; 114 | } 115 | 116 | // Loop through the users dependency file, replacing the imported version with the latest release for each dep 117 | let dependencyFile = decoder.decode(Deno.readFileSync(pathToDepFile)); 118 | dependenciesToUpdate.forEach((dependency) => { 119 | dependencyFile = dependencyFile.replace( 120 | dependency.line, 121 | dependency.versionURL.replace("${version}", dependency.latestRelease), 122 | ); 123 | }); 124 | 125 | // Re-write the file 126 | Deno.writeFileSync( 127 | pathToDepFile, 128 | new TextEncoder().encode(dependencyFile), 129 | ); 130 | 131 | log.info("Updated your dependencies!"); 132 | } 133 | 134 | export interface Options extends DefaultOptions { 135 | file: string; 136 | global: boolean; 137 | } 138 | export type Arguments = [string[]]; 139 | 140 | export const updateCommand = new Command() 141 | .description("Update your dependencies") 142 | .version(version) 143 | .arguments("[deps...:string]") 144 | .option( 145 | "--file ", 146 | "Set dependency filename", 147 | { default: "deps.ts" }, 148 | ) 149 | .action(update); 150 | -------------------------------------------------------------------------------- /src/commands/upgrade.ts: -------------------------------------------------------------------------------- 1 | import { Command, log, NestLand, semver } from "../../deps.ts"; 2 | import type { DefaultOptions } from "../commands.ts"; 3 | 4 | import { version } from "../version.ts"; 5 | import { setupLog } from "../utilities/log.ts"; 6 | 7 | export async function upgrade(options: DefaultOptions) { 8 | await setupLog(options.debug); 9 | 10 | const newVersion = await NestLand.latestVersion("eggs"); 11 | if (!newVersion) { 12 | log.error("Could not retrieve latest version."); 13 | return; 14 | } 15 | if (semver.eq(newVersion, version)) { 16 | log.info("You are already using the latest CLI version!"); 17 | return; 18 | } 19 | 20 | const upgradeProcess = Deno.run({ 21 | cmd: [ 22 | "deno", 23 | "install", 24 | "--unstable", 25 | "-Afq", 26 | `https://x.nest.land/eggs@${newVersion}/eggs.ts`, 27 | ], 28 | stdout: "piped", 29 | stderr: "piped", 30 | }); 31 | 32 | const status = await upgradeProcess.status(); 33 | upgradeProcess.close(); 34 | 35 | const stdout = new TextDecoder("utf-8").decode(await upgradeProcess.output()); 36 | const stderr = new TextDecoder("utf-8").decode( 37 | await upgradeProcess.stderrOutput(), 38 | ); 39 | 40 | log.debug("stdout: ", stdout); 41 | log.debug("stderr: ", stderr); 42 | 43 | if (!status.success) { 44 | throw new Error("Failed to upgrade to the latest CLI version!"); 45 | } 46 | 47 | log.info("Successfully upgraded eggs cli!"); 48 | } 49 | 50 | export type Options = DefaultOptions; 51 | export type Arguments = []; 52 | 53 | export const upgradeCommand = new Command() 54 | .version(version) 55 | .description("Upgrade the current nest.land CLI.") 56 | .action(upgrade); 57 | -------------------------------------------------------------------------------- /src/context/config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | exists, 3 | extname, 4 | join, 5 | parseYaml, 6 | semver, 7 | stringifyYaml, 8 | } from "../../deps.ts"; 9 | import { writeJson } from "../utilities/json.ts"; 10 | 11 | /** Supported configuration formats. */ 12 | export enum ConfigFormat { 13 | YAML = "yml", 14 | JSON = "json", 15 | } 16 | 17 | /** Configuration options. 18 | * All fields are optional but most 19 | * commands require at least some. */ 20 | export interface Config { 21 | $schema: string; 22 | 23 | name: string; 24 | entry: string; 25 | description: string; 26 | homepage: string; 27 | unstable?: boolean; 28 | unlisted: boolean; 29 | 30 | version: string; 31 | releaseType?: semver.ReleaseType; 32 | 33 | files?: string[]; 34 | ignore?: string[]; 35 | 36 | yes?: boolean; 37 | checkFormat?: boolean | string; 38 | checkTests?: boolean | string; 39 | checkInstallation?: boolean; 40 | check: boolean; 41 | } 42 | 43 | /** Filenames of the default configs. 44 | * The `defaultConfig` method checks 45 | * if one of this config files is 46 | * available in the cwd. */ 47 | const DEFAULT_CONFIGS = [ 48 | "egg.json", 49 | "egg.yaml", 50 | "egg.yml", 51 | ]; 52 | 53 | /** Get default config in cwd. */ 54 | export async function defaultConfig( 55 | wd: string = Deno.cwd(), 56 | ): Promise { 57 | for (const path of DEFAULT_CONFIGS) { 58 | if (await exists(join(wd, path))) return path; 59 | } 60 | } 61 | 62 | /** Get config format for provided path. 63 | * @param path configuration file path */ 64 | export function configFormat(path: string): ConfigFormat { 65 | const ext = extname(path); 66 | if (ext.match(/^.ya?ml$/)) return ConfigFormat.YAML; 67 | return ConfigFormat.JSON; 68 | } 69 | 70 | /** writeYaml. (similar to writeJson) 71 | * @private */ 72 | function writeYaml(filename: string, content: string): void { 73 | return Deno.writeFileSync(filename, new TextEncoder().encode(content)); 74 | } 75 | 76 | /** Write config with specific provided format. */ 77 | export async function writeConfig( 78 | data: Partial, 79 | format: ConfigFormat, 80 | ): Promise { 81 | switch (format) { 82 | case ConfigFormat.YAML: 83 | await writeYaml(join(Deno.cwd(), "egg.yml"), stringifyYaml(data)); 84 | break; 85 | case ConfigFormat.JSON: 86 | await writeJson(join(Deno.cwd(), "egg.json"), data, { spaces: 2 }); 87 | break; 88 | default: 89 | throw new Error(`Unknown config format: ${format}`); 90 | } 91 | } 92 | 93 | /** Read configuration from provided path. */ 94 | export async function readConfig(path: string): Promise> { 95 | const format = configFormat(path); 96 | const data = await Deno.readTextFile(path); 97 | return parseConfig(data, format); 98 | } 99 | 100 | /** Parse configuration (provided as string) 101 | * for specific provided format */ 102 | export function parseConfig( 103 | data: string, 104 | format: ConfigFormat, 105 | ): Partial { 106 | if (format == ConfigFormat.YAML) { 107 | return (parseYaml(data) ?? {}) as Partial; 108 | } 109 | return JSON.parse(data) as Partial; 110 | } 111 | -------------------------------------------------------------------------------- /src/context/config_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "../../test/deps.ts"; 2 | import { ConfigFormat, configFormat } from "./config.ts"; 3 | 4 | Deno.test({ 5 | name: "internal | config | file matching", 6 | fn(): void { 7 | assertEquals(configFormat("eggs.yml"), ConfigFormat.YAML); 8 | assertEquals(configFormat("eggs.yaml"), ConfigFormat.YAML); 9 | assertEquals(configFormat("eggs.json"), ConfigFormat.JSON); 10 | assertEquals(configFormat("eggs.js"), ConfigFormat.JSON); // because of fallback 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /src/context/context.ts: -------------------------------------------------------------------------------- 1 | import { log } from "../../deps.ts"; 2 | 3 | import { Config, defaultConfig, readConfig } from "./config.ts"; 4 | import { defaultIgnore, Ignore, readIgnore } from "./ignore.ts"; 5 | 6 | export async function gatherContext( 7 | wd: string = Deno.cwd(), 8 | ): Promise<[Partial | undefined, Ignore | undefined]> { 9 | let config: Partial = {}; 10 | let ignore: Ignore = { 11 | accepts: [], 12 | denies: [], 13 | extends: [], 14 | }; 15 | const configPath = await defaultConfig(wd); 16 | 17 | if (configPath) { 18 | try { 19 | config = await readConfig(configPath); 20 | } catch (err) { 21 | log.error("Unable to read config file.", err); 22 | return [undefined, undefined]; 23 | } 24 | } 25 | 26 | const ignorePath = await defaultIgnore(wd); 27 | if (ignorePath) { 28 | try { 29 | ignore = await readIgnore(ignorePath); 30 | } catch (err) { 31 | throw err; 32 | } 33 | return [config, ignore]; 34 | } 35 | 36 | return [config, undefined]; 37 | } 38 | -------------------------------------------------------------------------------- /src/context/files.ts: -------------------------------------------------------------------------------- 1 | import { 2 | base64, 3 | bold, 4 | expandGlobSync, 5 | globToRegExp, 6 | log, 7 | relative, 8 | resolve, 9 | walkSync, 10 | } from "../../deps.ts"; 11 | import type { Config } from "./config.ts"; 12 | import type { Ignore } from "./ignore.ts"; 13 | 14 | export interface MatchedFile { 15 | fullPath: string; 16 | path: string; 17 | lstat: Deno.FileInfo; 18 | } 19 | 20 | export function matchFiles( 21 | config: Config, 22 | ignore: Ignore | undefined, 23 | ): MatchedFile[] | undefined { 24 | let matched: MatchedFile[] = []; 25 | 26 | if (config.files) { 27 | for (const file of [config.entry, ...config.files]) { 28 | const matches = [ 29 | ...expandGlobSync(file, { 30 | root: Deno.cwd(), 31 | extended: true, 32 | }), 33 | ] 34 | .map((file) => ({ 35 | fullPath: file.path.replace(/\\/g, "/"), 36 | path: "./" + relative(Deno.cwd(), file.path).replace(/\\/g, "/"), 37 | lstat: Deno.lstatSync(file.path), 38 | })); 39 | if (matches.length === 0) { 40 | log.error( 41 | `${ 42 | bold(file) 43 | } did not match any file. There may be a typo in the path.`, 44 | ); 45 | return; 46 | } 47 | matched.push(...matches); 48 | } 49 | } else { 50 | (ignore as Ignore).accepts.push(globToRegExp(config.entry)); 51 | for (const entry of walkSync(".")) { 52 | const path = "./" + entry.path.replace(/\\/g, "/"); 53 | const fullPath = resolve(entry.path); 54 | const lstat = Deno.lstatSync(entry.path); 55 | const file: MatchedFile = { 56 | fullPath, 57 | path, 58 | lstat, 59 | }; 60 | matched.push(file); 61 | } 62 | } 63 | 64 | matched = matched.filter((file) => file.lstat.isFile).filter((file) => { 65 | if ( 66 | ignore?.denies.some((rgx) => 67 | // check for "./" and "" 68 | rgx.test(file.path) || rgx.test(file.path.substr(2)) 69 | ) 70 | ) { 71 | return ignore.accepts.some((rgx) => rgx.test(file.path)); 72 | } 73 | return true; 74 | }); 75 | 76 | return matched; 77 | } 78 | 79 | export function readFiles(matched: MatchedFile[]): { [x: string]: string } { 80 | function readFileBtoa(path: string): string { 81 | const data = Deno.readFileSync(path); 82 | return base64.fromUint8Array(data); 83 | } 84 | 85 | return matched.map((el) => 86 | [el, readFileBtoa(el.fullPath)] as [typeof el, string] 87 | ).reduce((p, c) => { 88 | p[c[0].path] = c[1]; 89 | return p; 90 | }, {} as { [x: string]: string }); 91 | } 92 | -------------------------------------------------------------------------------- /src/context/ignore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | basename, 3 | exists, 4 | expandGlob, 5 | globToRegExp, 6 | join, 7 | log, 8 | relative, 9 | } from "../../deps.ts"; 10 | 11 | export interface Ignore { 12 | accepts: RegExp[]; 13 | denies: RegExp[]; 14 | extends: string[]; 15 | } 16 | 17 | const DEFAULT_IGNORES = [ 18 | ".eggignore", 19 | ]; 20 | 21 | export async function defaultIgnore( 22 | wd: string = Deno.cwd(), 23 | ): Promise { 24 | for (const path of DEFAULT_IGNORES) { 25 | if (await exists(join(wd, path))) return path; 26 | } 27 | } 28 | 29 | export async function readIgnore(path: string): Promise { 30 | try { 31 | const data = await Deno.readTextFile(path); 32 | const ignore = parseIgnore(data, basename(path)); 33 | return extendsIgnore(ignore); 34 | } catch (err) { 35 | throw new Error(`Error while reading ${path}: ${err}`); 36 | } 37 | } 38 | 39 | export async function extendsIgnore(ignore: Ignore) { 40 | while (ignore.extends.length > 0) { 41 | const pattern = ignore.extends.pop() as string; 42 | if (pattern.match(/.gitignore$/)) { 43 | ignore.denies.push(globToRegExp(".git*/**")); 44 | } 45 | const files = expandGlob(pattern, { root: Deno.cwd() }); 46 | for await (const file of files) { 47 | const path = relative(Deno.cwd(), file.path).replace(/\\/g, "/"); 48 | const { accepts, denies } = await readIgnore(path); 49 | ignore.accepts.push(...accepts); 50 | ignore.denies.push(...denies); 51 | } 52 | } 53 | return ignore; 54 | } 55 | 56 | export function parseIgnore( 57 | data: string, 58 | name = "", 59 | ): Ignore { 60 | const ignore: Ignore = { 61 | accepts: [], 62 | denies: [], 63 | extends: [], 64 | }; 65 | const lines = data.split(/\r\n|\r|\n/).map((_) => _.replace(/^\s*/, "")); 66 | 67 | for (let i = 0; i < lines.length; i++) { 68 | let line = lines[i]; 69 | // A blank line matches no files, so it can serve as a separator for readability. 70 | if (!line) continue; 71 | // A line starting with # serves as a comment. Put a backslash ("\") in front of 72 | // the first hash for patterns that begin with a hash. 73 | if (line.startsWith("#")) continue; 74 | // An optional prefix "!" which negates the pattern. 75 | const accepts = line.startsWith("!"); 76 | // An optional prefix "extends " which imports other ignore files (.gitignore). 77 | const extends_ = line.startsWith("extends "); 78 | // Trailing spaces are ignored unless they are quoted with backslash ("\"). 79 | line = line.replace(/(? { 22 | const keyPath = join(envHOMEDIR(), KEY_FILE); 23 | log.debug("Key path", keyPath); 24 | await Deno.writeFile(keyPath, new TextEncoder().encode(key)); 25 | } 26 | 27 | export async function getAPIKey(): Promise { 28 | if ( 29 | !existsSync(join(envHOMEDIR(), KEY_FILE)) 30 | ) { 31 | return ""; // empty string 32 | } 33 | const decoder = new TextDecoder("utf-8"); 34 | return decoder.decode( 35 | await Deno.readFile(join(envHOMEDIR(), KEY_FILE)), 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "nestdotland/eggs.json", 4 | "properties": { 5 | "name": { 6 | "description": "The name of your module/repository.", 7 | "type": ["string"], 8 | "examples": ["my-module"] 9 | }, 10 | "description": { 11 | "description": "Your module/repository description.", 12 | "type": "string", 13 | "examples": ["This is the description of my repository"] 14 | }, 15 | "version": { 16 | "type": "string", 17 | "description": "Current version of your module. Must follow semver.", 18 | "examples": ["0.0.1"] 19 | }, 20 | "bump": { 21 | "type": "string", 22 | "description": "Increment the version by release type. See https://docs.nest.land/eggs/configuration.html#field-information.", 23 | "examples": ["patch"] 24 | }, 25 | "entry": { 26 | "type": "string", 27 | "description": "The index file of your project. This is what users will see when they try to import your module from our registry! Defaults to ./mod.ts.", 28 | "default": "./mod.ts" 29 | }, 30 | "unstable": { 31 | "type": "boolean", 32 | "description": "Is this version unstable?. Default value is determined by Semantic Versioning rules.", 33 | "examples": [false] 34 | }, 35 | "unlisted": { 36 | "type": "boolean", 37 | "description": "Should people be able to find this module/version on the gallery?. Defaults to false.", 38 | "default": false 39 | }, 40 | "repository": { 41 | "type": "string", 42 | "description": "A link to your repository. Defaults to null.", 43 | "default": "null" 44 | }, 45 | "files": { 46 | "type": "array", 47 | "items": { 48 | "type": "string" 49 | }, 50 | "description": "All the files that should be uploaded to nest.land. Supports file globbing. Do not use ./**/* for the files field! This has been known to cause errors in the publishing process.", 51 | "examples": [["src/**/*"]] 52 | }, 53 | "ignore": { 54 | "type": "array", 55 | "items": { 56 | "type": "string" 57 | }, 58 | "description": "All the files that should be ignored when uploading to nest.land. Supports file globbing.", 59 | "examples": [["tests/**/*"]] 60 | }, 61 | "checkFormat": { 62 | "type": ["boolean", "string"], 63 | "description": "Automatically format your code before publishing to the blockchain. Defaults to false", 64 | "default": false 65 | }, 66 | "checkTests": { 67 | "type": ["boolean", "string"], 68 | "description": "Run deno test. Defaults to false.", 69 | "default": false 70 | }, 71 | "checkInstallation": { 72 | "type": "boolean", 73 | "description": "Simulates a dummy installation and check for missing files in the dependency tree. Defaults to false.", 74 | "default": false 75 | }, 76 | "checkAll": { 77 | "type": "boolean", 78 | "description": "Performs all checks. Defaults to true.", 79 | "default": true 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/utilities/environment.ts: -------------------------------------------------------------------------------- 1 | export function envHOMEDIR(): string { 2 | return Deno.env.get("HOME") ?? // for linux / mac 3 | Deno.env.get("USERPROFILE") ?? // for windows 4 | "/"; 5 | } 6 | 7 | export function envENDPOINT(): string { 8 | return Deno.env.get("EGGS_ENDPOINT") ?? 9 | "https://x.nest.land"; 10 | } 11 | -------------------------------------------------------------------------------- /src/utilities/json.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | type Replacer = (key: string, value: unknown) => unknown; 3 | 4 | export interface WriteJsonOptions extends Deno.WriteFileOptions { 5 | replacer?: Array | Replacer; 6 | spaces?: number | string; 7 | } 8 | 9 | function serialize( 10 | filePath: string, 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | object: unknown, 13 | options: WriteJsonOptions, 14 | ): string { 15 | try { 16 | const jsonString = JSON.stringify( 17 | object, 18 | options.replacer as string[], 19 | options.spaces, 20 | ); 21 | return `${jsonString}\n`; 22 | } catch (err) { 23 | err.message = `${filePath}: ${err.message}`; 24 | throw err; 25 | } 26 | } 27 | 28 | /* Writes an object to a JSON file. */ 29 | export async function writeJson( 30 | filePath: string, 31 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 32 | object: unknown, 33 | options: WriteJsonOptions = {}, 34 | ): Promise { 35 | const jsonString = serialize(filePath, object, options); 36 | await Deno.writeTextFile(filePath, jsonString, { 37 | append: options.append, 38 | create: options.create, 39 | mode: options.mode, 40 | }); 41 | } 42 | 43 | /* Writes an object to a JSON file. */ 44 | export function writeJsonSync( 45 | filePath: string, 46 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 47 | object: unknown, 48 | options: WriteJsonOptions = {}, 49 | ): void { 50 | const jsonString = serialize(filePath, object, options); 51 | Deno.writeTextFileSync(filePath, jsonString, { 52 | append: options.append, 53 | create: options.create, 54 | mode: options.mode, 55 | }); 56 | } 57 | 58 | /** Reads a JSON file and then parses it into an object */ 59 | export async function readJson(filePath: string): Promise { 60 | const decoder = new TextDecoder("utf-8"); 61 | 62 | const content = decoder.decode(await Deno.readFile(filePath)); 63 | 64 | try { 65 | return JSON.parse(content); 66 | } catch (err) { 67 | err.message = `${filePath}: ${err.message}`; 68 | throw err; 69 | } 70 | } 71 | 72 | /** Reads a JSON file and then parses it into an object */ 73 | export function readJsonSync(filePath: string): unknown { 74 | const decoder = new TextDecoder("utf-8"); 75 | 76 | const content = decoder.decode(Deno.readFileSync(filePath)); 77 | 78 | try { 79 | return JSON.parse(content); 80 | } catch (err) { 81 | err.message = `${filePath}: ${err.message}`; 82 | throw err; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/utilities/log.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseHandler, 3 | blue, 4 | bold, 5 | gray, 6 | log, 7 | LogLevels, 8 | LogRecord, 9 | red, 10 | resolve, 11 | Spinner, 12 | stripColor, 13 | underline, 14 | wait, 15 | yellow, 16 | } from "../../deps.ts"; 17 | 18 | import { version } from "../version.ts"; 19 | 20 | const DEBUG_LOG_FILE = "./eggs-debug.log"; 21 | 22 | export let masterLogRecord = ""; 23 | export let errorOccurred = false; 24 | let detailedLog = false; 25 | 26 | const prefix = { 27 | debug: gray("[DEBUG]"), 28 | info: blue("[INFO]"), 29 | warning: yellow("[WARN]"), 30 | error: red("[ERR]"), 31 | critical: bold(red("[CRIT]")), 32 | }; 33 | 34 | class ConsoleHandler extends BaseHandler { 35 | format(record: LogRecord): string { 36 | let msg = ""; 37 | if (record.msg) { 38 | switch (record.level) { 39 | case LogLevels.DEBUG: 40 | msg += prefix.debug; 41 | break; 42 | case LogLevels.INFO: 43 | msg += prefix.info; 44 | break; 45 | case LogLevels.WARNING: 46 | msg += prefix.warning; 47 | break; 48 | case LogLevels.ERROR: 49 | msg += prefix.error; 50 | errorOccurred = true; 51 | break; 52 | case LogLevels.CRITICAL: 53 | msg += prefix.critical; 54 | break; 55 | default: 56 | break; 57 | } 58 | 59 | msg += ` ${record.msg}`; 60 | } 61 | 62 | if (detailedLog) { 63 | for (const arg of record.args) { 64 | msg += ` ${Deno.inspect(arg, { depth: 10, colors: true })}`; 65 | } 66 | } 67 | 68 | return msg; 69 | } 70 | 71 | log(msg: string): void { 72 | console.log(msg); 73 | } 74 | } 75 | 76 | class FileHandler extends BaseHandler { 77 | format(record: LogRecord): string { 78 | let msg = record.datetime.toISOString() + " "; 79 | 80 | switch (record.level) { 81 | case LogLevels.DEBUG: 82 | msg += "[DEBUG] "; 83 | break; 84 | case LogLevels.INFO: 85 | msg += "[INFO] "; 86 | break; 87 | case LogLevels.WARNING: 88 | msg += "[WARNING] "; 89 | break; 90 | case LogLevels.ERROR: 91 | msg += "[ERROR] "; 92 | break; 93 | case LogLevels.CRITICAL: 94 | msg += "[CRITICAL]"; 95 | break; 96 | default: 97 | break; 98 | } 99 | 100 | msg += ` ${stripColor(record.msg)}`; 101 | 102 | for (const arg of record.args) { 103 | msg += ` ${Deno.inspect(arg, { depth: Infinity })}`; 104 | } 105 | 106 | return msg; 107 | } 108 | 109 | log(msg: string): void { 110 | masterLogRecord += msg + "\n"; 111 | } 112 | } 113 | 114 | /** Setup custom deno logger. Follows format: 115 | * `[LEVEL] ` */ 116 | export async function setupLog( 117 | debugEnabled = false, 118 | ): Promise { 119 | detailedLog = debugEnabled; 120 | await log.setup({ 121 | handlers: { 122 | console: new ConsoleHandler(debugEnabled ? "DEBUG" : "INFO"), 123 | file: new FileHandler("DEBUG"), 124 | }, 125 | loggers: { 126 | default: { 127 | level: "DEBUG", 128 | handlers: ["console", "file"], 129 | }, 130 | }, 131 | }); 132 | } 133 | 134 | export async function writeLogFile() { 135 | const encoder = new TextEncoder(); 136 | 137 | const args = `Arguments:\n ${Deno.args}\n\n`; 138 | const denoVersion = 139 | `Deno version:\n deno: ${Deno.version.deno}\n v8: ${Deno.version.v8}\n typescript: ${Deno.version.typescript}\n\n`; 140 | const eggsVersion = `Eggs version:\n ${version}\n\n`; 141 | const platform = `Platform:\n ${Deno.build.target}\n\n`; 142 | 143 | await Deno.writeFile( 144 | DEBUG_LOG_FILE, 145 | encoder.encode( 146 | args + 147 | denoVersion + 148 | eggsVersion + 149 | platform + 150 | masterLogRecord, 151 | ), 152 | ); 153 | 154 | log.info( 155 | `Debug file created. (${highlight(resolve(Deno.cwd(), DEBUG_LOG_FILE))})`, 156 | ); 157 | } 158 | 159 | export async function handleError(err: Error) { 160 | log.critical(`An unexpected error occurred: "${err.message}"`, err.stack); 161 | await writeLogFile(); 162 | log.info( 163 | `If you think this is a bug, please open a bug report at ${ 164 | highlight("https://github.com/nestdotland/eggs/issues/new/choose") 165 | } with the information provided in ${ 166 | highlight(resolve(Deno.cwd(), DEBUG_LOG_FILE)) 167 | }`, 168 | ); 169 | log.info( 170 | `Visit ${ 171 | highlight("https://docs.nest.land/eggs/") 172 | } for documentation about this command.`, 173 | ); 174 | } 175 | 176 | export function highlight(msg: string) { 177 | return underline(bold(msg)); 178 | } 179 | 180 | const ci = Deno.env.get("CI"); 181 | const ciSpinner = { stop: () => {}, text: "" } as Spinner; 182 | 183 | export class spinner { 184 | static info(msg: string) { 185 | return ci ? ciSpinner : wait({ 186 | text: msg, 187 | prefix: prefix.info, 188 | }).start(); 189 | } 190 | 191 | static warning(msg: string) { 192 | return ci ? ciSpinner : wait({ 193 | text: msg, 194 | prefix: prefix.warning, 195 | }).start(); 196 | } 197 | 198 | static error(msg: string) { 199 | return ci ? ciSpinner : wait({ 200 | text: msg, 201 | prefix: prefix.error, 202 | }).start(); 203 | } 204 | 205 | static critical(msg: string) { 206 | return ci ? ciSpinner : wait({ 207 | text: msg, 208 | prefix: prefix.critical, 209 | }).start(); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/utilities/types.ts: -------------------------------------------------------------------------------- 1 | import { ITypeInfo, semver } from "../../deps.ts"; 2 | 3 | export const releases = [ 4 | "patch", 5 | "minor", 6 | "major", 7 | "pre", 8 | "prepatch", 9 | "preminor", 10 | "premajor", 11 | "prerelease", 12 | ]; 13 | 14 | export function validateRelease(value: string): boolean { 15 | return releases.includes(value); 16 | } 17 | 18 | export function releaseType({ name, value }: ITypeInfo): semver.ReleaseType { 19 | if (!validateRelease(value)) { 20 | throw new Error( 21 | `Option --${name} must be a valid release type but got: ${value}.\nAccepted values are ${ 22 | releases.join(", ") 23 | }.`, 24 | ); 25 | } 26 | return value as semver.ReleaseType; 27 | } 28 | 29 | export function validateVersion(value: string): boolean { 30 | return !!semver.valid(value); 31 | } 32 | 33 | export function versionType({ name, value }: ITypeInfo): string { 34 | if (!validateVersion(value)) { 35 | throw new Error( 36 | `Option --${name} must be a valid version but got: ${value}.\nVersion must follow Semantic Versioning 2.0.0.`, 37 | ); 38 | } 39 | return value; 40 | } 41 | 42 | export function validateURL(value: string): boolean { 43 | const urlRegex = 44 | /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/; 45 | return !!value.match(urlRegex); 46 | } 47 | 48 | export function urlType({ name, value }: ITypeInfo): string { 49 | if (!validateURL(value)) { 50 | throw new Error( 51 | `Option --${name} must be a valid url but got: ${value}.`, 52 | ); 53 | } 54 | return value; 55 | } 56 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | export const version = "0.3.10"; 2 | -------------------------------------------------------------------------------- /test/deps.ts: -------------------------------------------------------------------------------- 1 | // *_test.ts files 2 | export { 3 | assert, 4 | assertEquals, 5 | } from "https://x.nest.land/std@0.107.0/testing/asserts.ts"; 6 | 7 | export { expandGlob } from "https://x.nest.land/std@0.107.0/fs/mod.ts"; 8 | --------------------------------------------------------------------------------