From ec29eca26c49fe69a6bdb83231e38bcff98c20ca Mon Sep 17 00:00:00 2001 From: Grant Hutchinson Date: Wed, 17 Feb 2021 21:39:47 -0500 Subject: [PATCH] Server Plugin API (#471) * 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 --- packages/cli/src/commands/develop.js | 27 +++++--- packages/cli/src/lib/server-interface.js | 18 ++++++ packages/cli/src/lifecycles/config.js | 2 +- packages/cli/src/lifecycles/serve.js | 9 ++- .../plugins/resource/plugin-standard-html.js | 10 --- .../src/plugins/server/plugin-livereload.js | 58 +++++++++++++++++ .../build.plugins.error-type.spec.js | 2 +- www/pages/plugins/index.md | 7 +- www/pages/plugins/resource.md | 2 +- www/pages/plugins/server.md | 64 +++++++++++++++++++ 10 files changed, 173 insertions(+), 26 deletions(-) create mode 100644 packages/cli/src/lib/server-interface.js create mode 100644 packages/cli/src/plugins/server/plugin-livereload.js create mode 100644 www/pages/plugins/server.md diff --git a/packages/cli/src/commands/develop.js b/packages/cli/src/commands/develop.js index 4f8006f28..55c54ef9a 100644 --- a/packages/cli/src/commands/develop.js +++ b/packages/cli/src/commands/develop.js @@ -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 () => { @@ -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); diff --git a/packages/cli/src/lib/server-interface.js b/packages/cli/src/lib/server-interface.js new file mode 100644 index 000000000..aeaed6d7e --- /dev/null +++ b/packages/cli/src/lib/server-interface.js @@ -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 +}; \ No newline at end of file diff --git a/packages/cli/src/lifecycles/config.js b/packages/cli/src/lifecycles/config.js index 2d830ce07..e81c3e517 100644 --- a/packages/cli/src/lifecycles/config.js +++ b/packages/cli/src/lifecycles/config.js @@ -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) { diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js index 6651dc025..8faa1773f 100644 --- a/packages/cli/src/lifecycles/serve.js +++ b/packages/cli/src/lifecycles/serve.js @@ -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(); @@ -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; diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js index 372a45919..92dd526c7 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-html.js +++ b/packages/cli/src/plugins/resource/plugin-standard-html.js @@ -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('', ` - - - `); - } - 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'); diff --git a/packages/cli/src/plugins/server/plugin-livereload.js b/packages/cli/src/plugins/server/plugin-livereload.js new file mode 100644 index 000000000..24ea910e7 --- /dev/null +++ b/packages/cli/src/plugins/server/plugin-livereload.js @@ -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('', ` + + + `); + + 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) + }]; +}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.plugins.error-type/build.plugins.error-type.spec.js b/packages/cli/test/cases/build.plugins.error-type/build.plugins.error-type.spec.js index 74cd25d4a..7806047bd 100644 --- a/packages/cli/test/cases/build.plugins.error-type/build.plugins.error-type.spec.js +++ b/packages/cli/test/cases/build.plugins.error-type/build.plugins.error-type.spec.js @@ -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.'); } }); }); diff --git a/www/pages/plugins/index.md b/www/pages/plugins/index.md index 9d6d5e0d9..1cdb7e6d1 100644 --- a/www/pages/plugins/index.md +++ b/www/pages/plugins/index.md @@ -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_. @@ -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. \ No newline at end of file +[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. \ No newline at end of file diff --git a/www/pages/plugins/resource.md b/www/pages/plugins/resource.md index 72fbb4aef..cc95aa3b1 100644 --- a/www/pages/plugins/resource.md +++ b/www/pages/plugins/resource.md @@ -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 diff --git a/www/pages/plugins/server.md b/www/pages/plugins/server.md new file mode 100644 index 000000000..d8e29108f --- /dev/null +++ b/www/pages/plugins/server.md @@ -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) + } +}; +``` \ No newline at end of file