├── .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 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
9 |
10 |
11 |
Eggs CLI
12 |
13 | The CLI used to publish and update modules in nest.land.
14 |
15 |
16 |
17 |
18 |
19 |
23 |
27 |
31 |
32 |
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 |
--------------------------------------------------------------------------------