Resource plugins allow developers to interact with the request and response lifecycles of files at a variety of different points along the development and build workflow, when running the develop
and build
commands. These lifecycles provide the ability to do things like:
Although JavaScript is loosely typed, a resource "interface" 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 oncontentType
: A browser compatible contentType to ensure browsers correctly interpret you transformationsconst fs = require('fs');
const { ResourceInterface } = require('@greenwood/cli/src/lib/resource-interface');
class ResourceInterface {
constructor(compilation, options = {}) {
this.compilation = compilation;
this.options = options;
this.extensions = [];
this.contentType = '';
}
// test if this plugin should change a relative URL from the browser to an absolute path on disk
// like for node_modules/ resolution. not commonly needed by most resource plugins
// return true | false
async shouldResolve(url) {
return Promise.resolve(false);
}
// return an absolute path
async resolve(url) {
return Promise.resolve(url);
}
// test if this plugin should be used to process a given url / header for the browser
// ex: `<script type="module-shim" src="index.ts">`
// return true | false
async shouldServe(url, headers) {
return Promise.resolve(this.extensions.indexOf(path.extname(url)) >= 0);
}
// return the new body and / or contentType, e.g. convert file.foo -> file.js
async serve(url, headers) {
return Promise.resolve({});
}
// test if this plugin should return a new body for an already resolved resource
// useful for modifying code on the fly without needing to read the file from disk
// return true | false
async shouldIntercept(url, body, headers) {
return Promise.resolve(false);
}
// return the new body
async intercept(url, body, headers) {
return Promise.resolve({ body });
}
// test if this plugin should manipulate any files prior to any final optmizations happening
// ex: add a "banner" to all .js files with a timestamp of the build, or minifying files
// return true | false
async shouldOptimize(url, body) {
return Promise.resolve(false);
}
// return the new body
async optimize (url, body) {
return Promise.resolve(body);
}
}
module.exports = (options = {}) => {
return {
type: 'resource',
name: 'plugin-example',
provider: (compilation) => new ExampleResource(compilation, options)
}
};
Below is an example of turning files that have a .foo extension into JavaScript.
// file.foo
interface User {
id: number
firstName: string
lastName: string
role: string
}
console.log('hello from file.foo with non standard JavaScript in it.');
// plugin-foo.js
const fs = require('fs');
const { ResourceInterface } = require('@greenwood/cli/src/lib/resource-interface');
class FooResource extends ResourceInterface {
constructor(compilation, options) {
super(compilation, options);
this.extensions = ['.foo'];
this.contentType = 'text/javascript';
}
async serve(url) {
return new Promise(async (resolve, reject) => {
try {
let body = await fs.promises.readFile(url, 'utf-8');
// remove non standard JavaScript usage so browser is happy
body = body.replace(/interface (.*){(.*)}/s, '');
// and we can return .foo as .js!
resolve({
body,
contentType: this.contentType
});
} catch (e) {
reject(e);
}
});
}
}
module.exports = (options = {}) => {
return {
type: 'resource',
name: 'plugin-foo',
provider: (compilation) => new StandardJsonResource(compilation, options)
}
};
// greenwood.config.js
const pluginFoo = require('./plugin-foo');
module.exports = {
...
plugins: [
pluginFoo()
]
}
You can see more in-depth examples of resource plugin by reviewing the default plugins maintained in Greenwood's CLI package.