├── .gitignore ├── .nvmrc ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── src ├── actions │ ├── completion │ │ ├── package-name.ts │ │ └── possible-keys.ts │ ├── hover │ │ └── package-details.ts │ ├── index.ts │ └── index.types.ts ├── composer-cli.ts ├── composer-schema.ts ├── context.ts ├── index.ts └── tsconfig.json ├── tests ├── __fixtures__ │ ├── composer-schema.json │ ├── composer.json │ └── in-progress.composer.json ├── composer.test.ts ├── helpers.ts ├── parsing.test.ts └── tsconfig.json └── tsconfig.base.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.4.0 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Composer Language Server 2 | 3 | A language server (LSP) for composer.json files. 4 | 5 | ## Features 6 | 7 | - **package name hover:** Shows full details about the package. 8 | - **package name completion:** Suggests packages as you type them. 9 | - **property completion:** Suggests property keys as you type them, fully nested. 10 | 11 | ### In the future 12 | 13 | If you have a feature request, please submit an issue so it can be considered. 14 | 15 | - **property hover:** Shows detailed documentation for a property. 16 | - **diagnostics:** Validate the file and report problems. 17 | 18 | 19 | ## Installation 20 | 21 | ```bash 22 | npm i -g composer-language-server 23 | ``` 24 | 25 | ### Requirements 26 | 27 | NodeJS, npm and composer installed and added to your path. 28 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "composer-language-server", 3 | "version": "0.1.1", 4 | "description": "Language server for composer.json files.", 5 | "scripts": { 6 | "build": "tsc -b src", 7 | "watch": "tsc -b src -w", 8 | "test": "jest -c" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/laytan/composer-language-server.git" 13 | }, 14 | "author": "Laytan Laats", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/laytan/composer-language-server/issues" 18 | }, 19 | "homepage": "https://github.com/laytan/composer-language-server#readme", 20 | "dependencies": { 21 | "axios": "^0.24.0", 22 | "jsonc-parser": "^3.0.0", 23 | "vscode-languageserver": "^7.0.0", 24 | "vscode-languageserver-textdocument": "^1.0.3" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^27.0.3", 28 | "@types/json-schema": "^7.0.9", 29 | "@types/node": "^16.11.12", 30 | "jest": "^27.4.4", 31 | "prettier": "^2.5.1", 32 | "ts-jest": "^27.1.1", 33 | "typescript": "^4.5.3" 34 | }, 35 | "bin": { 36 | "composer-language-server": "./dist/index.js" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | singleQuote: true 4 | }; 5 | -------------------------------------------------------------------------------- /src/actions/completion/package-name.ts: -------------------------------------------------------------------------------- 1 | import { CompletionResult } from '../index.types'; 2 | import { search } from '../../composer-cli'; 3 | import Context from '../../context'; 4 | 5 | export default async function (context: Context): Promise { 6 | if (!context.isDefiningDependencies()) return []; 7 | 8 | const query = context.getCurrentKeyValue(); 9 | 10 | if (!query || query.length <= 3) return []; 11 | 12 | return search(query); 13 | } 14 | -------------------------------------------------------------------------------- /src/actions/completion/possible-keys.ts: -------------------------------------------------------------------------------- 1 | import Context from '../../context'; 2 | import { CompletionResult } from '../index.types'; 3 | import { getSchema, getPossibleProperties } from '../../composer-schema'; 4 | 5 | /** 6 | * Returns the possible properties for the current path. 7 | */ 8 | export default async function (context: Context): Promise { 9 | try { 10 | const schema = await getSchema(); 11 | if (!schema) { 12 | throw new Error('No schema found or the schema could not be parsed.'); 13 | } 14 | 15 | if (!context.isAtPropertyKey()) return []; 16 | 17 | const properties = getPossibleProperties(schema, context.location.path); 18 | 19 | return Object.entries(properties).map(([k, v]) => ({ 20 | name: k, 21 | description: v.description ?? '', 22 | })); 23 | } catch (e) { 24 | if (typeof e === 'object' && typeof e?.toString === 'function') { 25 | context.logger.error(e.toString()); 26 | } 27 | 28 | return []; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/actions/hover/package-details.ts: -------------------------------------------------------------------------------- 1 | import Context from '../../context'; 2 | import { HoverResult, HoverResultKind } from '../index.types'; 3 | import { fetchDetails } from '../../composer-cli'; 4 | 5 | export default async function ( 6 | context: Context 7 | ): Promise { 8 | if (!context.isDefiningDependencies()) return; 9 | 10 | const packageName = context.getCurrentKeyValue(); 11 | 12 | // Is the property of adequate length? 13 | if (!packageName || packageName.length <= 3) return; 14 | 15 | // Fetch details and return them. 16 | const details = await fetchDetails(packageName); 17 | 18 | return { 19 | value: details ?? 'Package not found.', 20 | kind: HoverResultKind.Plain, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/actions/index.ts: -------------------------------------------------------------------------------- 1 | import Context from '../context'; 2 | import { Actions, CompletionResult, HoverResult } from './index.types'; 3 | 4 | import hoverPackageDetailsAction from './hover/package-details'; 5 | import completePackageNameAction from './completion/package-name'; 6 | import completePossibleKeysAction from './completion/possible-keys'; 7 | 8 | export const actions = { 9 | hover: [hoverPackageDetailsAction], 10 | completion: [completePackageNameAction, completePossibleKeysAction], 11 | } as Actions; 12 | 13 | /** 14 | * Runs the defined hover actions, stops at first result. 15 | */ 16 | export async function runHover( 17 | context: Context 18 | ): Promise { 19 | for (const action of actions.hover) { 20 | const result = await action(context); 21 | if (result) return result; 22 | } 23 | } 24 | 25 | /** 26 | * Runs the defined completion actions, combining each action's results. 27 | */ 28 | export async function runCompletion( 29 | context: Context 30 | ): Promise { 31 | return ( 32 | await Promise.all(actions.completion.map((action) => action(context))) 33 | ).flat(); 34 | } 35 | -------------------------------------------------------------------------------- /src/actions/index.types.ts: -------------------------------------------------------------------------------- 1 | import Context from '../context'; 2 | 3 | export enum HoverResultKind { 4 | Markdown = 'markdown', 5 | Plain = 'plaintext', 6 | } 7 | 8 | export interface HoverResult { 9 | kind: HoverResultKind; 10 | value: string; 11 | } 12 | 13 | export interface CompletionResult { 14 | name: string; 15 | description: string; 16 | } 17 | 18 | export type Action = (context: Context) => Promise; 19 | 20 | export interface Actions { 21 | hover: Action[]; 22 | completion: Action[]; 23 | } 24 | -------------------------------------------------------------------------------- /src/composer-cli.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from 'util'; 2 | import { exec } from 'child_process'; 3 | 4 | const cli = promisify(exec); 5 | 6 | interface SearchResult { 7 | name: string; 8 | description: string; 9 | } 10 | 11 | /** 12 | * Checks that the returned objects are search results. 13 | */ 14 | function isSearchResult(result: any): result is SearchResult[] { 15 | if (!Array.isArray(result)) { 16 | return false; 17 | } 18 | 19 | if ( 20 | result.some( 21 | (res) => 22 | typeof res?.name !== 'string' && typeof res?.description !== 'string' 23 | ) 24 | ) { 25 | return false; 26 | } 27 | 28 | return true; 29 | } 30 | 31 | /** 32 | * Searches for a package using composer's cli. 33 | */ 34 | export async function search(query: string): Promise { 35 | // search global so an incomplete (invalid JSON) composer.json does not make this error. 36 | const { stdout } = await cli(`composer global search ${query} --format json`); 37 | 38 | const result = JSON.parse(stdout); 39 | 40 | if (!isSearchResult(result)) return []; 41 | 42 | return result; 43 | } 44 | 45 | /** 46 | * Searches details for the given package name. 47 | */ 48 | export async function fetchDetails(name: string): Promise { 49 | try { 50 | // search global so an incomplete (invalid JSON) composer.json does not make this error. 51 | const { stdout } = await cli(`composer global show ${name} -a`); 52 | return stdout; 53 | } catch { 54 | return undefined; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/composer-schema.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Segment } from 'jsonc-parser'; 3 | import { JSONSchema4 } from 'json-schema'; 4 | 5 | let schema: JSONSchema4 | undefined; 6 | export async function getSchema(): Promise { 7 | if (schema === undefined) { 8 | schema = await axios 9 | .get('https://getcomposer.org/schema.json') 10 | .then((r) => r.data as JSONSchema4); 11 | } 12 | 13 | return schema; 14 | } 15 | 16 | /** 17 | * Follows the given path in the schema and returns the properties that are possible at that path. 18 | */ 19 | export function getPossibleProperties( 20 | schema: JSONSchema4, 21 | path: Segment[] 22 | ): Record { 23 | // Remove the last element because that is the element currently being written. 24 | path.pop(); 25 | 26 | // Loop through the path and keep deepening the properties 27 | // untill we are at the current location (last index of path). 28 | let properties: Record | undefined = schema.properties; 29 | while (path.length) { 30 | const part = path.shift(); 31 | 32 | if (!part || typeof part === 'number') continue; 33 | if (!properties) break; 34 | 35 | // Resolve $ref to the definitions. 36 | if (typeof properties[part].$ref === 'string') { 37 | // $ref is something like #/definitions/authors, we only need the last one. 38 | // to retrieve the definition. 39 | const definitionParts = (properties[part].$ref as string).split('/'); 40 | const definitionId = definitionParts[definitionParts.length - 1]; 41 | 42 | properties = schema.definitions?.[definitionId] ?? undefined; 43 | } else { 44 | properties = properties[part]; 45 | } 46 | 47 | // If the next part is a number, we are in an array and need to use the 48 | // .item.properties. 49 | properties = 50 | typeof path[0] === 'number' 51 | ? properties?.items.properties ?? undefined 52 | : properties?.properties ?? undefined; 53 | } 54 | 55 | return properties ?? {}; 56 | } 57 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import { Position as LspPosition, Connection } from 'vscode-languageserver'; 2 | import { TextDocument } from 'vscode-languageserver-textdocument'; 3 | import { 4 | Node, 5 | getLocation, 6 | findNodeAtOffset, 7 | parseTree, 8 | Location, 9 | } from 'jsonc-parser'; 10 | import { inspect } from 'util'; 11 | 12 | export interface Position { 13 | line: number; 14 | character: number; 15 | offset: number; 16 | } 17 | 18 | export type LoggerFunction = (message: any, ...optionalParams: any[]) => void; 19 | 20 | export interface Logger { 21 | info: LoggerFunction; 22 | warn: LoggerFunction; 23 | error: LoggerFunction; 24 | } 25 | 26 | /** 27 | * An instance of context is passed to all actions, 28 | * it contains functionality for the user's current position. 29 | */ 30 | export default class Context { 31 | /** 32 | * Get the parsed AST tree, cached with a getter as it parses the document (heavy). 33 | */ 34 | private _tree: Node | undefined; 35 | public get tree(): Node { 36 | if (!this._tree) { 37 | this._tree = parseTree(this.content); 38 | } 39 | 40 | return this._tree!; 41 | } 42 | 43 | /** 44 | * Get the current location context, cached with a getter as it parses the document (heavy). 45 | */ 46 | private _location: Location | undefined; 47 | public get location(): Location { 48 | if (!this._location) { 49 | this._location = getLocation(this.content, this.position.offset); 50 | } 51 | 52 | return this._location; 53 | } 54 | 55 | public constructor( 56 | public readonly content: string, 57 | public readonly position: Position, 58 | public readonly logger: Logger 59 | ) {} 60 | 61 | /** 62 | * Creates a context from the LSP provided variables. 63 | */ 64 | public static fromLSP( 65 | document: TextDocument, 66 | position: LspPosition, 67 | connection: Connection 68 | ): Context { 69 | const offset = document.offsetAt(position); 70 | return new Context( 71 | document.getText(), 72 | { ...position, offset }, 73 | connection.console 74 | ); 75 | } 76 | 77 | /** 78 | * Whether the current locations is in a require or require-dev block. 79 | */ 80 | public isDefiningDependencies(): boolean { 81 | return ( 82 | ['require', 'require-dev'].includes(this.location.path[0].toString()) && 83 | this.location.path.length === 2 84 | ); 85 | } 86 | 87 | /** 88 | * Whether the user is currently at a property key. 89 | */ 90 | public isAtPropertyKey(): boolean { 91 | const currentNode = this.getCurrentNode(); 92 | this.logger.info(inspect(currentNode)); 93 | if (currentNode?.type === 'string') { 94 | return currentNode.parent?.children?.[0] === currentNode; 95 | } 96 | 97 | return currentNode?.type === 'property' && !!currentNode?.children?.length; 98 | } 99 | 100 | /** 101 | * Returns the value of the current line's key. 102 | */ 103 | public getCurrentKeyValue(): string | undefined { 104 | if (!this.isAtPropertyKey) return; 105 | 106 | const currentNode = this.getCurrentNode(); 107 | 108 | const property = 109 | currentNode?.type === 'string' ? currentNode.parent : currentNode; 110 | return property?.children?.[0].value ?? undefined; 111 | } 112 | 113 | /** 114 | * Returns the node at the current offset. 115 | */ 116 | public getCurrentNode(): Node | undefined { 117 | return findNodeAtOffset(this.tree, this.position.offset); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { 3 | createConnection, 4 | TextDocuments, 5 | ProposedFeatures, 6 | CompletionItem, 7 | TextDocumentPositionParams, 8 | TextDocumentSyncKind, 9 | CompletionItemKind, 10 | HoverParams, 11 | Hover, 12 | } from 'vscode-languageserver/node'; 13 | import { TextDocument } from 'vscode-languageserver-textdocument'; 14 | 15 | import Context from './context'; 16 | import { runHover, runCompletion } from './actions'; 17 | 18 | // Create a connection for the server, using Node's IPC as a transport. 19 | // Also include all preview / proposed LSP features. 20 | const connection = createConnection(ProposedFeatures.all); 21 | 22 | // Create a simple text document manager. 23 | const documents: TextDocuments = new TextDocuments(TextDocument); 24 | 25 | connection.onInitialize(() => ({ 26 | capabilities: { 27 | // Incremental file updates are sent. 28 | textDocumentSync: TextDocumentSyncKind.Incremental, 29 | // Tell the client that this server supports code completion. 30 | completionProvider: { 31 | triggerCharacters: 'abcdefghijklmnopqrstuvwxyz/-_"'.split(''), 32 | }, 33 | hoverProvider: true, 34 | }, 35 | })); 36 | 37 | connection.onHover( 38 | async ({ 39 | textDocument: file, 40 | position, 41 | }: HoverParams): Promise => { 42 | if (!file.uri.endsWith('composer.json')) return; 43 | 44 | const document = documents.get(file.uri); 45 | if (!document) return; 46 | 47 | const context = Context.fromLSP(document, position, connection); 48 | const result = await runHover(context); 49 | if (!result) return; 50 | 51 | return { 52 | contents: result, 53 | }; 54 | } 55 | ); 56 | 57 | connection.onCompletion( 58 | async ({ 59 | textDocument: file, 60 | position, 61 | }: TextDocumentPositionParams): Promise => { 62 | if (!file.uri.endsWith('composer.json')) return; 63 | 64 | const document = documents.get(file.uri); 65 | if (!document) return; 66 | 67 | const context = Context.fromLSP(document, position, connection); 68 | const results = await runCompletion(context); 69 | 70 | return results.map((result) => ({ 71 | label: result.name, 72 | kind: CompletionItemKind.Module, 73 | documentation: result.description, 74 | })); 75 | } 76 | ); 77 | 78 | // Make the text document manager listen on the connection 79 | // for open, change and close text document events 80 | documents.listen(connection); 81 | 82 | // Listen on the connection 83 | connection.listen(); 84 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "declarationDir": "../dist", 5 | "outDir": "../dist", 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/__fixtures__/composer-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft-04/schema#", 3 | "title": "Package", 4 | "type": "object", 5 | "properties": { 6 | "name": { 7 | "type": "string", 8 | "description": "Package name, including 'vendor-name/' prefix.", 9 | "pattern": "^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$" 10 | }, 11 | "description": { 12 | "type": "string", 13 | "description": "Short package description." 14 | }, 15 | "license": { 16 | "type": ["string", "array"], 17 | "description": "License name. Or an array of license names." 18 | }, 19 | "type": { 20 | "description": "Package type, either 'library' for common packages, 'composer-plugin' for plugins, 'metapackage for empty packages, or a custom type ([a-z0-9-]+) defined by whatever project this package applies to.", 21 | "type": "string", 22 | "pattern": "^[a-z0-9-]+$" 23 | }, 24 | "abandoned": { 25 | "type": ["boolean", "string"], 26 | "description": "Indicates whether this package has been abandoned, it can be boolean or a package name/URL pointing to a recommended alternative. Defaults to false." 27 | }, 28 | "version": { 29 | "type": "string", 30 | "description": "Package version, see https://getcomposer.org/doc/04-schema.md#version for more info on valid schemes.", 31 | "pattern": "^v?\\d+(\\.\\d+){0,3}|^dev-" 32 | }, 33 | "default-branch": { 34 | "type": ["boolean"], 35 | "description": "Internal use only, do not specify this in composer.json. Indicates whether this version is the default branch of the linked VCS repository. Defaults to false." 36 | }, 37 | "non-feature-branches": { 38 | "type": ["array"], 39 | "description": "A set of string or regex patterns for non-numeric branch names that will not be handled as feature branches.", 40 | "items": { 41 | "type": "string" 42 | } 43 | }, 44 | "keywords": { 45 | "type": "array", 46 | "items": { 47 | "type": "string", 48 | "description": "A tag/keyword that this package relates to." 49 | } 50 | }, 51 | "readme": { 52 | "type": "string", 53 | "description": "Relative path to the readme document." 54 | }, 55 | "time": { 56 | "type": "string", 57 | "description": "Package release date, in 'YYYY-MM-DD', 'YYYY-MM-DD HH:MM:SS' or 'YYYY-MM-DDTHH:MM:SSZ' format." 58 | }, 59 | "authors": { 60 | "$ref": "#/definitions/authors" 61 | }, 62 | "homepage": { 63 | "type": "string", 64 | "description": "Homepage URL for the project.", 65 | "format": "uri" 66 | }, 67 | "support": { 68 | "type": "object", 69 | "properties": { 70 | "email": { 71 | "type": "string", 72 | "description": "Email address for support.", 73 | "format": "email" 74 | }, 75 | "issues": { 76 | "type": "string", 77 | "description": "URL to the issue tracker.", 78 | "format": "uri" 79 | }, 80 | "forum": { 81 | "type": "string", 82 | "description": "URL to the forum.", 83 | "format": "uri" 84 | }, 85 | "wiki": { 86 | "type": "string", 87 | "description": "URL to the wiki.", 88 | "format": "uri" 89 | }, 90 | "irc": { 91 | "type": "string", 92 | "description": "IRC channel for support, as irc://server/channel.", 93 | "format": "uri" 94 | }, 95 | "chat": { 96 | "type": "string", 97 | "description": "URL to the support chat.", 98 | "format": "uri" 99 | }, 100 | "source": { 101 | "type": "string", 102 | "description": "URL to browse or download the sources.", 103 | "format": "uri" 104 | }, 105 | "docs": { 106 | "type": "string", 107 | "description": "URL to the documentation.", 108 | "format": "uri" 109 | }, 110 | "rss": { 111 | "type": "string", 112 | "description": "URL to the RSS feed.", 113 | "format": "uri" 114 | } 115 | } 116 | }, 117 | "funding": { 118 | "type": "array", 119 | "description": "A list of options to fund the development and maintenance of the package.", 120 | "items": { 121 | "type": "object", 122 | "properties": { 123 | "type": { 124 | "type": "string", 125 | "description": "Type of funding or platform through which funding is possible." 126 | }, 127 | "url": { 128 | "type": "string", 129 | "description": "URL to a website with details on funding and a way to fund the package.", 130 | "format": "uri" 131 | } 132 | } 133 | } 134 | }, 135 | "_comment": { 136 | "type": ["array", "string"], 137 | "description": "A key to store comments in" 138 | }, 139 | "require": { 140 | "type": "object", 141 | "description": "This is an object of package name (keys) and version constraints (values) that are required to run this package.", 142 | "additionalProperties": { 143 | "type": "string" 144 | } 145 | }, 146 | "require-dev": { 147 | "type": "object", 148 | "description": "This is an object of package name (keys) and version constraints (values) that this package requires for developing it (testing tools and such).", 149 | "additionalProperties": { 150 | "type": "string" 151 | } 152 | }, 153 | "replace": { 154 | "type": "object", 155 | "description": "This is an object of package name (keys) and version constraints (values) that can be replaced by this package.", 156 | "additionalProperties": { 157 | "type": "string" 158 | } 159 | }, 160 | "conflict": { 161 | "type": "object", 162 | "description": "This is an object of package name (keys) and version constraints (values) that conflict with this package.", 163 | "additionalProperties": { 164 | "type": "string" 165 | } 166 | }, 167 | "provide": { 168 | "type": "object", 169 | "description": "This is an object of package name (keys) and version constraints (values) that this package provides in addition to this package's name.", 170 | "additionalProperties": { 171 | "type": "string" 172 | } 173 | }, 174 | "suggest": { 175 | "type": "object", 176 | "description": "This is an object of package name (keys) and descriptions (values) that this package suggests work well with it (this will be suggested to the user during installation).", 177 | "additionalProperties": { 178 | "type": "string" 179 | } 180 | }, 181 | "repositories": { 182 | "type": ["object", "array"], 183 | "description": "A set of additional repositories where packages can be found.", 184 | "additionalProperties": { 185 | "anyOf": [ 186 | { "$ref": "#/definitions/repository" }, 187 | { "type": "boolean", "enum": [false] } 188 | ] 189 | }, 190 | "items": { 191 | "anyOf": [ 192 | { "$ref": "#/definitions/repository" }, 193 | { 194 | "type": "object", 195 | "additionalProperties": { "type": "boolean", "enum": [false] }, 196 | "minProperties": 1, 197 | "maxProperties": 1 198 | } 199 | ] 200 | } 201 | }, 202 | "minimum-stability": { 203 | "type": ["string"], 204 | "description": "The minimum stability the packages must have to be install-able. Possible values are: dev, alpha, beta, RC, stable.", 205 | "enum": ["dev", "alpha", "beta", "rc", "RC", "stable"] 206 | }, 207 | "prefer-stable": { 208 | "type": ["boolean"], 209 | "description": "If set to true, stable packages will be preferred to dev packages when possible, even if the minimum-stability allows unstable packages." 210 | }, 211 | "autoload": { 212 | "$ref": "#/definitions/autoload" 213 | }, 214 | "autoload-dev": { 215 | "type": "object", 216 | "description": "Description of additional autoload rules for development purpose (eg. a test suite).", 217 | "properties": { 218 | "psr-0": { 219 | "type": "object", 220 | "description": "This is an object of namespaces (keys) and the directories they can be found into (values, can be arrays of paths) by the autoloader.", 221 | "additionalProperties": { 222 | "type": ["string", "array"], 223 | "items": { 224 | "type": "string" 225 | } 226 | } 227 | }, 228 | "psr-4": { 229 | "type": "object", 230 | "description": "This is an object of namespaces (keys) and the PSR-4 directories they can map to (values, can be arrays of paths) by the autoloader.", 231 | "additionalProperties": { 232 | "type": ["string", "array"], 233 | "items": { 234 | "type": "string" 235 | } 236 | } 237 | }, 238 | "classmap": { 239 | "type": "array", 240 | "description": "This is an array of paths that contain classes to be included in the class-map generation process." 241 | }, 242 | "files": { 243 | "type": "array", 244 | "description": "This is an array of files that are always required on every request." 245 | } 246 | } 247 | }, 248 | "target-dir": { 249 | "description": "DEPRECATED: Forces the package to be installed into the given subdirectory path. This is used for autoloading PSR-0 packages that do not contain their full path. Use forward slashes for cross-platform compatibility.", 250 | "type": "string" 251 | }, 252 | "include-path": { 253 | "type": ["array"], 254 | "description": "DEPRECATED: A list of directories which should get added to PHP's include path. This is only present to support legacy projects, and all new code should preferably use autoloading.", 255 | "items": { 256 | "type": "string" 257 | } 258 | }, 259 | "bin": { 260 | "type": ["string", "array"], 261 | "description": "A set of files, or a single file, that should be treated as binaries and symlinked into bin-dir (from config).", 262 | "items": { 263 | "type": "string" 264 | } 265 | }, 266 | "archive": { 267 | "type": ["object"], 268 | "description": "Options for creating package archives for distribution.", 269 | "properties": { 270 | "name": { 271 | "type": "string", 272 | "description": "A base name for archive." 273 | }, 274 | "exclude": { 275 | "type": "array", 276 | "description": "A list of patterns for paths to exclude or include if prefixed with an exclamation mark." 277 | } 278 | } 279 | }, 280 | "config": { 281 | "type": "object", 282 | "description": "Composer options.", 283 | "properties": { 284 | "platform": { 285 | "type": "object", 286 | "description": "This is an object of package name (keys) and version (values) that will be used to mock the platform packages on this machine.", 287 | "additionalProperties": { 288 | "type": ["string", "boolean"] 289 | } 290 | }, 291 | "allow-plugins": { 292 | "type": ["object", "boolean"], 293 | "description": "This is an object of {\"pattern\": true|false} with packages which are allowed to be loaded as plugins, or true to allow all, false to allow none. Defaults to {} which prompts when an unknown plugin is added.", 294 | "additionalProperties": { 295 | "type": ["boolean"] 296 | } 297 | }, 298 | "process-timeout": { 299 | "type": "integer", 300 | "description": "The timeout in seconds for process executions, defaults to 300 (5mins)." 301 | }, 302 | "use-include-path": { 303 | "type": "boolean", 304 | "description": "If true, the Composer autoloader will also look for classes in the PHP include path." 305 | }, 306 | "use-parent-dir": { 307 | "type": ["string", "boolean"], 308 | "description": "When running Composer in a directory where there is no composer.json, if there is one present in a directory above Composer will by default ask you whether you want to use that directory's composer.json instead. One of: true (always use parent if needed), false (never ask or use it) or \"prompt\" (ask every time), defaults to prompt." 309 | }, 310 | "preferred-install": { 311 | "type": ["string", "object"], 312 | "description": "The install method Composer will prefer to use, defaults to auto and can be any of source, dist, auto, or an object of {\"pattern\": \"preference\"}.", 313 | "additionalProperties": { 314 | "type": ["string"] 315 | } 316 | }, 317 | "notify-on-install": { 318 | "type": "boolean", 319 | "description": "Composer allows repositories to define a notification URL, so that they get notified whenever a package from that repository is installed. This option allows you to disable that behaviour, defaults to true." 320 | }, 321 | "github-protocols": { 322 | "type": "array", 323 | "description": "A list of protocols to use for github.com clones, in priority order, defaults to [\"git\", \"https\", \"http\"].", 324 | "items": { 325 | "type": "string" 326 | } 327 | }, 328 | "github-oauth": { 329 | "type": "object", 330 | "description": "An object of domain name => github API oauth tokens, typically {\"github.com\":\"\"}.", 331 | "additionalProperties": { 332 | "type": "string" 333 | } 334 | }, 335 | "gitlab-oauth": { 336 | "type": "object", 337 | "description": "An object of domain name => gitlab API oauth tokens, typically {\"gitlab.com\":\"\"}.", 338 | "additionalProperties": { 339 | "type": "string" 340 | } 341 | }, 342 | "gitlab-token": { 343 | "type": "object", 344 | "description": "An object of domain name => gitlab private tokens, typically {\"gitlab.com\":\"\"}.", 345 | "additionalProperties": { 346 | "type": "string" 347 | } 348 | }, 349 | "gitlab-protocol": { 350 | "enum": ["git", "http", "https"], 351 | "description": "A protocol to force use of when creating a repository URL for the `source` value of the package metadata. One of `git` or `http`. By default, Composer will generate a git URL for private repositories and http one for public repos." 352 | }, 353 | "bearer": { 354 | "type": "object", 355 | "description": "An object of domain name => bearer authentication token, for example {\"example.com\":\"\"}.", 356 | "additionalProperties": { 357 | "type": "string" 358 | } 359 | }, 360 | "disable-tls": { 361 | "type": "boolean", 362 | "description": "Defaults to `false`. If set to true all HTTPS URLs will be tried with HTTP instead and no network level encryption is performed. Enabling this is a security risk and is NOT recommended. The better way is to enable the php_openssl extension in php.ini." 363 | }, 364 | "secure-http": { 365 | "type": "boolean", 366 | "description": "Defaults to `true`. If set to true only HTTPS URLs are allowed to be downloaded via Composer. If you really absolutely need HTTP access to something then you can disable it, but using \"Let's Encrypt\" to get a free SSL certificate is generally a better alternative." 367 | }, 368 | "secure-svn-domains": { 369 | "type": "array", 370 | "description": "A list of domains which should be trusted/marked as using a secure Subversion/SVN transport. By default svn:// protocol is seen as insecure and will throw. This is a better/safer alternative to disabling `secure-http` altogether.", 371 | "items": { 372 | "type": "string" 373 | } 374 | }, 375 | "cafile": { 376 | "type": "string", 377 | "description": "A way to set the path to the openssl CA file. In PHP 5.6+ you should rather set this via openssl.cafile in php.ini, although PHP 5.6+ should be able to detect your system CA file automatically." 378 | }, 379 | "capath": { 380 | "type": "string", 381 | "description": "If cafile is not specified or if the certificate is not found there, the directory pointed to by capath is searched for a suitable certificate. capath must be a correctly hashed certificate directory." 382 | }, 383 | "http-basic": { 384 | "type": "object", 385 | "description": "An object of domain name => {\"username\": \"...\", \"password\": \"...\"}.", 386 | "additionalProperties": { 387 | "type": "object", 388 | "required": ["username", "password"], 389 | "properties": { 390 | "username": { 391 | "type": "string", 392 | "description": "The username used for HTTP Basic authentication" 393 | }, 394 | "password": { 395 | "type": "string", 396 | "description": "The password used for HTTP Basic authentication" 397 | } 398 | } 399 | } 400 | }, 401 | "store-auths": { 402 | "type": ["string", "boolean"], 403 | "description": "What to do after prompting for authentication, one of: true (store), false (do not store) or \"prompt\" (ask every time), defaults to prompt." 404 | }, 405 | "vendor-dir": { 406 | "type": "string", 407 | "description": "The location where all packages are installed, defaults to \"vendor\"." 408 | }, 409 | "bin-dir": { 410 | "type": "string", 411 | "description": "The location where all binaries are linked, defaults to \"vendor/bin\"." 412 | }, 413 | "data-dir": { 414 | "type": "string", 415 | "description": "The location where old phar files are stored, defaults to \"$home\" except on XDG Base Directory compliant unixes." 416 | }, 417 | "cache-dir": { 418 | "type": "string", 419 | "description": "The location where all caches are located, defaults to \"~/.composer/cache\" on *nix and \"%LOCALAPPDATA%\\Composer\" on windows." 420 | }, 421 | "cache-files-dir": { 422 | "type": "string", 423 | "description": "The location where files (zip downloads) are cached, defaults to \"{$cache-dir}/files\"." 424 | }, 425 | "cache-repo-dir": { 426 | "type": "string", 427 | "description": "The location where repo (git/hg repo clones) are cached, defaults to \"{$cache-dir}/repo\"." 428 | }, 429 | "cache-vcs-dir": { 430 | "type": "string", 431 | "description": "The location where vcs infos (git clones, github api calls, etc. when reading vcs repos) are cached, defaults to \"{$cache-dir}/vcs\"." 432 | }, 433 | "cache-ttl": { 434 | "type": "integer", 435 | "description": "The default cache time-to-live, defaults to 15552000 (6 months)." 436 | }, 437 | "cache-files-ttl": { 438 | "type": "integer", 439 | "description": "The cache time-to-live for files, defaults to the value of cache-ttl." 440 | }, 441 | "cache-files-maxsize": { 442 | "type": ["string", "integer"], 443 | "description": "The cache max size for the files cache, defaults to \"300MiB\"." 444 | }, 445 | "cache-read-only": { 446 | "type": ["boolean"], 447 | "description": "Whether to use the Composer cache in read-only mode." 448 | }, 449 | "bin-compat": { 450 | "enum": ["auto", "full", "proxy", "symlink"], 451 | "description": "The compatibility of the binaries, defaults to \"auto\" (automatically guessed), can be \"full\" (compatible with both Windows and Unix-based systems) and \"proxy\" (only bash-style proxy)." 452 | }, 453 | "discard-changes": { 454 | "type": ["string", "boolean"], 455 | "description": "The default style of handling dirty updates, defaults to false and can be any of true, false or \"stash\"." 456 | }, 457 | "autoloader-suffix": { 458 | "type": "string", 459 | "description": "Optional string to be used as a suffix for the generated Composer autoloader. When null a random one will be generated." 460 | }, 461 | "optimize-autoloader": { 462 | "type": "boolean", 463 | "description": "Always optimize when dumping the autoloader." 464 | }, 465 | "prepend-autoloader": { 466 | "type": "boolean", 467 | "description": "If false, the composer autoloader will not be prepended to existing autoloaders, defaults to true." 468 | }, 469 | "classmap-authoritative": { 470 | "type": "boolean", 471 | "description": "If true, the composer autoloader will not scan the filesystem for classes that are not found in the class map, defaults to false." 472 | }, 473 | "apcu-autoloader": { 474 | "type": "boolean", 475 | "description": "If true, the Composer autoloader will check for APCu and use it to cache found/not-found classes when the extension is enabled, defaults to false." 476 | }, 477 | "github-domains": { 478 | "type": "array", 479 | "description": "A list of domains to use in github mode. This is used for GitHub Enterprise setups, defaults to [\"github.com\"].", 480 | "items": { 481 | "type": "string" 482 | } 483 | }, 484 | "github-expose-hostname": { 485 | "type": "boolean", 486 | "description": "Defaults to true. If set to false, the OAuth tokens created to access the github API will have a date instead of the machine hostname." 487 | }, 488 | "gitlab-domains": { 489 | "type": "array", 490 | "description": "A list of domains to use in gitlab mode. This is used for custom GitLab setups, defaults to [\"gitlab.com\"].", 491 | "items": { 492 | "type": "string" 493 | } 494 | }, 495 | "use-github-api": { 496 | "type": "boolean", 497 | "description": "Defaults to true. If set to false, globally disables the use of the GitHub API for all GitHub repositories and clones the repository as it would for any other repository." 498 | }, 499 | "archive-format": { 500 | "type": "string", 501 | "description": "The default archiving format when not provided on cli, defaults to \"tar\"." 502 | }, 503 | "archive-dir": { 504 | "type": "string", 505 | "description": "The default archive path when not provided on cli, defaults to \".\"." 506 | }, 507 | "htaccess-protect": { 508 | "type": "boolean", 509 | "description": "Defaults to true. If set to false, Composer will not create .htaccess files in the composer home, cache, and data directories." 510 | }, 511 | "sort-packages": { 512 | "type": "boolean", 513 | "description": "Defaults to false. If set to true, Composer will sort packages when adding/updating a new dependency." 514 | }, 515 | "lock": { 516 | "type": "boolean", 517 | "description": "Defaults to true. If set to false, Composer will not create a composer.lock file." 518 | }, 519 | "platform-check": { 520 | "type": ["boolean", "string"], 521 | "description": "Defaults to \"php-only\" which checks only the PHP version. Setting to true will also check the presence of required PHP extensions. If set to false, Composer will not create and require a platform_check.php file as part of the autoloader bootstrap." 522 | } 523 | } 524 | }, 525 | "extra": { 526 | "type": ["object", "array"], 527 | "description": "Arbitrary extra data that can be used by plugins, for example, package of type composer-plugin may have a 'class' key defining an installer class name.", 528 | "additionalProperties": true 529 | }, 530 | "scripts": { 531 | "type": ["object"], 532 | "description": "Script listeners that will be executed before/after some events.", 533 | "properties": { 534 | "pre-install-cmd": { 535 | "type": ["array", "string"], 536 | "description": "Occurs before the install command is executed, contains one or more Class::method callables or shell commands." 537 | }, 538 | "post-install-cmd": { 539 | "type": ["array", "string"], 540 | "description": "Occurs after the install command is executed, contains one or more Class::method callables or shell commands." 541 | }, 542 | "pre-update-cmd": { 543 | "type": ["array", "string"], 544 | "description": "Occurs before the update command is executed, contains one or more Class::method callables or shell commands." 545 | }, 546 | "post-update-cmd": { 547 | "type": ["array", "string"], 548 | "description": "Occurs after the update command is executed, contains one or more Class::method callables or shell commands." 549 | }, 550 | "pre-status-cmd": { 551 | "type": ["array", "string"], 552 | "description": "Occurs before the status command is executed, contains one or more Class::method callables or shell commands." 553 | }, 554 | "post-status-cmd": { 555 | "type": ["array", "string"], 556 | "description": "Occurs after the status command is executed, contains one or more Class::method callables or shell commands." 557 | }, 558 | "pre-package-install": { 559 | "type": ["array", "string"], 560 | "description": "Occurs before a package is installed, contains one or more Class::method callables or shell commands." 561 | }, 562 | "post-package-install": { 563 | "type": ["array", "string"], 564 | "description": "Occurs after a package is installed, contains one or more Class::method callables or shell commands." 565 | }, 566 | "pre-package-update": { 567 | "type": ["array", "string"], 568 | "description": "Occurs before a package is updated, contains one or more Class::method callables or shell commands." 569 | }, 570 | "post-package-update": { 571 | "type": ["array", "string"], 572 | "description": "Occurs after a package is updated, contains one or more Class::method callables or shell commands." 573 | }, 574 | "pre-package-uninstall": { 575 | "type": ["array", "string"], 576 | "description": "Occurs before a package has been uninstalled, contains one or more Class::method callables or shell commands." 577 | }, 578 | "post-package-uninstall": { 579 | "type": ["array", "string"], 580 | "description": "Occurs after a package has been uninstalled, contains one or more Class::method callables or shell commands." 581 | }, 582 | "pre-autoload-dump": { 583 | "type": ["array", "string"], 584 | "description": "Occurs before the autoloader is dumped, contains one or more Class::method callables or shell commands." 585 | }, 586 | "post-autoload-dump": { 587 | "type": ["array", "string"], 588 | "description": "Occurs after the autoloader is dumped, contains one or more Class::method callables or shell commands." 589 | }, 590 | "post-root-package-install": { 591 | "type": ["array", "string"], 592 | "description": "Occurs after the root-package is installed, contains one or more Class::method callables or shell commands." 593 | }, 594 | "post-create-project-cmd": { 595 | "type": ["array", "string"], 596 | "description": "Occurs after the create-project command is executed, contains one or more Class::method callables or shell commands." 597 | } 598 | } 599 | }, 600 | "scripts-descriptions": { 601 | "type": ["object"], 602 | "description": "Descriptions for custom commands, shown in console help.", 603 | "additionalProperties": { 604 | "type": "string" 605 | } 606 | } 607 | }, 608 | "definitions": { 609 | "authors": { 610 | "type": "array", 611 | "description": "List of authors that contributed to the package. This is typically the main maintainers, not the full list.", 612 | "items": { 613 | "type": "object", 614 | "additionalProperties": false, 615 | "required": [ "name"], 616 | "properties": { 617 | "name": { 618 | "type": "string", 619 | "description": "Full name of the author." 620 | }, 621 | "email": { 622 | "type": "string", 623 | "description": "Email address of the author.", 624 | "format": "email" 625 | }, 626 | "homepage": { 627 | "type": "string", 628 | "description": "Homepage URL for the author.", 629 | "format": "uri" 630 | }, 631 | "role": { 632 | "type": "string", 633 | "description": "Author's role in the project." 634 | } 635 | } 636 | } 637 | }, 638 | "autoload": { 639 | "type": "object", 640 | "description": "Description of how the package can be autoloaded.", 641 | "properties": { 642 | "psr-0": { 643 | "type": "object", 644 | "description": "This is an object of namespaces (keys) and the directories they can be found in (values, can be arrays of paths) by the autoloader.", 645 | "additionalProperties": { 646 | "type": ["string", "array"], 647 | "items": { 648 | "type": "string" 649 | } 650 | } 651 | }, 652 | "psr-4": { 653 | "type": "object", 654 | "description": "This is an object of namespaces (keys) and the PSR-4 directories they can map to (values, can be arrays of paths) by the autoloader.", 655 | "additionalProperties": { 656 | "type": ["string", "array"], 657 | "items": { 658 | "type": "string" 659 | } 660 | } 661 | }, 662 | "classmap": { 663 | "type": "array", 664 | "description": "This is an array of paths that contain classes to be included in the class-map generation process." 665 | }, 666 | "files": { 667 | "type": "array", 668 | "description": "This is an array of files that are always required on every request." 669 | }, 670 | "exclude-from-classmap": { 671 | "type": "array", 672 | "description": "This is an array of patterns to exclude from autoload classmap generation. (e.g. \"exclude-from-classmap\": [\"/test/\", \"/tests/\", \"/Tests/\"]" 673 | } 674 | } 675 | }, 676 | "repository": { 677 | "type": "object", 678 | "anyOf": [ 679 | { "$ref": "#/definitions/composer-repository" }, 680 | { "$ref": "#/definitions/vcs-repository" }, 681 | { "$ref": "#/definitions/path-repository" }, 682 | { "$ref": "#/definitions/artifact-repository" }, 683 | { "$ref": "#/definitions/pear-repository" }, 684 | { "$ref": "#/definitions/package-repository" } 685 | ] 686 | }, 687 | "composer-repository": { 688 | "type": "object", 689 | "required": ["type", "url"], 690 | "properties": { 691 | "type": { "type": "string", "enum": ["composer"] }, 692 | "url": { "type": "string" }, 693 | "canonical": { "type": "boolean" }, 694 | "only": { 695 | "type": "array", 696 | "items": { 697 | "type": "string" 698 | } 699 | }, 700 | "exclude": { 701 | "type": "array", 702 | "items": { 703 | "type": "string" 704 | } 705 | }, 706 | "options": { 707 | "type": "object", 708 | "additionalProperties": true 709 | }, 710 | "allow_ssl_downgrade": { "type": "boolean" }, 711 | "force-lazy-providers": { "type": "boolean" } 712 | } 713 | }, 714 | "vcs-repository": { 715 | "type": "object", 716 | "required": ["type", "url"], 717 | "properties": { 718 | "type": { "type": "string", "enum": ["vcs", "github", "git", "gitlab", "bitbucket", "git-bitbucket", "hg", "fossil", "perforce", "svn"] }, 719 | "url": { "type": "string" }, 720 | "canonical": { "type": "boolean" }, 721 | "only": { 722 | "type": "array", 723 | "items": { 724 | "type": "string" 725 | } 726 | }, 727 | "exclude": { 728 | "type": "array", 729 | "items": { 730 | "type": "string" 731 | } 732 | }, 733 | "no-api": { "type": "boolean" }, 734 | "secure-http": { "type": "boolean" }, 735 | "svn-cache-credentials": { "type": "boolean" }, 736 | "trunk-path": { "type": ["string", "boolean"] }, 737 | "branches-path": { "type": ["string", "boolean"] }, 738 | "tags-path": { "type": ["string", "boolean"] }, 739 | "package-path": { "type": "string" }, 740 | "depot": { "type": "string" }, 741 | "branch": { "type": "string" }, 742 | "unique_perforce_client_name": { "type": "string" }, 743 | "p4user": { "type": "string" }, 744 | "p4password": { "type": "string" } 745 | } 746 | }, 747 | "path-repository": { 748 | "type": "object", 749 | "required": ["type", "url"], 750 | "properties": { 751 | "type": { "type": "string", "enum": ["path"] }, 752 | "url": { "type": "string" }, 753 | "canonical": { "type": "boolean" }, 754 | "only": { 755 | "type": "array", 756 | "items": { 757 | "type": "string" 758 | } 759 | }, 760 | "exclude": { 761 | "type": "array", 762 | "items": { 763 | "type": "string" 764 | } 765 | }, 766 | "options": { 767 | "type": "object", 768 | "properties": { 769 | "symlink": { "type": ["boolean", "null"] } 770 | }, 771 | "additionalProperties": true 772 | } 773 | } 774 | }, 775 | "artifact-repository": { 776 | "type": "object", 777 | "required": ["type", "url"], 778 | "properties": { 779 | "type": { "type": "string", "enum": ["artifact"] }, 780 | "url": { "type": "string" }, 781 | "canonical": { "type": "boolean" }, 782 | "only": { 783 | "type": "array", 784 | "items": { 785 | "type": "string" 786 | } 787 | }, 788 | "exclude": { 789 | "type": "array", 790 | "items": { 791 | "type": "string" 792 | } 793 | } 794 | } 795 | }, 796 | "pear-repository": { 797 | "type": "object", 798 | "required": ["type", "url"], 799 | "properties": { 800 | "type": { "type": "string", "enum": ["pear"] }, 801 | "url": { "type": "string" }, 802 | "canonical": { "type": "boolean" }, 803 | "only": { 804 | "type": "array", 805 | "items": { 806 | "type": "string" 807 | } 808 | }, 809 | "exclude": { 810 | "type": "array", 811 | "items": { 812 | "type": "string" 813 | } 814 | }, 815 | "vendor-alias": { "type": "string" } 816 | } 817 | }, 818 | "package-repository": { 819 | "type": "object", 820 | "required": ["type", "package"], 821 | "properties": { 822 | "type": { "type": "string", "enum": ["package"] }, 823 | "canonical": { "type": "boolean" }, 824 | "only": { 825 | "type": "array", 826 | "items": { 827 | "type": "string" 828 | } 829 | }, 830 | "exclude": { 831 | "type": "array", 832 | "items": { 833 | "type": "string" 834 | } 835 | }, 836 | "package": { 837 | "oneOf": [ 838 | { "$ref": "#/definitions/inline-package" }, 839 | { 840 | "type": "array", 841 | "items": { "$ref": "#/definitions/inline-package" } 842 | } 843 | ] 844 | } 845 | } 846 | }, 847 | "inline-package": { 848 | "type": "object", 849 | "required": ["name", "version"], 850 | "properties": { 851 | "name": { 852 | "type": "string", 853 | "description": "Package name, including 'vendor-name/' prefix." 854 | }, 855 | "type": { 856 | "type": "string" 857 | }, 858 | "target-dir": { 859 | "description": "DEPRECATED: Forces the package to be installed into the given subdirectory path. This is used for autoloading PSR-0 packages that do not contain their full path. Use forward slashes for cross-platform compatibility.", 860 | "type": "string" 861 | }, 862 | "description": { 863 | "type": "string" 864 | }, 865 | "keywords": { 866 | "type": "array", 867 | "items": { 868 | "type": "string" 869 | } 870 | }, 871 | "homepage": { 872 | "type": "string", 873 | "format": "uri" 874 | }, 875 | "version": { 876 | "type": "string" 877 | }, 878 | "time": { 879 | "type": "string" 880 | }, 881 | "license": { 882 | "type": [ 883 | "string", 884 | "array" 885 | ] 886 | }, 887 | "authors": { 888 | "$ref": "#/definitions/authors" 889 | }, 890 | "require": { 891 | "type": "object", 892 | "additionalProperties": { 893 | "type": "string" 894 | } 895 | }, 896 | "replace": { 897 | "type": "object", 898 | "additionalProperties": { 899 | "type": "string" 900 | } 901 | }, 902 | "conflict": { 903 | "type": "object", 904 | "additionalProperties": { 905 | "type": "string" 906 | } 907 | }, 908 | "provide": { 909 | "type": "object", 910 | "additionalProperties": { 911 | "type": "string" 912 | } 913 | }, 914 | "require-dev": { 915 | "type": "object", 916 | "additionalProperties": { 917 | "type": "string" 918 | } 919 | }, 920 | "suggest": { 921 | "type": "object", 922 | "additionalProperties": { 923 | "type": "string" 924 | } 925 | }, 926 | "extra": { 927 | "type": ["object", "array"], 928 | "additionalProperties": true 929 | }, 930 | "autoload": { 931 | "$ref": "#/definitions/autoload" 932 | }, 933 | "archive": { 934 | "type": ["object"], 935 | "properties": { 936 | "exclude": { 937 | "type": "array" 938 | } 939 | } 940 | }, 941 | "bin": { 942 | "type": ["string", "array"], 943 | "description": "A set of files, or a single file, that should be treated as binaries and symlinked into bin-dir (from config).", 944 | "items": { 945 | "type": "string" 946 | } 947 | }, 948 | "include-path": { 949 | "type": ["array"], 950 | "description": "DEPRECATED: A list of directories which should get added to PHP's include path. This is only present to support legacy projects, and all new code should preferably use autoloading.", 951 | "items": { 952 | "type": "string" 953 | } 954 | }, 955 | "source": { 956 | "type": "object", 957 | "required": ["type", "url", "reference"], 958 | "properties": { 959 | "type": { 960 | "type": "string" 961 | }, 962 | "url": { 963 | "type": "string" 964 | }, 965 | "reference": { 966 | "type": "string" 967 | }, 968 | "mirrors": { 969 | "type": "array" 970 | } 971 | } 972 | }, 973 | "dist": { 974 | "type": "object", 975 | "required": ["type", "url"], 976 | "properties": { 977 | "type": { 978 | "type": "string" 979 | }, 980 | "url": { 981 | "type": "string" 982 | }, 983 | "reference": { 984 | "type": "string" 985 | }, 986 | "shasum": { 987 | "type": "string" 988 | }, 989 | "mirrors": { 990 | "type": "array" 991 | } 992 | } 993 | } 994 | }, 995 | "additionalProperties": true 996 | } 997 | } 998 | } 999 | -------------------------------------------------------------------------------- /tests/__fixtures__/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "type": "project", 4 | "description": "Description", 5 | "license": "MIT", 6 | "require-dev": { 7 | "phpunit/phpunit": "^9.5.10", 8 | }, 9 | "minimum-stability": "dev", 10 | "prefer-stable": true 11 | } 12 | -------------------------------------------------------------------------------- /tests/__fixtures__/in-progress.composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "type": "project", 4 | "description": "Description", 5 | "license": "MIT", 6 | "require": { 7 | "php": "^7.3|^8.0", 8 | "phps 9 | }, 10 | "require-dev": { 11 | "phpunit/phpunit": "^9.5.10" 12 | }, 13 | "minimum-stability": "dev", 14 | "prefer-stable": true 15 | } 16 | -------------------------------------------------------------------------------- /tests/composer.test.ts: -------------------------------------------------------------------------------- 1 | import { fetchDetails } from '../src/composer-cli'; 2 | 3 | describe('FetchDetails', () => { 4 | it('returns info for a known package', async () => { 5 | const details = await fetchDetails('phpstan/phpstan'); 6 | expect(details).toContain('[git] https://github.com/phpstan/phpstan.git'); 7 | }); 8 | 9 | it('returns undefined for an unknown package', async () => { 10 | const details = await fetchDetails('laytan/unknown-package'); 11 | expect(details).toBe(undefined); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs/promises'; 2 | 3 | export const fixturesPath = __dirname + '/__fixtures__'; 4 | 5 | export async function getFixture(fixture: string): Promise { 6 | return (await readFile(`${fixturesPath}/${fixture}`)).toString(); 7 | } 8 | -------------------------------------------------------------------------------- /tests/parsing.test.ts: -------------------------------------------------------------------------------- 1 | import { getLocation } from 'jsonc-parser'; 2 | import { getFixture } from './helpers'; 3 | import { getPossibleProperties } from '../src/composer-schema'; 4 | import { JSONSchema4 } from 'json-schema'; 5 | 6 | it('parses partly invalid JSON', async () => { 7 | const json = await getFixture('in-progress.composer.json'); 8 | const location = getLocation(json, json.indexOf('"phps')); 9 | expect(location.path).toEqual(['require', 'phps']); 10 | }); 11 | 12 | describe('composer JSON schema', () => { 13 | describe('getPossibleProperties', () => { 14 | let schema: JSONSchema4; 15 | beforeAll(async () => { 16 | schema = JSON.parse( 17 | await getFixture('composer-schema.json') 18 | ) as JSONSchema4; 19 | }); 20 | 21 | it('returns top level properties', () => { 22 | const path = ['lol']; 23 | 24 | const properties = getPossibleProperties(schema, path); 25 | 26 | expect(properties).toBe(schema.properties); 27 | }); 28 | 29 | it('parses a simple path', () => { 30 | const path = ['support', 'lol']; 31 | 32 | const properties = getPossibleProperties(schema, path); 33 | 34 | expect(properties).toEqual(schema?.properties?.support?.properties); 35 | }); 36 | 37 | it("follows $ref's", () => { 38 | const path = ['autoload', 'lol']; 39 | 40 | const properties = getPossibleProperties(schema, path); 41 | 42 | expect(properties).toEqual(schema?.definitions?.autoload?.properties); 43 | }); 44 | 45 | it('handles arrays of objects', () => { 46 | const path = ['funding', 0, 'lol']; 47 | 48 | const properties = getPossibleProperties(schema, path); 49 | 50 | expect(properties).toEqual( 51 | (schema?.properties?.funding?.items as JSONSchema4)?.properties 52 | ); 53 | }); 54 | 55 | it("handles arrays of objects with $ref's", () => { 56 | const path = ['authors', 0, 'lol']; 57 | 58 | const properties = getPossibleProperties(schema, path); 59 | 60 | expect(properties).toEqual( 61 | (schema?.definitions?.authors?.items as JSONSchema4)?.properties 62 | ); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "declarationMap": true, 7 | "esModuleInterop": true, 8 | "lib": ["es6"], 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "sourceMap": true, 13 | "strict": true, 14 | "target": "es6" 15 | } 16 | } 17 | --------------------------------------------------------------------------------