Introduced as a concept in the Context Plugin docs, a theme pack is what Greenwood uses to refer to a plugin that aims to provide a set of reusable templates, pages and more to a user (think of CSS Zen Garden). A good example (and the one this guide is based on) is greenwood-starter-presentation, which provides the starting point for creating a slide deck entirely from markdown, using Greenwood!
This guide will walk through the process of setting up Greenwood to support the developing and publishing of your package (theme pack) to npm.
To try and focus on just the theme pack aspects, this guide assumes a couple things:
We encourage using Greenwood to develop your theme pack mainly so that you can ensure a seamless experience when publishing to npm knowing that things should just work. ™️
For the sake of development, you can create as much as you need to recreate a user workspace and to simulate what your theme pack would look like. Think of it like creating a Storybook for your theme pack.
For this guide, we will be publishing layouts/ (templates) and styles/ to npm. The pages/ directory is just being used to pull in the template for local development and testing purposes for you as the plugin author.
src/
pages/
index.md
styles/
theme.css
layouts/
blog-post.html
package.json
my-theme-pack.js
greenwood.config.js
package.json
{
"name": "my-theme-pack",
"version": "0.1.0",
"description": "My Custom Greenwood Theme Pack",
"main": "my-theme-pack.js",
"type": "module",
"files": [
"dist/"
]
}
my-theme-pack.js
import { fileURLToPath } from 'url';
const myThemePack = () => [{
type: 'context',
name: 'my-theme-pack:context',
provider: () => {
return {
templates: [
// import.meta.url will be located at _node_modules/your-package/_
// when your plugin is run in a user's project
fileURLToPath(new URL('./dist/layouts', import.meta.url))
]
};
}
}];
export {
myThemePack
};
src/layouts/blog-post.html
<html>
<!-- we're using the npm publishing paths here which will come up again in the development section -->
<head>
<!-- reference JS or assets/ too! -->
<link rel="stylesheet" href="/node_modules/my-theme-pack/dist/styles/theme.css">
</head>
<body>
<!-- whatever else you want to add to the page for the user! -->
<content-outlet></content-outlet>
</body>
</html>
src/styles/theme.css
* {
color: red
}
src/pages/index.md
---
template: 'blog-post'
---
# Title of blog post
Lorum Ipsum, this is a test.
The main consideration needed for development is that your files won't be in node_modules, which is what the case would be for users when you publish. So for that reason, we need to add a little boilerplate to my-theme-pack.js. There might be others way to solve it, but for right now, accepting a "developer only" flag can easily make the plugin pivot into local or "published" modes.
new URL('.', import.meta.url)
(which would resolve to the package's location inside node_modules) as the base pathprocess.cwd
So using our current example, our final my-theme-pack.js would look like this:
import path from 'path';
import { fileURLToPath, URL } from 'url';
const myThemePackPlugin = (options = {}) => [{
type: 'context',
name: 'my-theme-pack:context',
provider: (compilation) => {
// you can use other directory names besides templates/ this way!
const templateLocation = options.__isDevelopment
? path.join(compilation.context.userWorkspace, 'layouts')
: fileURLToPath(new URL('dist/layouts', import.meta.url));
return {
templates: [
templateLocation
]
};
}
}];
export {
myThemePackPlugin
};
And our final greenwood.config.js would look like this, which adds a "one-off" resource plugin to tell Greenwood to route requests to your theme pack files away from _node_modules+ and to the location of your projects files for development.
Additionally, we make sure to pass the flag from above for __isDevelopment
to our plugin.
// shared from another test
import { myThemePackPlugin } from './my-theme-pack.js';
import fs from 'fs';
import path from 'path';
import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js';
const packageName = JSON.parse(fs.readFileSync('./package.json', 'utf-8')).name;
class MyThemePackDevelopmentResource extends ResourceInterface {
constructor(compilation, options) {
super(compilation, options);
this.extensions = ['*'];
}
async shouldResolve(url) {
// eslint-disable-next-line no-underscore-dangle
return Promise.resolve((process.env.__GWD_COMMAND__ === 'develop') && url.indexOf(`/node_modules/${packageName}/`) >= 0);
}
async resolve(url) {
const { userWorkspace } = this.compilation.context;
const filePath = this.getBareUrlPath(url).split(`/node_modules/${packageName}/dist/`)[1];
return Promise.resolve(path.join(userWorkspace, filePath));
}
}
export default {
plugins: [
...myThemePackPlugin({
__isDevelopment: true
}),
{
type: 'resource',
name: 'my-theme-pack:resource',
provider: (compilation, options) => new MyThemePackDevelopmentResource(compilation, options)
}
]
};
You should then be able to run yarn develop
and load /
in your browser and the color of the text should be red.
You're all ready for development now! 🙌
You can also use Greenwood to test your theme pack using a production build so that you can run greenwood build
or greenwood serve
to validate your work. To do so requires just one additional script to your package.json to put your theme pack files in the node_modules where Greenwood would assume them to be. Just call this before build
or serve
.
{
"scripts": {
"build:pre": "mkdir -pv ./node_modules/greenwood-starter-presentation/dist && rsync -rv --exclude 'pages/' ./src/ ./node_modules/greenwood-starter-presentation/dist",
"build": "npm run build:pre && greenwood build",
"serve": "npm run build:pre && greenwood serve"
}
}
When it comes to publishing, it should be fairly straightforward, and you'll just want to do the following:
files
location you want to use for publishing)prepublish
script to your package.json to create the dist/ directory with all the needed layouts (templates) / and _styles/
{
"name": "my-theme-pack",
"version": "0.1.0",
"description": "My Custom Greenwood Theme Pack",
"main": "my-theme-pack.js",
"type": "module",
"files": [
"dist/"
],
"scripts": {
"prepublish": "rm -rf dist/ && mkdir dist/ && rsync -rv --exclude 'pages/' src/ dist"
}
}
npm publish
a fresh dist/ folder will be made and included in your packageWith the above in place and the package published, you're now ready to share your theme pack with other Greenwood users!
For users, they would just need to do the following:
Install the plugin from npm
$ npm install my-theme-pack --save-dev
Add the plugin to their greenwood.config.js
const myThemePackPlugin = require('my-theme-pack');
module.exports = {
plugins: [
...myThemePackPlugin()
]
};
Then in any of their markdown files, users would just need to reference the published template's filename
---
template: 'blog-post'
---
My Blog Post using Theme Packs! 💯
Success! 🥳
Don't forget, user's can also include additional CSS / JS files in their frontmatter, to further extend, customize, and override your templates!
Support for including pages as part of a theme pack is planned and coming soon, pretty much as soon as we can support external data sources in the CLI.
Yes, we do realize this current workflow is a bit clunky at the moment, so please follow this discussion for ways we can try and make this more elegant! 🙏🏻
ex.
<html>
<!-- we're using the npm publishing paths here which will come up again in the development section -->
<head>
<!-- reference JS or assets/ too! -->
<link rel="stylesheet" href="../styles/theme.css">
</head>
<body>
...
</body>
</html>
Good question! We tried that approach initially as it would help alleviate the open issues and needing to work around local development vs published development identified above, but the issue that was faced was that relative paths like the above don't preserve their location / context on disk when coming through the development server
# with explicit path that includes node_modules (is exactly the same)
url -> /node_modules/my-theme-pack/dist/styles/theme.css
# with relative paths
url -> < pagesDir >/styles/theme.css
And so at this time Greenwood only looks in the user's workspace, not in _node_modules) and so it will 404
.