Skip to content

Commit 4a2fa50

Browse files
authored
Merge pull request #1542 from andrewnicols/composerDev
[docs] Added initial docs for Composer-installed plugin guidance
2 parents e4a885c + 0190318 commit 4a2fa50

File tree

8 files changed

+269
-12
lines changed

8 files changed

+269
-12
lines changed

docs/_utils.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
import React, { type ReactNode } from 'react';
1818
import ComponentFileSummaryGeneric, {
19+
type ComponentFileSummaryDescription,
1920
type ComponentFileSummaryProps,
2021
} from '@site/src/components/ComponentFileSummary';
2122
import { MDXProvider } from '@mdx-js/react';
@@ -38,7 +39,7 @@ export const fillDefaultProps = (props: ComponentFileSummaryProps): ComponentFil
3839
...props,
3940
});
4041

41-
const normaliseDescription = (Value: ReactNode | string): null | JSX.Element => {
42+
const normaliseDescription = (Value: ComponentFileSummaryDescription): null | React.JSX.Element => {
4243
if (typeof Value === 'boolean' || !Value) {
4344
return null;
4445
}
@@ -70,7 +71,7 @@ export const getDescription = ({
7071
description = null,
7172
extraDescription = null,
7273
children = null,
73-
}: ComponentFileSummaryProps, defaultDescription?: ReactNode | string): null | ReactNode | JSX.Element => {
74+
}: ComponentFileSummaryProps, defaultDescription?: ComponentFileSummaryDescription): null | ReactNode | JSX.Element => {
7475
if (children) {
7576
const Description = normaliseDescription(children);
7677
return (

docs/apis/_files/composer-json.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!-- markdownlint-disable first-line-heading -->
2+
The `composer.json` file allows your plugin to be distributed and installed via [Composer](https://getcomposer.org/).
3+
4+
Moodle uses the [moodle/composer-installer](https://github.com/moodle/composer-installer) package to install Moodle plugins from Composer into the correct locations within the Moodle directory structure.
5+
6+
The most important fields for Moodle plugin compatibility are:
7+
8+
- `type`: the Moodle plugin type in the format `moodle-[plugintype]` (for example, `moodle-block`)
9+
- `name`: the Composer package name, where the package component follows the format `moodle-[plugintype]_[pluginname]` (for example, `myplugin/moodle-block_myblock`)
10+
- `require.moodle/moodle`: the supported Moodle version range for your plugin
11+
- `require.moodle/composer-installer`: a production dependency so Moodle package types are installed into the correct locations
12+
13+
You can also declare dependencies on other Moodle plugins in `require`.
14+
15+
Composer runtime dependencies are only guaranteed when the plugin is installed via Composer. For now, avoid introducing Composer-only runtime dependencies that would break non-Composer plugin installs.
16+
17+
See the [Composer guide](../../guides/composer/index.md) for further information.

docs/apis/_files/composer-json.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Copyright (c) Moodle Pty Ltd.
3+
*
4+
* Moodle is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* Moodle is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
import React from 'react';
18+
import { ComponentFileSummary, type ComponentFileSummaryProps } from '../../_utils';
19+
import DefaultDescription from './composer-json.mdx';
20+
21+
const defaultExample = `{
22+
"name": "myplugin/moodle-block_myblock",
23+
"description": "A description of my Moodle block plugin",
24+
"type": "moodle-block",
25+
"require": {
26+
"moodle/moodle": "^5.2",
27+
"moodle/composer-installer": "*",
28+
"abgreeve/moodle-block_stash": "^5.2"
29+
},
30+
"license": "GPL-3.0-or-later"
31+
}`;
32+
33+
export default function ComposerJSON(props: ComponentFileSummaryProps): React.JSX.Element {
34+
return (
35+
<ComponentFileSummary
36+
filepath="/composer.json"
37+
filetype="json"
38+
summary="Allows the plugin to be distributed and installed via Composer"
39+
examplePurpose="Basic Composer package definition for a block plugin"
40+
defaultDescription={DefaultDescription}
41+
defaultExample={defaultExample}
42+
showLicense={false}
43+
showFileHeader={false}
44+
{...props}
45+
/>
46+
);
47+
}

docs/apis/_files/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import AmdDir from './amd-dir';
1919
import BackupDir from './backup-dir';
20+
import ComposerJSON from './composer-json';
2021
import CLIDir from './cli-dir';
2122
import Changes from './changes';
2223
import ClassesDir from './classes-dir';
@@ -49,6 +50,7 @@ import YUIDir from './yui-dir';
4950
export {
5051
AmdDir,
5152
BackupDir,
53+
ComposerJSON,
5254
CLIDir,
5355
Changes,
5456
ClassesDir,

docs/apis/commonfiles/index.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ tags:
99
import {
1010
AmdDir,
1111
BackupDir,
12+
ComposerJSON,
1213
CLIDir,
1314
Changes,
1415
ClassesDir,
@@ -150,6 +151,10 @@ Files inside the `db/` folder (such as `install.xml`, `upgrade.php`, `access.php
150151

151152
<ThirdpartylibsXML />
152153

154+
### composer.json
155+
156+
<ComposerJSON />
157+
153158
### readme_moodle.txt
154159

155160
<ReadmeMoodleTXT />

docs/guides.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Learn about key Moodle features for developers through our Developer Guides. The
1111

1212
- Introduction to [JavaScript](./guides/javascript/index.md) in Moodle
1313
- Learn about how Moodle uses [Templates](./guides/templates/index.md) to render content
14+
- Distribute and install Moodle plugins using [Composer](./guides/composer/index.md)

docs/guides/composer/index.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
---
2+
title: Composer support for plugins
3+
tags:
4+
- Composer
5+
- Plugins
6+
- Package management
7+
description: How to make your Moodle plugin available via Composer, and developer tips for working with Composer-based Moodle sites.
8+
---
9+
10+
<Since version="5.2" issueNumber="MDL-87473" />
11+
12+
Moodle 5.2 introduced native support for distributing and installing Moodle plugins using [Composer](https://getcomposer.org/), the PHP package manager. Plugin developers can publish their plugins to [Packagist](https://packagist.org/) and site administrators can install them with a simple `composer require` command.
13+
14+
The [moodle/composer-installer](https://github.com/moodle/composer-installer) Composer plugin handles placing each Moodle plugin package into the correct location within the Moodle directory structure, based on the plugin type declared in the package's `composer.json`.
15+
16+
## Making your plugin available via Composer
17+
18+
### Adding a composer.json
19+
20+
Add a `composer.json` file to the root of your plugin directory with at minimum the following fields:
21+
22+
- `name`: the Composer package name, where the package component follows the format `moodle-[plugintype]_[pluginname]` (for example, `myplugin/moodle-block_myblock`)
23+
- `type`: the Moodle plugin type in the format `moodle-[plugintype]` (for example, `moodle-block`)
24+
- `require`: the Moodle version requirement, to communicate which versions your plugin supports
25+
26+
In `require`, include a production dependency on `moodle/composer-installer`.
27+
28+
```json title="composer.json"
29+
{
30+
"name": "myplugin/moodle-block_myblock",
31+
"description": "A description of my Moodle block plugin",
32+
"type": "moodle-block",
33+
"require": {
34+
"moodle/moodle": "^5.2",
35+
"moodle/composer-installer": "*"
36+
},
37+
"license": "GPL-3.0-or-later"
38+
}
39+
```
40+
41+
The `type` field is what the `moodle/composer-installer` package uses to determine the installation path. For example, a package with `"type": "moodle-block"` will be installed into `blocks/myblock/` within the Moodle directory.
42+
43+
:::tip Vendor name
44+
45+
The vendor prefix in the `name` field (for example, `myplugin`) is your Packagist vendor name and is independent of any Moodle conventions. The package name component (`moodle-block_myblock`) follows the Moodle convention.
46+
47+
:::
48+
49+
### Publishing to Packagist
50+
51+
Once your plugin has a valid `composer.json` and is in a public Git repository, you can publish it to [Packagist](https://packagist.org/):
52+
53+
1. Visit [https://packagist.org/packages/submit](https://packagist.org/packages/submit) and submit your repository URL.
54+
2. Set up a [GitHub webhook](https://packagist.org/about#how-to-update-packages) to keep Packagist in sync whenever you push to your repository.
55+
56+
Once published, site administrators can install your plugin with:
57+
58+
```bash
59+
composer require myplugin/moodle-block_myblock
60+
```
61+
62+
The installer will automatically place the plugin into `blocks/myblock/` within their Moodle directory.
63+
64+
## Development tips
65+
66+
### Creating a development Moodle site
67+
68+
The [moodle/seed](https://github.com/moodle/seed) project provides a quick way to spin up a new Moodle site using Composer. This is particularly useful for plugin developers who want a reproducible development environment.
69+
70+
```bash
71+
composer create-project moodle/seed [yourlocation]
72+
```
73+
74+
The Moodle scaffolding tool will guide you through the initial site configuration. Within your `[yourlocation]` directory you will find:
75+
76+
- a `composer.json` and `composer.lock`
77+
- a `vendor/` directory
78+
- a `moodle/` directory containing your Moodle installation
79+
80+
To target a specific version of Moodle:
81+
82+
```bash
83+
cd [yourlocation]
84+
composer require "moodle/moodle:~5.2.0"
85+
```
86+
87+
To install a plugin from Packagist:
88+
89+
```bash
90+
cd [yourlocation]
91+
composer require myplugin/moodle-block_myblock
92+
```
93+
94+
### Developing a plugin with a local path repository
95+
96+
When working on your plugin locally, you can tell Composer to use your local checkout instead of downloading from Packagist by adding a `path` repository to your site's `composer.json`:
97+
98+
```json title="composer.json (Moodle site root)"
99+
{
100+
"repositories": [
101+
{
102+
"type": "path",
103+
"url": "/path/to/your/local/moodle-block_myblock",
104+
"options": {
105+
"symlink": false
106+
}
107+
}
108+
],
109+
"require": {
110+
"moodle/composer-installer": "*",
111+
"myplugin/moodle-block_myblock": "*"
112+
}
113+
}
114+
```
115+
116+
Do not use symlinked plugin paths with Moodle. Many PHP entry points in plugins (for example, `view.php` and `index.php`) include `config.php` using relative paths like `require_once('../../config.php')`. With symlinked plugins, PHP resolves the symlink target first, which can make those relative includes resolve outside your Moodle site.
117+
118+
Set `"symlink": false` so Composer mirrors (copies) the plugin into the Moodle tree instead of symlinking it. This avoids relative include path issues.
119+
120+
When developing with a mirrored path repository, re-run `composer update myplugin/moodle-block_myblock` (or remove and re-require the package) after local changes so the copied plugin is refreshed.
121+
122+
:::note
123+
124+
For local path repositories, use `"*"` or `"@dev"` in your `require` constraint so Composer can resolve your local development version regardless of tagged releases.
125+
126+
:::
127+
128+
### Declaring dependencies
129+
130+
:::caution Current recommendation
131+
132+
Moodle currently supports plugins installed both with Composer and without Composer. Composer-declared runtime dependencies are only guaranteed to be installed when the plugin itself is installed via Composer.
133+
134+
For now, avoid introducing Composer-only runtime dependencies that would break non-Composer plugin installs.
135+
136+
:::
137+
138+
When adding Composer metadata to your plugin, these dependency rules apply:
139+
140+
- You can declare dependencies on other Moodle plugins as Composer package requirements.
141+
- You should declare a production dependency on `moodle/composer-installer` in `require` (not `require-dev`) so Moodle package types are installed into the correct locations.
142+
143+
```json title="composer.json with Moodle package dependencies"
144+
{
145+
"name": "myplugin/moodle-block_myblock",
146+
"type": "moodle-block",
147+
"require": {
148+
"moodle/moodle": "^5.2",
149+
"moodle/composer-installer": "*",
150+
"abgreeve/moodle-block_stash": "^5.2"
151+
},
152+
"license": "GPL-3.0-or-later"
153+
}
154+
```
155+
156+
If your plugin has required Moodle plugin dependencies, continue to declare them in `version.php` too, so dependency checks also work for non-Composer installation workflows.
157+
158+
## See also
159+
160+
- [`composer.json`](../../apis/commonfiles/index.mdx#composerjson) — common file reference for Moodle plugins
161+
- [moodle/composer-installer](https://github.com/moodle/composer-installer) — the Composer plugin that installs Moodle packages into the correct directory
162+
- [moodle/seed](https://github.com/moodle/seed) — a Composer project template for spinning up a new Moodle site
163+
- [Packagist](https://packagist.org/) — the main Composer package repository
164+
- [Composer documentation](https://getcomposer.org/doc/) — full Composer reference

src/components/ComponentFileSummary.tsx

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@
1616
*/
1717

1818
/* eslint-disable react/no-unused-prop-types */
19-
import React, { type ReactNode } from 'react';
19+
import React, { type ComponentType, type ReactElement, type ReactNode } from 'react';
2020
import Chip from '@mui/material/Chip';
2121
import Tooltip from '@mui/material/Tooltip';
2222
import Grid from '@mui/material/Grid';
2323
import Details from '@theme/Details';
2424
import { MDXProvider } from '@mdx-js/react';
2525

26-
const getBadge = (title, description, colour = 'info'): JSX.Element => (
26+
type BadgeColour = 'info' | 'success' | 'warning' | 'error';
27+
28+
const getBadge = (title, description, colour: BadgeColour = 'info'): React.JSX.Element => (
2729
<Grid item key={title}>
2830
<Tooltip title={description}>
2931
<Chip
@@ -40,7 +42,7 @@ function getBadges({
4042
deprecated = false,
4143
refreshedDuringUpgrade = false,
4244
refreshedDuringPurge = false,
43-
}): Array<typeof Grid> {
45+
}): React.JSX.Element[] {
4446
const badges = [];
4547
if (refreshedDuringUpgrade) {
4648
// This file is re-read during an upgrade and configuration will be re-applied.
@@ -109,14 +111,30 @@ function getExamples(props) {
109111
return null;
110112
}
111113

114+
export type ComponentFileSummaryDescription = string | ReactElement | ComponentType;
115+
116+
const normaliseDescription = (value?: ComponentFileSummaryDescription): null | ReactNode => {
117+
if (typeof value === 'boolean' || !value) {
118+
return null;
119+
}
120+
121+
if (typeof value === 'string' || React.isValidElement(value)) {
122+
return value;
123+
}
124+
125+
const Description = value;
126+
127+
return <Description />;
128+
};
129+
112130
export interface ComponentFileSummaryProps {
113-
description?: string | ReactNode,
114-
defaultDescription?: string | ReactNode,
131+
description?: ComponentFileSummaryDescription,
132+
defaultDescription?: ComponentFileSummaryDescription,
115133
defaultExample?: string | ReactNode,
116-
example?: string | ReactNode | JSX.Element,
134+
example?: string | ReactNode,
117135
exampleFilepath?: string,
118136
examplePurpose?: string,
119-
extraDescription?: string,
137+
extraDescription?: ComponentFileSummaryDescription,
120138
filepath?: string,
121139
filetype?: string,
122140
modulename?: string,
@@ -132,7 +150,7 @@ export interface ComponentFileSummaryProps {
132150
refreshedDuringPurge?: boolean,
133151
}
134152

135-
export default function ComponentFileSummary(props: ComponentFileSummaryProps): JSX.Element {
153+
export default function ComponentFileSummary(props: ComponentFileSummaryProps): React.JSX.Element {
136154
const {
137155
filepath,
138156
summary,
@@ -141,10 +159,12 @@ export default function ComponentFileSummary(props: ComponentFileSummaryProps):
141159
const badges = getBadges(props);
142160

143161
const description = (() => {
144-
if (props.description) {
162+
const content = normaliseDescription(props.description);
163+
164+
if (content) {
145165
return (
146166
<Grid item xs={12}>
147-
{props.description}
167+
{content}
148168
</Grid>
149169
);
150170
}

0 commit comments

Comments
 (0)