├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── docs ├── CNAME ├── README.md ├── client-spec.md └── spec.md ├── examples ├── packspec.1.json └── packspec.1.lua ├── lua ├── packspec │ ├── schema.lua │ └── version_parser.lua └── utils │ └── format_cjson.lua ├── schema └── packspec_schema.json ├── scripts ├── generate_json_schema.lua └── packspec.lua └── spec └── version_parser_spec.lua /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "Test" 2 | on: 3 | pull_request: 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - run: | 10 | sudo apt-get update 11 | sudo apt-get install -y luarocks 12 | - run: make test 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | deps 6 | *.src.rock 7 | *.zip 8 | *.tar.gz 9 | 10 | # Object files 11 | *.o 12 | *.os 13 | *.ko 14 | *.obj 15 | *.elf 16 | 17 | # Precompiled Headers 18 | *.gch 19 | *.pch 20 | 21 | # Libraries 22 | *.lib 23 | *.a 24 | *.la 25 | *.lo 26 | *.def 27 | *.exp 28 | 29 | # Shared objects (inc. Windows DLLs) 30 | *.dll 31 | *.so 32 | *.so.* 33 | *.dylib 34 | 35 | # Executables 36 | *.exe 37 | *.out 38 | *.app 39 | *.i*86 40 | *.x86_64 41 | *.hex 42 | 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Configure luarocks to use luajit-openresty 2 | # 3 | # Example for MacOS: 4 | # brew install luajit-openresty 5 | # luarocks config --local lua_dir /opt/homebrew/opt/luajit-openresty 6 | # 7 | # This will configure luarocks using 5.1 to use luajit-openresty. 8 | LUA_VERSION=5.1 9 | 10 | LUA_PATH = deps/share/lua/$(LUA_VERSION)/?.lua;deps/share/lua/$(LUA_VERSION)/?/init.lua 11 | LUA_PATH := $(LUA_PATH);lua/?.lua;lua/?/init.lua 12 | LUA_PATH := $(LUA_PATH);$(shell lua$(LUA_VERSION) -e 'print(package.path)') 13 | export LUA_PATH 14 | 15 | export LUA_CPATH = deps/lib/lua/$(LUA_VERSION)/?.so;$(shell lua$(LUA_VERSION) -e 'print(package.cpath)') 16 | 17 | ifdef PCRE_DIR 18 | LREXLIB_PCRE_FLAGS = PCRE_DIR=$(PCRE_DIR) 19 | endif 20 | # Example for MacOS installed via Homebrew: 21 | # PCRE_DIR = /opt/homebrew/Cellar/pcre/8.45 22 | 23 | LUAROCKS = luarocks --lua-version=$(LUA_VERSION) --tree=deps 24 | 25 | deps: 26 | $(LUAROCKS) install busted 27 | $(LUAROCKS) install lrexlib-pcre $(LREXLIB_PCRE_FLAGS) 28 | $(LUAROCKS) install inspect 29 | $(LUAROCKS) install net-url 30 | $(LUAROCKS) install jsonschema 31 | $(LUAROCKS) install lua-cjson 32 | 33 | .PHONY: test 34 | test: deps json 35 | ./scripts/packspec.lua examples/packspec.1.lua 36 | ./scripts/packspec.lua examples/packspec.1.json 37 | ./deps/bin/busted 38 | 39 | .PHONY: json 40 | json: deps 41 | ./scripts/generate_json_schema.lua 42 | 43 | .PHONY: clean 44 | clean: 45 | $(RM) -rf deps 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pkg.json 2 | 3 | > The JSON of package specs. 4 | 5 | `pkg.json` is a wild-west "package" format for defining packages without a package system. 6 | It's a (very) limited subset of NPM's `package.json` that allows any project to declare dependencies on arbitrary URLs. 7 | 8 | The initial use-case is for Vim and Emacs plugins (which can be downloaded from anywhere), but the format is designed to be generic. 9 | 10 | See [/docs](https://github.com/neovim/packspec/tree/main/docs) for full documentation. 11 | 12 | ## TL;DR 13 | 14 | ``` 15 | { 16 | "name" : "lspconfig", // OPTIONAL cosmetic name, not used for resolution nor filesystem locations. 17 | "description" : "Quickstart configurations for the Nvim-lsp client", // OPTIONAL 18 | "engines": { 19 | "nvim": "^0.10.0", 20 | "vim": "^9.1.0" 21 | }, 22 | "repository": { // REQUIRED 23 | "type": "git", // reserved for future use 24 | "url": "https://github.com/neovim/nvim-lspconfig" 25 | }, 26 | "dependencies" : { // OPTIONAL 27 | "https://github.com/neovim/neovim" : "0.6.1", 28 | "https://github.com/lewis6991/gitsigns.nvim" : "0.3" 29 | }, 30 | } 31 | ``` 32 | 33 | ## Build 34 | 35 | brew install luarocks 36 | make 37 | make test 38 | 39 | ## Run tests 40 | 41 | cd test-npm/ 42 | npm ci 43 | npm run test 44 | 45 | ## What about LuaRocks? 46 | 47 | LuaRocks is a natural choice as the Nvim plugin manager, but defining a "federated package spec" also makes sense because: 48 | 49 | - We can do both, at low cost. `pkg.json` is a fairly "cheap" approach. 50 | - LuaRocks is a "centralized" approach that requires active participation from many plugins. 51 | In contrast, `pkg.json` is a decentralized, "infectious" approach that is useful at the "leaf nodes": 52 | it only requires the consumer to provide a `pkg.json`, the upstream dependencies don't need to be "compliant" or participate in any way. 53 | - LuaRocks + Nvim is starting to see [progress](https://github.com/nvim-neorocks), but momentum will take time. 54 | A decentralized, lowest-common-denominator, "infectious" approach can be tried without losing much time or effort. 55 | - There's no central _asset registry_, just a bunch of URLs. (Though "aggregators" are possible and welcome.) 56 | - LuaRocks has 10x more scope than `pkg.json` and [unresolved edge cases](https://github.com/luarocks/luarocks/issues/905). 57 | `pkg.json` side-steps that by punting the ecosystem-dependent questions to the client. 58 | 59 | ## Release 60 | 61 | TBD 62 | 63 | ## TODO 64 | 65 | - [x] specify packspec (above) 66 | - [ ] specify ecosystem-agnostic client behavior (report conflicts, fetch things into `pack/` dir, update existing dir, ...) 67 | - [ ] specify what is undefined (i.e. owned by the per-ecosystem "engine", for example vim/nvim packages are fetched into 'packpath') 68 | - [ ] TODO: support other kinds of artifacts, like zip archives or binaries. 69 | - [ ] nested packages in workspace (`foo/pkg.json`, `foo/bar/pkg.json`) 70 | - [ ] Nvim: client can support conflicting dependencies by using git worktree. 71 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | packspec.org -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # pkg.json 2 | 3 | `pkg.json` is a wild-west "package" format for defining packages without a package system. 4 | It's a (very) limited subset of NPM's `package.json` that allows any project to declare dependencies on arbitrary URLs. 5 | 6 | The initial use-case is for Vim and Emacs plugins (which can be downloaded from anywhere), but the format is designed to be generic. 7 | 8 | # TL;DR 9 | 10 | ``` 11 | { 12 | "name" : "lspconfig", // OPTIONAL cosmetic name, not used for resolution nor filesystem locations. 13 | "description" : "Quickstart configurations for the Nvim-lsp client", // OPTIONAL 14 | "engines": { 15 | "nvim": "^0.10.0", 16 | "vim": "^9.1.0" 17 | }, 18 | "repository": { // REQUIRED 19 | "type": "git", // reserved for future use 20 | "url": "https://github.com/neovim/nvim-lspconfig" 21 | }, 22 | "dependencies" : { // OPTIONAL 23 | "https://github.com/neovim/neovim" : "0.6.1", 24 | "https://github.com/lewis6991/gitsigns.nvim" : "0.3" 25 | }, 26 | } 27 | ``` 28 | 29 | # Features 30 | 31 | - `pkg.json` is just a way to declare dependencies on URLs and versions 32 | - Decentralized ("federated", omg) 33 | - Subset of `package.json` 34 | - Upstream dependencies don't need a `pkg.json` file. 35 | - Gives aggregators a way to find plugins for their `engine`. 36 | 37 | # Used by: 38 | 39 | - [lazy.nvim](https://github.com/folke/lazy.nvim/) 40 | - TBD 41 | 42 | # Limitations 43 | 44 | - No client spec (yet): only the format is specified, not client behavior. 45 | - No official client (yet) 46 | 47 | # Package requirements 48 | 49 | The [package specification](./spec.md) specifies the structure of a package and the `pkg.json` format. 50 | 51 | - Dependency URLs are expected to be git repos. 52 | - TODO: support other kinds of artifacts, like zip archives or binaries. 53 | 54 | # Client requirements 55 | 56 | - `git` (packages can live at any git URL) 57 | - JSON parser 58 | - [client guidelines](./client-spec.md) 59 | 60 | # Design 61 | 62 | 1. Support _all "assets" or "artifacts" of any kind_. 63 | 1. Why JSON: 64 | - ubiquitous 65 | - "machine readable" (sandboxed by design): can recursively download an entire dependency tree before executing any code, including hooks. Aggregators such as https://neovimcraft.com/ can consume it. 66 | - Turing-complete formats (unlike JSON) invite sprawling, special-case behavior (nvim-lspconfig is a [living example](https://github.com/neovim/nvim-lspconfig/pull/2595)). 67 | 1. Client requirements are only `git` and a JSON parser. 68 | 1. Avoid fields that overlap with info provided by git. Git is a client requirement. Git is the "common case" for servers (but not a requirement). 69 | 1. Strive to be a subset of NPM's `package.json`. Avoid unnecessary entropy. 70 | 1. No side-effects: evaluating `pkg.json` not have side-effects, only input and output. 71 | 72 | # References 73 | 74 | - https://json-schema.org/ 75 | - https://github.com/luarocks/luarocks/wiki/Rockspec-format 76 | - lazy.nvim [pkg.json impl](https://github.com/folke/lazy.nvim/pull/910/files#diff-eeb8f2e48ace6e2f4c40bf159b7f59e5eb1208e056a3f9f1b9cc6822ecb45371) 77 | - [A way for artifacts to depend on other artifacts.](https://sink.io/jmk/artifact-system) 78 | -------------------------------------------------------------------------------- /docs/client-spec.md: -------------------------------------------------------------------------------- 1 | # pkg.json client specification 2 | 3 | _Work-in-progress: These guideliens are subject to change._ 4 | 5 | pkg.json clients... 6 | 7 | ## Fetching dependencies 8 | 9 | - MUST be able to fetch and parse `pkg.json` files 10 | - MUST be able to use `git` to clone dependencies and query git repo info 11 | - MUST be able to fetch tagged commits for specified package versions 12 | - MUST be able to check for updated `pkg.json` files 13 | 14 | ## Resolving dependencies 15 | 16 | - MUST be able to check the current version of `Neovim` and warn on incompatibility 17 | - MUST be able to fetch and manage the specified versions of dependencies transitively: if dependencies have `pkg.json`, read it and process it, recursively. 18 | - MUST either be able to solve for compatible versions of dependency packages across all dependency relationships, or warn users if using a potentially inconsistent version resolution strategy (e.g. picking the first specified version of a dependency). 19 | - MAY remove dependencies when they are no longer required (transitively) by any user-specified packages 20 | -------------------------------------------------------------------------------- /docs/spec.md: -------------------------------------------------------------------------------- 1 | # pkg.json format specification 2 | 3 | ## Example 4 | 5 | ``` 6 | { 7 | "name" : "lspconfig", // OPTIONAL cosmetic name, not used for resolution nor filesystem locations. 8 | "description" : "Quickstart configurations for the Nvim-lsp client", // OPTIONAL 9 | "engines": { 10 | "nvim": "^0.10.0", 11 | "vim": "^9.1.0" 12 | }, 13 | "repository": { // REQUIRED 14 | "type": "git", // reserved for future use 15 | "url": "https://github.com/neovim/nvim-lspconfig" 16 | }, 17 | "dependencies" : { // OPTIONAL 18 | "https://github.com/neovim/neovim" : "0.6.1", 19 | "https://github.com/lewis6991/gitsigns.nvim" : "0.3" 20 | }, 21 | } 22 | ``` 23 | 24 | - Dependencies aren't required to have a `pkg.json` file. Only required for the "leaf nodes". 25 | - `pkg.json` can declare a dependency on any random artifact fetchable by URL. The upstream dependency doesn't need a `pkg.json`. 26 | - Version specifiers in `dependencies` follow the [NPM version range spec](https://devhints.io/semver) ~~[cargo spec](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html)~~ 27 | - Supported by Nvim `vim.version.range()`. 28 | - Extensions to npm version spec: 29 | - `"HEAD"` means git HEAD. (npm version spec defines `""` and `"*"` as latest stable version.) 30 | - ~~Do NOT support "Combined ranges".~~ 31 | - Treat any string of length >=7 and lacking "." as a commit-id. 32 | - Only support commit-id, tags, and HEAD. 33 | - Tags must contain a non-alphanumeric char. 34 | - Out of scope: 35 | - "pack" (creating a package) 36 | - "publish" is out of scope, because `pkg.json` is decentralized. Publishing a package means pushing it to a git repo with a top-level `pkg.json`. 37 | - "uninstall" https://docs.npmjs.com/cli/v9/using-npm/scripts#a-note-on-a-lack-of-npm-uninstall-scripts 38 | 39 | ## Semantic versioning 40 | 41 | Packages SHOULD be [semantically versioned](https://semver.org/). While other versioning schema have uses, semver allows for plugin managers to provide smart dependency resolution. 42 | 43 | ## File location 44 | 45 | Packages MUST have a single, top-level package metadata file named `pkg.json`. 46 | This may be relaxed in the future. 47 | 48 | ## Fields 49 | 50 | * metadata: `pkg.json` allows arbitrary user-defined fields at any nesting level, as long as they don't conflict with specification-owned fields. 51 | * Like NPM's `package.json`, this makes the format easy to extend. But application fields should be chosen to avoid potential conflict with fields added to future versions of the spec. For example, `devDependencies` may be added to the spec so applications SHOULD NOT extend the format with that field. 52 | * `package` (String) the name of the package 53 | * `version` (String) the version of the package. SHOULD obey semantic versioning conventions. Plugins SHOULD have a git _tag_ matching this version. For all version identifiers, implementation should check for a `version` prefixed with `v` in the git repository, as this is a common convention. 54 | * `source` (String) The URL of the package source archive. Examples: "http://github.com/downloads/keplerproject/wsapi/wsapi-1.3.4.tar.gz", "git://github.com/keplerproject/wsapi.git". Different protocols are supported: 55 | * `file://` - for URLs in the local filesystem (note that for Unix paths, the root slash is the third slash, resulting in paths like "file:///full/path/filename" 56 | * `dependencies` (`List[Table]`) Object whose keys are URLs and values are version specifiers. 57 | * Compare NPM, where URL is the value rather than the key: [URLs as Dependencies](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#urls-as-dependencies) 58 | 59 | ## Changes 60 | 61 | - renamed `packspec.json` to `pkg.json` ~~`deps.json`~~ (to hint that it's basically a subset of NPM's `package.json`) 62 | - removed `"version" : "0.1.2",` because package version is provided by the `.git` repo info 63 | - removed `external_dependencies` 64 | - removed `specification_version`. The lack of a "spec version" field means the spec version is `1.0.0`. If breaking changes are ever needed then we could introduce a "spec version" field. 65 | - renamed `"source" : "git:…",` to `repository.url` 66 | - renamed `package` to `name` (to align with NPM) 67 | - changed the shape of `description` from object to string (to align with NPM) 68 | - changed `dependencies` shape to align with NPM. Except the keys are URLs. 69 | - Leaves the door open for non-URL keys in the future. 70 | 71 | ## Closed questions 72 | 73 | - Top-level application-defined "metadata" field (`client`, `user`, `metadata`, ...?) for use by clients (package managers)? 74 | - `pkg.json` allows arbitrary application-defined fields, as `package.json` does. 75 | - "Ecosystem-agnostic" means that https://luarocks.org packages can't be consumed? 76 | - If Nvim plugins can successfully use luarocks then `pkg.json` is redundant. `pkg.json` is only useful for ecosystems that don't have centralized package management. 77 | - Are git submodules/subtrees a viable solution for git-only dependency trees? 78 | - https://stackoverflow.com/a/61961021/152142 79 | - pro: avoids another package/deps format 80 | - con: 81 | - not easy for package authors to implement (run `git` commands instead of editing a json file) 82 | - no `engines` field: how will aggregators build a package list? 83 | - no support for non-git blobs 84 | - Does the lack of a `version` field mean that a manifest file always tracks HEAD of the git repo? 85 | - The dependents declare what version they need, which must be available as a git tag in the dependency. Thus no need for `pkg.json` to repeat that information. The reason that `package.json` and other package formats need a `version` field is because they _don't_ require a `.git` repo to be present. 86 | - Should consumers of dependencies need to control how a dependency is resolved? 87 | - `repository.type` is available for future use if we want to deal with that. 88 | - It'd be nice if the spec enforces globally unique names... Then `dependencies` could look like `{ "dependencies": { "plenary.nvim": "1.0.0" } }` 89 | - Requiring URIs achieves that, without a central registry. 90 | - Should `name` be removed? Because `repository.url` already defines the "name" (which can be prettified in UIs). 91 | - Defined `name` as OPTIONAL and strictly cosmetic (not used for programmatic decisions or filesystem paths). 92 | - `package.json` has an [`engines` field](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#engines) that declares what software can _run_ the package. Example: 93 | ``` 94 | "engines": { 95 | "vscode": "^1.71.0" 96 | }, 97 | - How to deal with dependencies moving to a new host? Should `pkg.json` support "fallback" URLs? 98 | - The downstream must update its URLs. 99 | 100 | ## Open questions 101 | 102 | - should URL be the version (value) rather than the key? see NPM [URLs as Dependencies](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#urls-as-dependencies) 103 | - via @folke: most important to ideally be in the spec: 104 | - ✅ dependencies 105 | - ✅ metadata probably makes sense, NPM itself allows arbitrary fields in `package.json` 106 | - ❓ build 107 | - ❓whether the plugin needs/supports setup() 108 | - ❓main module for the plugin (lazy guesses that automatically, but would be better to have this part of the spec) 109 | - Can `pkg.json` be a strict subset of NPM `package.json` ? The ability to validate it with https://www.npmjs.com/package/read-package-json is attractive... 110 | - Non-git dependencies ("blobs"): require version specifier to be object (instead of string): 111 | - `"https://www.leonerd.org.uk/code/libvterm/libvterm-0.3.2.tar.gz": { "type": "tar+gzip", "version": "…" }` 112 | - How can a package manager know the blob has been updated if there's no git info? (Answer: undefined.) 113 | - `scripts` and "build-time" tasks ([lifecycle](https://docs.npmjs.com/cli/v9/using-npm/scripts#life-cycle-operation-order)) 114 | - Scripts must be array of strings (unlike npm package.json). 115 | - Scripts are run from the root of the package folder, regardless of what the current working directory is. 116 | - Predefined script names and lifecycle order: 117 | - These all run after fetching and writing the package contents to the engine-defined package path, in order. 118 | - `preinstall` 119 | - `install` 120 | - `postinstall` 121 | - Naming conflict: what happens if `https://github.com/.../foo` and `https://sr.ht/.../foo` are in the dependency tree? 122 | ``` 123 | .local/share/nvim/site/pack/github.com/start/ 124 | .local/share/nvim/site/pack/sr.ht/start/ 125 | ``` 126 | -------------------------------------------------------------------------------- /examples/packspec.1.json: -------------------------------------------------------------------------------- 1 | { 2 | "package" : "lspconfig", 3 | "version" : "0.1.2", 4 | "packspec": "0.1.0", 5 | "$schema": "https://raw.githubusercontent.com/nvim-lua/nvim-package-specification/master/schema/packspec_schema.json", 6 | "source" : "git://github.com/neovim/nvim-lspconfig.git", 7 | "description" : { 8 | "summary" : "Quickstart configurations for the Nvim-lsp client", 9 | "detailed" : "lspconfig is a set of configurations for language servers for use with Neovim's built-in language server client. Lspconfig handles configuring, launching, and attaching language servers", 10 | "homepage" : "https://github.com/neovim/nvim-lspconfig/", 11 | "license" : "Apache-2.0", 12 | "author": { 13 | "name": "Neovim team", 14 | "email": "neovim@neovim.io" 15 | } 16 | }, 17 | "dependencies" : { 18 | "neovim" : { 19 | "version" : ">= 0.6.1", 20 | "source" : "git://github.com/neovim/neovim.git" 21 | }, 22 | "gitsigns" : { 23 | "version" : "> 0.3", 24 | "source" : "git://github.com/lewis6991/gitsigns.nvim.git" 25 | } 26 | }, 27 | "external_dependencies" : { 28 | "git" : { 29 | "version" : ">= 1.6.0" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/packspec.1.lua: -------------------------------------------------------------------------------- 1 | package = "lspconfig" 2 | version = "0.1.2" 3 | packspec = "0.1.0" 4 | source = "git://github.com/neovim/nvim-lspconfig.git" 5 | description = { 6 | summary = "Quickstart configurations for the Nvim-lsp client", 7 | detailed = [[ 8 | lspconfig is a set of configurations for language servers for use with Neovim's built-in language server client. Lspconfig handles configuring, launching, and attaching language servers. 9 | ]], 10 | homepage = "git://github.com/neovim/nvim-lspconfig/", 11 | license = "Apache-2.0", 12 | author = { 13 | name = "Neovim team", 14 | email = "neovim@neovim.io", 15 | } 16 | } 17 | dependencies = { 18 | neovim = { 19 | version = ">= 0.6.1", 20 | source = "git://github.com/neovim/neovim.git", 21 | }, 22 | gitsigns = { 23 | version = "> 0.3", 24 | source = "git://github.com/lewis6991/gitsigns.nvim.git", 25 | } 26 | } 27 | external_dependencies = { 28 | git = { 29 | version = ">= 1.6.0", 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /lua/packspec/schema.lua: -------------------------------------------------------------------------------- 1 | local pat_version = [[(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*]] 2 | local pat_range = [[((==|~=|<|>|<=|>=|~>)\s*)?]]..pat_version 3 | 4 | local PAT_VERSION = "^"..pat_version.."$" 5 | local PAT_RANGE = "^"..pat_range..[[(\s*,\s*]]..pat_range..[[)*$]] 6 | local PAT_URL = [[^\w+://]] 7 | 8 | local function dedent(s) 9 | local lines = {} 10 | local indent = nil 11 | 12 | for line in s:gmatch("[^\n]*\n?") do 13 | if indent == nil then 14 | if not line:match("^%s*$") then 15 | -- save pattern for indentation from the first non-empty line 16 | indent, line = line:match("^(%s*)(.*)$") 17 | indent = "^"..indent.."(.*)$" 18 | table.insert(lines, line) 19 | end 20 | else 21 | if line:match("^%s*$") then 22 | -- replace empty lines with a single newline character. 23 | -- empty lines are handled separately to allow the 24 | -- closing "]]" to be one indentation level lower. 25 | table.insert(lines, "\n") 26 | else 27 | -- strip indentation on non-empty lines 28 | line = assert(line:match(indent), "inconsistent indentation") 29 | table.insert(lines, line) 30 | end 31 | end 32 | end 33 | 34 | lines = table.concat(lines) 35 | -- trim trailing whitespace 36 | return lines:match("^(.-)%s*$") 37 | end 38 | 39 | return { 40 | title = "packspec", 41 | description = "A package specification for Neovim", 42 | type = 'object', 43 | additionalProperties = false, 44 | properties = { 45 | package = { 46 | description = "The name of the package", 47 | type = "string", 48 | }, 49 | version = { 50 | description = dedent [[ 51 | The version of the package. Should obey semantic versioning 52 | conventions, for example `0.1.0`. Plugins should have a git commit 53 | with a `tag` matching this version. For all version identifiers, 54 | implementation should check for a `version` prefixed with `v` in the 55 | git repository, as this is a common convention. 56 | ]], 57 | type = "string", 58 | pattern = PAT_VERSION, 59 | }, 60 | packspec = { 61 | description = "The current specification version. (0.1.0) at this time.", 62 | type = "string", 63 | pattern = PAT_VERSION, 64 | }, 65 | ["$schema"] = { 66 | description = "The optional json schema URI for validation with json-language-server.", 67 | type = "string" 68 | }, 69 | source = { 70 | description = dedent [[ 71 | The URL of the package source archive. Examples: 72 | "http://github.com/downloads/keplerproject/wsapi/wsapi-1.3.4.tar.gz", 73 | "git://github.com/keplerproject/wsapi.git". Different protocols are 74 | supported: 75 | 76 | * `luarocks://` - for luarocks packages 77 | * `file://` - for URLs in the local filesystem (note that for Unix 78 | paths, the root slash is the third slash, resulting in paths like 79 | "file:///full/path/filename" 80 | * `git://` - for the Git source control manager 81 | * `git+https://` - for the Git source control manager when using 82 | repositories that need https:// URLs. 83 | * `git+ssh://` - for the Git source control manager when using 84 | repositories that need SSH login, such as git@example.com/myrepo. 85 | * `http://` - for HTTP URLs 86 | * `https://` - for HTTPS URLs 87 | ]], 88 | examples = { 89 | "luarocks://argparse", 90 | "git://github.com/nvim-lua/plenary.nvim" 91 | }, 92 | type = "string", 93 | pattern = PAT_URL, 94 | }, 95 | description = { 96 | description = "Description of the package", 97 | type = 'object', 98 | additionalProperties = false, 99 | properties = { 100 | summary = { 101 | description = dedent [[ 102 | Short description of the package, typically less than 100 character 103 | long. 104 | ]], 105 | type = "string" 106 | }, 107 | detailed = { 108 | description = dedent [[ 109 | Long-form description of the package, this should convey the 110 | package's principal functionality to the user without being as 111 | detailed as the package readme. 112 | ]], 113 | type = "string" 114 | }, 115 | author = { 116 | description = "Author of the package", 117 | type = 'object', 118 | additionalProperties = false, 119 | properties = { 120 | name = { 121 | description = 'Author name', 122 | type = "string" 123 | }, 124 | email = { 125 | description = 'Author email', 126 | type = "string" 127 | } 128 | } 129 | }, 130 | homepage = { 131 | description = dedent [[ 132 | Homepage of the package. In most cases this will be the GitHub URL. 133 | ]], 134 | type = "string" 135 | }, 136 | license = { 137 | description = dedent [[ 138 | This is [SPDX](https://spdx.org/licenses/) license identifier. Dual 139 | licensing is indicated via joining the relevant licenses via `/`. 140 | ]], 141 | type = "string" 142 | } 143 | } 144 | }, 145 | 146 | dependencies = { 147 | patternProperties = { 148 | [".*"] = { 149 | type = 'object', 150 | additionalProperties = false, 151 | properties = { 152 | version = { 153 | description = dedent [[ 154 | Version constraints on the package. 155 | * Accepted operators are the relational operators of Lua: 156 | == \~= < > <= >= , as well as a special operator, \~>, 157 | inspired by the "pessimistic operator" of RubyGems 158 | ("\~> 2" means ">= 2, < 3"; "~> 2.4" means ">= 2.4, < 2.5"). 159 | No operator means an implicit == (i.e., "lfs 1.0" is the 160 | same as "lfs == 1.0"). "lua" is an special dependency name; 161 | it matches not a rock, but the version of Lua in use. 162 | Multiple version constraints can be joined with a `comma`, 163 | e.g. `"neovim >= 5.0, < 7.0"`. 164 | * If no version is specified, then HEAD is assumed valid. 165 | * If no upper bound is specified, then any commit after the 166 | tag corresponding to the lower bound is assumed valid. The 167 | commit chosen is up to the plugin manager's discretion, but 168 | implementers are strongly encouraged to always use the 169 | latest valid commit. 170 | * If an upper bound is specified, then the the tag 171 | corresponding to that upper bound is the latest commit that 172 | is valid 173 | ]], 174 | type = 'string', 175 | pattern = PAT_RANGE, 176 | }, 177 | source = { 178 | description = dedent [[ 179 | Source of the dependency. See previous `source` description. 180 | ]], 181 | type = 'string', 182 | pattern = PAT_URL, 183 | }, 184 | releases_only = { 185 | description = dedent [[ 186 | Whether the package manager should only resolve version 187 | constraints to include tagged releases. 188 | ]], 189 | type = 'boolean' 190 | } 191 | } 192 | } 193 | } 194 | }, 195 | 196 | external_dependencies = { 197 | description = dedent [[ 198 | Like dependencies, this specifies packages which are required for the 199 | package but should *not* be managed by the Neovim package manager, such 200 | as `gcc` or `cmake`. Package managers are encouraged to provide a 201 | notification to the user if the dependency is not available. 202 | ]], 203 | patternProperties = { 204 | [".*"] = { 205 | type = "object", 206 | additionalProperties = false, 207 | properties = { 208 | version = { 209 | description = "Same as `dependencies`", 210 | type = 'string', 211 | pattern = PAT_RANGE, 212 | } 213 | } 214 | } 215 | } 216 | } 217 | }, 218 | 219 | required = { 220 | "package", 221 | "source" 222 | } 223 | 224 | } 225 | -------------------------------------------------------------------------------- /lua/packspec/version_parser.lua: -------------------------------------------------------------------------------- 1 | -- Example version parser implementation 2 | 3 | local parse = {} 4 | 5 | ---@alias Version number[] 6 | ---@alias Operator "==" | "~=" | "<" | ">" | "<=" | ">=" | "~>" 7 | ---@alias Range { [1]: Version, [2]: Operator|nil } 8 | 9 | --- Parse version 10 | ---@param s string 11 | ---@return Version 12 | function parse.version(s) 13 | assert(type(s) == "string", "expected string") 14 | 15 | -- trim leading and trailing whitespace 16 | s = assert(s:match("^%s*(.-)%s*$"), "invalid version") 17 | 18 | local v = {} 19 | while true do 20 | -- parse number 21 | local p = assert(s:match("^%d+"), "invalid version") 22 | table.insert(v, tonumber(p)) 23 | s = s:sub(#p + 1) 24 | 25 | -- try to parse next number 26 | if #s == 0 then break end 27 | assert(s:match("^%."), "invalid version") 28 | s = s:sub(2) 29 | end 30 | return v 31 | end 32 | 33 | --- Parse version ranges 34 | ---@param s string 35 | ---@return Range[] 36 | function parse.version_range(s) 37 | assert(type(s) == "string", "expected string") 38 | local res = {} 39 | 40 | while true do 41 | -- skip leading whitespace 42 | s = assert(s:match("^%s*(%S.*)"), "empty expression") 43 | 44 | -- parse operator 45 | local op = s:match("^[=~<>]=") or s:match("^[<>]") or s:match("^~>") 46 | if op then 47 | s = assert(s:sub(#op + 1):match("^%s*(%S.*)"), "expected version") 48 | end 49 | 50 | -- parse version 51 | local v = assert(s:match("^[%d%.]+"), "expected version") 52 | table.insert(res, { parse.version(v), op }) 53 | 54 | -- try to parse next range 55 | s = s:sub(#v + 1):match("^%s*(%S.*)") 56 | if not s then break end 57 | assert(s:match("^,"), "trailing characters") 58 | s = s:sub(2) 59 | end 60 | 61 | return res 62 | end 63 | 64 | return parse 65 | -------------------------------------------------------------------------------- /lua/utils/format_cjson.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2015 — 2016, Aapo Talvensaari 2 | -- All rights reserved. 3 | -- 4 | -- Redistribution and use in source and binary forms, with or without 5 | -- modification, are permitted provided that the following conditions are met: 6 | -- 7 | -- * Redistributions of source code must retain the above copyright notice, this 8 | -- list of conditions and the following disclaimer. 9 | -- 10 | -- * Redistributions in binary form must reproduce the above copyright notice, 11 | -- this list of conditions and the following disclaimer in the documentation 12 | -- and/or other materials provided with the distribution. 13 | -- 14 | -- * Neither the name of lua-resty-prettycjson nor the names of its 15 | -- contributors may be used to endorse or promote products derived from 16 | -- this software without specific prior written permission. 17 | -- 18 | -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | -- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | -- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | -- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | -- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | -- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | -- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | -- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | -- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | -- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | local ok, cjson = pcall(require, "cjson.safe") 30 | local enc = ok and cjson.encode or function() return nil, "Lua cJSON encoder not found" end 31 | local cat = table.concat 32 | local sub = string.sub 33 | local rep = string.rep 34 | return function(dt, lf, id, ac, ec) 35 | local s, e = (ec or enc)(dt) 36 | if not s then return s, e end 37 | lf, id, ac = lf or "\n", id or "\t", ac or " " 38 | local i, j, k, n, r, p, q = 1, 0, 0, #s, {}, nil, nil 39 | local al = sub(ac, -1) == "\n" 40 | for x = 1, n do 41 | local c = sub(s, x, x) 42 | if not q and (c == "{" or c == "[") then 43 | r[i] = p == ":" and cat{ c, lf } or cat{ rep(id, j), c, lf } 44 | j = j + 1 45 | elseif not q and (c == "}" or c == "]") then 46 | j = j - 1 47 | if p == "{" or p == "[" then 48 | i = i - 1 49 | r[i] = cat{ rep(id, j), p, c } 50 | else 51 | r[i] = cat{ lf, rep(id, j), c } 52 | end 53 | elseif not q and c == "," then 54 | r[i] = cat{ c, lf } 55 | k = -1 56 | elseif not q and c == ":" then 57 | r[i] = cat{ c, ac } 58 | if al then 59 | i = i + 1 60 | r[i] = rep(id, j) 61 | end 62 | else 63 | if c == '"' and p ~= "\\" then 64 | q = not q and true or nil 65 | end 66 | if j ~= k then 67 | r[i] = rep(id, j) 68 | i, k = i + 1, j 69 | end 70 | r[i] = c 71 | end 72 | p, i = c, i + 1 73 | end 74 | return cat(r) 75 | end 76 | -------------------------------------------------------------------------------- /schema/packspec_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "title": "packspec", 4 | "required": [ 5 | "package", 6 | "source" 7 | ], 8 | "properties": { 9 | "packspec": { 10 | "type": "string", 11 | "description": "The current specification version. (0.1.0) at this time.", 12 | "pattern": "^(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*$" 13 | }, 14 | "dependencies": { 15 | "patternProperties": { 16 | ".*": { 17 | "type": "object", 18 | "additionalProperties": false, 19 | "properties": { 20 | "releases_only": { 21 | "description": "Whether the package manager should only resolve version\nconstraints to include tagged releases.", 22 | "type": "boolean" 23 | }, 24 | "version": { 25 | "type": "string", 26 | "description": "Version constraints on the package.\n * Accepted operators are the relational operators of Lua:\n == \\~= < > <= >= , as well as a special operator, \\~>,\n inspired by the \"pessimistic operator\" of RubyGems\n (\"\\~> 2\" means \">= 2, < 3\"; \"~> 2.4\" means \">= 2.4, < 2.5\").\n No operator means an implicit == (i.e., \"lfs 1.0\" is the\n same as \"lfs == 1.0\"). \"lua\" is an special dependency name;\n it matches not a rock, but the version of Lua in use.\n Multiple version constraints can be joined with a `comma`,\n e.g. `\"neovim >= 5.0, < 7.0\"`.\n * If no version is specified, then HEAD is assumed valid.\n * If no upper bound is specified, then any commit after the\n tag corresponding to the lower bound is assumed valid. The\n commit chosen is up to the plugin manager's discretion, but\n implementers are strongly encouraged to always use the\n latest valid commit.\n * If an upper bound is specified, then the the tag\n corresponding to that upper bound is the latest commit that\n is valid", 27 | "pattern": "^((==|~=|<|>|<=|>=|~>)\\s*)?(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*(\\s*,\\s*((==|~=|<|>|<=|>=|~>)\\s*)?(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*)*$" 28 | }, 29 | "source": { 30 | "type": "string", 31 | "description": "Source of the dependency. See previous `source` description.", 32 | "pattern": "^\\w+:\/\/" 33 | } 34 | } 35 | } 36 | } 37 | }, 38 | "version": { 39 | "type": "string", 40 | "description": "The version of the package. Should obey semantic versioning\nconventions, for example `0.1.0`. Plugins should have a git commit\nwith a `tag` matching this version. For all version identifiers,\nimplementation should check for a `version` prefixed with `v` in the\ngit repository, as this is a common convention.", 41 | "pattern": "^(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*$" 42 | }, 43 | "external_dependencies": { 44 | "description": "Like dependencies, this specifies packages which are required for the\npackage but should *not* be managed by the Neovim package manager, such\nas `gcc` or `cmake`. Package managers are encouraged to provide a\nnotification to the user if the dependency is not available.", 45 | "patternProperties": { 46 | ".*": { 47 | "type": "object", 48 | "additionalProperties": false, 49 | "properties": { 50 | "version": { 51 | "type": "string", 52 | "description": "Same as `dependencies`", 53 | "pattern": "^((==|~=|<|>|<=|>=|~>)\\s*)?(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*(\\s*,\\s*((==|~=|<|>|<=|>=|~>)\\s*)?(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*)*$" 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "package": { 60 | "description": "The name of the package", 61 | "type": "string" 62 | }, 63 | "description": { 64 | "properties": { 65 | "summary": { 66 | "description": "Short description of the package, typically less than 100 character\nlong.", 67 | "type": "string" 68 | }, 69 | "homepage": { 70 | "description": "Homepage of the package. In most cases this will be the GitHub URL.", 71 | "type": "string" 72 | }, 73 | "author": { 74 | "properties": { 75 | "name": { 76 | "description": "Author name", 77 | "type": "string" 78 | }, 79 | "email": { 80 | "description": "Author email", 81 | "type": "string" 82 | } 83 | }, 84 | "type": "object", 85 | "description": "Author of the package", 86 | "additionalProperties": false 87 | }, 88 | "license": { 89 | "description": "This is [SPDX](https:\/\/spdx.org\/licenses\/) license identifier. Dual\nlicensing is indicated via joining the relevant licenses via `\/`.", 90 | "type": "string" 91 | }, 92 | "detailed": { 93 | "description": "Long-form description of the package, this should convey the\npackage's principal functionality to the user without being as\ndetailed as the package readme.", 94 | "type": "string" 95 | } 96 | }, 97 | "type": "object", 98 | "description": "Description of the package", 99 | "additionalProperties": false 100 | }, 101 | "source": { 102 | "pattern": "^\\w+:\/\/", 103 | "type": "string", 104 | "description": "The URL of the package source archive. Examples:\n\"http:\/\/github.com\/downloads\/keplerproject\/wsapi\/wsapi-1.3.4.tar.gz\",\n\"git:\/\/github.com\/keplerproject\/wsapi.git\". Different protocols are\nsupported:\n\n * `luarocks:\/\/` - for luarocks packages\n * `file:\/\/` - for URLs in the local filesystem (note that for Unix\n paths, the root slash is the third slash, resulting in paths like\n \"file:\/\/\/full\/path\/filename\"\n * `git:\/\/` - for the Git source control manager\n * `git+https:\/\/` - for the Git source control manager when using\n repositories that need https:\/\/ URLs.\n * `git+ssh:\/\/` - for the Git source control manager when using\n repositories that need SSH login, such as git@example.com\/myrepo.\n * `http:\/\/` - for HTTP URLs\n * `https:\/\/` - for HTTPS URLs", 105 | "examples": [ 106 | "luarocks:\/\/argparse", 107 | "git:\/\/github.com\/nvim-lua\/plenary.nvim" 108 | ] 109 | }, 110 | "$schema": { 111 | "description": "The optional json schema URI for validation with json-language-server.", 112 | "type": "string" 113 | } 114 | }, 115 | "additionalProperties": false, 116 | "description": "A package specification for Neovim" 117 | } 118 | -------------------------------------------------------------------------------- /scripts/generate_json_schema.lua: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env lua5.1 2 | 3 | -- local cjson = require('cjson') 4 | local schema = require('packspec.schema') 5 | local format_json = require('utils.format_cjson') 6 | local encoded_json = format_json(schema, "\n", " ") 7 | 8 | local file = io.open('schema/packspec_schema.json', 'w') 9 | 10 | io.output(file) 11 | io.write(encoded_json) 12 | io.close(file) 13 | -------------------------------------------------------------------------------- /scripts/packspec.lua: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env lua5.1 2 | 3 | local function fatal(s, ...) 4 | print(string.format(s, ...)) 5 | os.exit(1) 6 | end 7 | 8 | if not arg[1] then 9 | fatal('usage: %s [files...]', arg[0]) 10 | end 11 | 12 | local jsonschema = require('jsonschema') 13 | local cjson = require('cjson.safe') 14 | local schema = require('packspec.schema') 15 | 16 | local validator = jsonschema.generate_validator(schema) 17 | 18 | for _, path in ipairs(arg) do 19 | local spec 20 | if path:match('%.lua$') then 21 | local chunk, err = loadfile(path) 22 | if err then 23 | fatal('%s: loading failed\n%s', path, err) 24 | end 25 | 26 | spec = {} 27 | setfenv(chunk, spec) 28 | 29 | local ok 30 | ok, err = pcall(chunk) 31 | if not ok then 32 | fatal('%s: evaluation failed\n%s', path, err) 33 | end 34 | elseif path:match('%.json$') then 35 | local file, err = io.open(path, 'rb') 36 | if err then 37 | fatal('%s: failed to open file\n%s', path, err) 38 | end 39 | 40 | local json = file:read('*a') 41 | file:close() 42 | if not json then 43 | fatal('%s: could not read the file', path) 44 | end 45 | 46 | spec, err = cjson.decode(json) 47 | if err then 48 | fatal('%s: decoding json failed\n%s', path, err) 49 | end 50 | else 51 | fatal('invalid filename, expected lua or json file: %s', path) 52 | end 53 | 54 | local ok, err = validator(spec) 55 | if not ok then 56 | fatal('%s: validation failed\n%s', path, err) 57 | end 58 | 59 | print(string.format('%s: OK', path)) 60 | end 61 | -------------------------------------------------------------------------------- /spec/version_parser_spec.lua: -------------------------------------------------------------------------------- 1 | local parse = require("packspec.version_parser") 2 | 3 | local unpack = unpack or table.unpack 4 | assert:register("assertion", "err_nil", function(_, arguments) 5 | local ok, res = pcall(unpack(arguments)) 6 | if ok then 7 | return res == nil 8 | else 9 | return true 10 | end 11 | end) 12 | 13 | 14 | describe("version parser", function() 15 | local p = parse.version 16 | 17 | it("should parse version", function() 18 | assert.are.same(p("1"), { 1 }) 19 | assert.are.same(p("2.3"), { 2, 3 }) 20 | assert.are.same(p("4.5.6"), { 4, 5, 6 }) 21 | assert.are.same(p("12.34.56"), { 12, 34, 56 }) 22 | end) 23 | 24 | it("should fail on empty string", function() 25 | assert.err_nil(p, "") 26 | assert.err_nil(p, " ") 27 | end) 28 | 29 | it("should fail on invalid characters", function() 30 | assert.err_nil(p, "x") 31 | assert.err_nil(p, "1.x") 32 | assert.err_nil(p, "x.0") 33 | assert.err_nil(p, "x1.0") 34 | assert.err_nil(p, "1x.0") 35 | assert.err_nil(p, "1.x0") 36 | assert.err_nil(p, "1.0x") 37 | end) 38 | 39 | it("should fail on spaces between numbers", function() 40 | assert.err_nil(p, "1 0") 41 | assert.err_nil(p, "1. 0") 42 | assert.err_nil(p, "1 .0") 43 | assert.err_nil(p, "1 . 0") 44 | end) 45 | 46 | it("should fail on dots delimiting nothing", function() 47 | assert.err_nil(p, ".") 48 | assert.err_nil(p, "1..0") 49 | assert.err_nil(p, "1.") 50 | assert.err_nil(p, "1.0.") 51 | assert.err_nil(p, ".1") 52 | assert.err_nil(p, ".1.0") 53 | end) 54 | end) 55 | 56 | 57 | describe("version range parser", function() 58 | local p = parse.version_range 59 | 60 | it("should parse version", function() 61 | assert.are.same(p("1"), { 62 | { { 1 } }, 63 | }) 64 | assert.are.same(p("1.0"), { 65 | { { 1, 0 } }, 66 | }) 67 | end) 68 | 69 | it("should parse range", function() 70 | assert.are.same(p("== 1.0"), { 71 | { { 1, 0 }, "==" }, 72 | }) 73 | assert.are.same(p("~= 1.0"), { 74 | { { 1, 0 }, "~=" }, 75 | }) 76 | assert.are.same(p(">= 1.0"), { 77 | { { 1, 0 }, ">=" }, 78 | }) 79 | assert.are.same(p("<= 1.0"), { 80 | { { 1, 0 }, "<=" }, 81 | }) 82 | assert.are.same(p("> 1.0"), { 83 | { { 1, 0 }, ">" }, 84 | }) 85 | assert.are.same(p("< 1.0"), { 86 | { { 1, 0 }, "<" }, 87 | }) 88 | assert.are.same(p("~> 1.0"), { 89 | { { 1, 0 }, "~>" }, 90 | }) 91 | assert.are.same(p("==1.0"), { 92 | { { 1, 0 }, "==" }, 93 | }) 94 | end) 95 | 96 | it("should parse multiple ranges", function() 97 | assert.are.same(p(">= 1.1, < 2.0"), { 98 | { { 1, 1 }, ">=" }, 99 | { { 2, 0 }, "<" }, 100 | }) 101 | assert.are.same(p(">= 1.1 , < 2.0"), { 102 | { { 1, 1 }, ">=" }, 103 | { { 2, 0 }, "<" }, 104 | }) 105 | assert.are.same(p(">=1.1,<2.0"), { 106 | { { 1, 1 }, ">=" }, 107 | { { 2, 0 }, "<" }, 108 | }) 109 | assert.are.same(p(">= 1.1, < 2.0, == 1.2"), { 110 | { { 1, 1 }, ">=" }, 111 | { { 2, 0 }, "<" }, 112 | { { 1, 2 }, "==" }, 113 | }) 114 | end) 115 | 116 | it("should fail on empty string", function() 117 | assert.err_nil(p, "") 118 | assert.err_nil(p, " ") 119 | end) 120 | 121 | it("should fail on invalid characters", function() 122 | assert.err_nil(p, "x") 123 | assert.err_nil(p, "1.0x") 124 | end) 125 | 126 | it("should fail on invalid operators", function() 127 | assert.err_nil(p, "=") 128 | assert.err_nil(p, "~") 129 | end) 130 | 131 | it("should fail on missing operand", function() 132 | assert.err_nil(p, "==") 133 | assert.err_nil(p, "~=") 134 | assert.err_nil(p, ">=") 135 | assert.err_nil(p, "<=") 136 | assert.err_nil(p, ">") 137 | assert.err_nil(p, "<") 138 | assert.err_nil(p, "~>") 139 | end) 140 | 141 | it("should fail on empty expression after comma", function() 142 | assert.err_nil(p, "1.0,") 143 | assert.err_nil(p, "1.0, ") 144 | end) 145 | end) 146 | --------------------------------------------------------------------------------