Skip to content

Commit

Permalink
Server Plugin API (#471)
Browse files Browse the repository at this point in the history
* task: adding initial server api

* fix lint and cases

* refactor live reload to be internal plugin

* server plugin docs

* updates post ResourceInterface changes from release branch

Co-authored-by: Owen Buckley <owenbuckley13@gmail.com>
  • Loading branch information
hutchgrant and thescientist13 committed Apr 3, 2021
1 parent c5c6cc8 commit ec29eca
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 26 deletions.
27 changes: 18 additions & 9 deletions packages/cli/src/commands/develop.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const generateCompilation = require('../lifecycles/compile');
const livereload = require('livereload');
const pluginLiveReloadServer = require('../plugins/server/plugin-livereload')()[0];
const { ServerInterface } = require('../lib/server-interface');
const { devServer } = require('../lifecycles/serve');

module.exports = runDevServer = async () => {
Expand All @@ -9,18 +10,26 @@ module.exports = runDevServer = async () => {
try {
const compilation = await generateCompilation();
const { port } = compilation.config.devServer;
const { userWorkspace } = compilation.context;

devServer(compilation).listen(port, () => {

console.info(`Started local development server at localhost:${port}`);
const liveReloadServer = livereload.createServer({
exts: ['html', 'css', 'js', 'md'],
applyCSSLive: false // https://github.com/napcs/node-livereload/issues/33#issuecomment-693707006
});
// custom user server plugins
const servers = [...compilation.config.plugins.concat([pluginLiveReloadServer]).filter((plugin) => {
return plugin.type === 'server';
}).map((plugin) => {
const provider = plugin.provider(compilation);

liveReloadServer.watch(userWorkspace, () => {
console.info(`Now watching directory "${userWorkspace}" for changes.`);
});
if (!(provider instanceof ServerInterface)) {
console.warn(`WARNING: ${plugin.name}'s provider is not an instance of ServerInterface.`);
}

return provider;
})];

return Promise.all(servers.map(async (server) => {
return server.start();
}));
});
} catch (err) {
reject(err);
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/src/lib/server-interface.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class ServerInterface {
constructor(compilation, options = {}) {
this.compilation = compilation;
this.options = options;
}

async start() {
return Promise.resolve(true);
}

async stop() {
return Promise.resolve(true);
}
}

module.exports = {
ServerInterface
};
2 changes: 1 addition & 1 deletion packages/cli/src/lifecycles/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ module.exports = readAndMergeConfig = async() => {
// }

if (plugins && plugins.length > 0) {
const types = ['resource'];
const types = ['resource', 'server'];

plugins.forEach(plugin => {
if (!plugin.type || types.indexOf(plugin.type) < 0) {
Expand Down
9 changes: 7 additions & 2 deletions packages/cli/src/lifecycles/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ const pluginResourceStandardHtml = require('../plugins/resource/plugin-standard-
const pluginResourceStandardImage = require('../plugins/resource/plugin-standard-image');
const pluginResourceStandardJavaScript = require('../plugins/resource/plugin-standard-javascript');
const pluginResourceStandardJson = require('../plugins/resource/plugin-standard-json');
const { ResourceInterface } = require('../lib/resource-interface');
const pluginLiveReloadResource = require('../plugins/server/plugin-livereload')()[1];
const pluginUserWorkspace = require('../plugins/resource/plugin-user-workspace');
const { ResourceInterface } = require('../lib/resource-interface');

function getDevServer(compilation) {
const app = new Koa();
Expand Down Expand Up @@ -87,7 +88,11 @@ function getDevServer(compilation) {

// allow intercepting of urls
app.use(async (ctx) => {
const reducedResponse = await resources.reduce(async (responsePromise, resource) => {
const modifiedResources = resources.concat(
pluginLiveReloadResource.provider(compilation)
);

const reducedResponse = await modifiedResources.reduce(async (responsePromise, resource) => {
const body = await responsePromise;
const { url } = ctx;
const { headers } = ctx.response;
Expand Down
10 changes: 0 additions & 10 deletions packages/cli/src/plugins/resource/plugin-standard-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,6 @@ const getAppTemplate = (contents, userWorkspace) => {
};

const getUserScripts = (contents) => {
// TODO use an HTML parser? https://www.npmjs.com/package/node-html-parser
if (process.env.__GWD_COMMAND__ === 'develop') { // eslint-disable-line no-underscore-dangle
// TODO setup and teardown should be done together
// console.debug('running in develop mode, attach live reload script');
contents = contents.replace('</head>', `
<script src="http://localhost:35729/livereload.js?snipver=1"></script>
</head>
`);
}

if (process.env.__GWD_COMMAND__ === 'build') { // eslint-disable-line no-underscore-dangle
// TODO setup and teardown should be done together
// console.debug('running in build mode, polyfill WebComponents for puppeteer');
Expand Down
58 changes: 58 additions & 0 deletions packages/cli/src/plugins/server/plugin-livereload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const livereload = require('livereload');
const path = require('path');
const { ResourceInterface } = require('../../lib/resource-interface');
const { ServerInterface } = require('../../lib/server-interface');

class LiveReloadServer extends ServerInterface {
constructor(compilation, options = {}) {
super(compilation, options);

this.liveReloadServer = livereload.createServer({
exts: ['html', 'css', 'js', 'md'],
applyCSSLive: false // https://github.com/napcs/node-livereload/issues/33#issuecomment-693707006
});
}

async start() {
const { userWorkspace } = this.compilation.context;

this.liveReloadServer.watch(userWorkspace, () => {
console.info(`Now watching directory "${userWorkspace}" for changes.`);
return Promise.resolve(true);
});
}
}

class LiveReloadResource extends ResourceInterface {

async shouldIntercept(url) {
return Promise.resolve(path.extname(url) === '' && process.env.__GWD_COMMAND__ === 'develop'); // eslint-disable-line no-underscore-dangle
}

async intercept(url, body) {
return new Promise((resolve, reject) => {
try {
const contents = body.replace('</head>', `
<script src="http://localhost:35729/livereload.js?snipver=1"></script>
</head>
`);

resolve(contents);
} catch (e) {
reject(e);
}
});
}
}

module.exports = (options = {}) => {
return [{
type: 'server',
name: 'plugin-live-reload:server',
provider: (compilation) => new LiveReloadServer(compilation, options)
}, {
type: 'resource',
name: 'plugin-live-reload:resource',
provider: (compilation) => new LiveReloadResource(compilation, options)
}];
};
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('Build Greenwood With: ', function() {
try {
await setup.runGreenwoodCommand('build');
} catch (err) {
expect(err).to.contain('Error: greenwood.config.js plugins must be one of type "resource". got "indexxx" instead.');
expect(err).to.contain('Error: greenwood.config.js plugins must be one of type "resource, server". got "indexxx" instead.');
}
});
});
Expand Down
7 changes: 5 additions & 2 deletions www/pages/plugins/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Greenwood aims to cater to these use cases through two approaches:
### API
Each plugin must return a function that has the following three properties:.
- `name`: A string to give your plugin a name and used for error handling and troubleshooting.
- `type`: A string to specify to Greenwood the type of plugin. Right now the current supported plugin type `'resource'`
- `type`: A string to specify to Greenwood the type of plugin. Right now the current supported plugin type [`'resource'`](/plugins/resource/) and [`'plugin'`](/plugins/server/).
- `provider`: A function that will be invoked by Greenwood that Can accept a `compilation` param that provides read-only access to parts of Greenwood's state and configuration that can be used by a plugin.

Here is an example of creating a plugin in a _greenwood.config.js_.
Expand Down Expand Up @@ -106,4 +106,7 @@ module.exports = {
While each API has its own documentation section on the left sidebar of this page, here is a quick overview of the current set of Plugin APIs Greenwood supports.

#### Resource Plugins
Resource plugins allow users to interact with the request and response lifecycles of files at a variety of different ways. These lifecycles provide the ability to do things like introduce new file types, to adding hosted 3rd party scripts to your site.
[Resource plugins](/plugins/resource/) allow users to interact with the request and response lifecycles of files at a variety of different ways. These lifecycles provide the ability to do things like introduce new file types, to adding hosted 3rd party scripts to your site.

#### Server Plugins
[Server plugins](/plugins/server/) allow developers to start and stop custom servers as part of the **serve** lifecycle of Greenwood. These lifecycles provide the ability to do things like start a GraphQL server, or reverse proy requests to a custom server.
2 changes: 1 addition & 1 deletion www/pages/plugins/resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Resource plugins allow developers to interact with the request and response life
- Introduce additional file types, like TypeScript

### API (Resource Interface)
Although JavaScript is loosely typed, a [resource "interface"](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/lib/resource-interface.js) has been provided by Greenwood that you can use to start building our own resource plugins. Effectively you have to define two things:
Although JavaScript is loosely typed, a [resource "interface"](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/lib/resource-interface.js) has been provided by Greenwood that you can use to start building your own resource plugins. Effectively you have to define two things:
- `extensions`: The file types your plugin will operate on
- `contentType`: A browser compatible contentType to ensure browsers correctly interpret you transformations

Expand Down
64 changes: 64 additions & 0 deletions www/pages/plugins/server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
label: 'Server'
menu: side
title: 'Server'
index: 3
---

## Server

Server plugins allow developers to start and stop custom servers as part of the serve lifecycle of Greenwood. These lifecycles provide the ability to do things like:
- Start a live reload server (like Greenwood does by default)
- Starting a GraphQL server
- Reverse proxy to help route external requests

### API (Server Interface)
Although JavaScript is loosely typed, a [server "interface"](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/lib/server-interface.js) has been provided by Greenwood that you can use to start building your own server plugins. Effectively you just have to provide two methods
- `start` - function to run to start your server
- `stop` - function to run to stop / teaddown your server


They can be used in a _greenwood.config.js_ just like any other plugin type.
```javascript
const pluginMyServerFoo = require('./plugin-my-server');

module.exports = {

...

plugins: [
pluginMyServer()
]

}
```

## Example
The below is an excerpt of [Greenwood's internal LiveReload server](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/plugins/server/plugin-livereload.js) plugin.

```javascript
class LiveReloadServer extends ServerInterface {
constructor(compilation, options = {}) {
super(compilation, options);

this.liveReloadServer = livereload.createServer({ /* options */});
}

async start() {
const { userWorkspace } = this.compilation.context;

return this.liveReloadServer.watch(userWorkspace, () => {
console.info(`Now watching directory "${userWorkspace}" for changes.`);
return Promise.resolve(true);
});
}
}

module.exports = (options = {}) => {
return {
type: 'server',
name: 'plugin-livereload',
provider: (compilation) => new LiveReloadServer(compilation, options)
}
};
```

0 comments on commit ec29eca

Please sign in to comment.