v0.28.0
Overview
In addition to some general refactoring around bundling SSR routes and some breaking changes (see section on Breaking Changes below), this release introduces some exciting new feature and enhancements for Greenwood!
- 📦 Node 18 (minimum) version upgrade
- 🕸️ Web API Standardization
- ⚡ API Routes
Checkout the accompanying blog post for more information on all these features.
If using Yarn, you can can upgrade all your @greenwood packages at once
$ yarn upgrade --scope @greenwood --latest
Changelog
https://github.com/ProjectEvergreen/greenwood/issues?q=label%3Av0.28.0
- upgrade to Node v18 as minimum supported version
- Standardize on Web APIs (
Request
/Response
/URL
/ etc) - API Routes
- Support web standard
<audio>
and<video>
formats - Create a plugin for import JSX plugin (using WCC)
- bundle and optimize server and API routes and serve from output directory (decouple
serve
task from source code)
Breaking Changes
Node 18
The new minimum supported Node version with Greenwood is now v18. So make sure you update your GitHub Actions, hosting configuration; e.g. _netlify.toml, .nvmrc, etc.
Additionally, Greenwood now longer depends on node-fetch however native fetch
in Node 18 can / should be a drop in replacement for most cases. Just remove your import
line for node-fetch and test, and if so, you should be good to go! (You can always self install node-fetch if you want it back though).
greenwood.config.js
Workspace
You can now pass in a URL object directly instead of having to convert it to a path
import { fileURLToPath } from 'url';
// before
export default {
workspace: fileURLToPath(new URL('./www', import.meta.url))
};
// after
export default {
workspace: new URL('./www', import.meta.url)
};
devServer.extensions
No period (.
) is needed when passing in custom extensions.
// before
extensions: ['.txt', '.rtf']
// after
extensions: ['txt', 'rtf']
Plugins
Resource
Resource plugins have had their lifecycle signatures and return types refactored to align exclusively with Web APIs. The docs have been updated with more in depth examples so it is recommended to review those, but otherwise the business logic of those lifecycles should still apply. Below is a high level breakdown of the changes and an example.
shouldResolve
/ resolve
- Function signature now provides an instance of
URL
instead of just a string path - Expects a
Request
to be returned
// before
async shouldResolve(url = '/') {
const { userWorkspace } = this.compilation.context;
return fs.existsSync(path.join(userWorkspace, url));
}
async resolve(url = '/') {
const { userWorkspace } = this.compilation.context;
return path.join(userWorkspace, bareUrl));
}
// after
async shouldResolve(url) {
const { userWorkspace } = this.compilation.context;
const { pathname } = url;
try {
await fs.access(new URL(`.${pathname}`, userWorkspace);
return true;
} catch(}{
return false;
}
}
async resolve(url) {
const { pathname } = url;
const { userWorkspace } = this.compilation.context;
return new Request(new URL(`.${pathname}`, userWorkspace));
}
shouldServe
/ serve
- Function signature now provides an instance of
URL
andRequest
- Expects a
Response
to be returned
// before
async shouldServe(url) {
return path.extname(url) === '.css' && fs.existsSync(url);
}
async serve(url) {
const css = await fs.promises.readFile(url, 'utf-8');
resolve({
body: css,
contentType: this.contentType
});
}
// after
async shouldServe(url) {
return url.protocol === 'file:' && this.extensions.indexOf(url.pathname.split('.').pop()) >= 0;
}
async serve(url) {
const body = await fs.readFile(url, 'utf-8');
return new Response(body, {
headers: {
'Content-Type': 'text/css'
}
});
}
shouldIntercept
/ intercept
- Function signature now provides an instance of
URL
,Request
, andResponse
- Expects a
Response
to be returned
// before
async shouldIntercept(url, body, headers = { request: {} }) {
const { originalUrl = '' } = headers.request;
const accept = headers.request.accept || '';
const isCssFile = path.extname(url) === '.css';
const notFromBrowser = accept.indexOf('text/css') < 0 && accept.indexOf('application/signed-exchange') < 0;
// https://github.com/ProjectEvergreen/greenwood/issues/492
const isCssInJs = originalUrl.indexOf('?type=css') >= 0
|| isCssFile && notFromBrowser
|| isCssFile && notFromBrowser && url.indexOf('/node_modules/') >= 0;
return isCssInJs
}
async intercept(url, body) {
const finalBody = body || await fs.promises.readFile(pathToFileURL(url), 'utf-8');
const cssInJsBody = `const css = \`${finalBody.replace(/\r?\n|\r/g, ' ').replace(/\\/g, '\\\\')}\`;\nexport default css;`;
return {
body: cssInJsBody,
contentType: this.contentType
};
}
// after
async shouldIntercept(url, request) {
const { pathname } = url;
const accepts = request.headers.get('accept') || '';
const isCssFile = pathname.split('.').pop() === 'css';
const notFromBrowser = accepts.indexOf('text/css') < 0 && accepts.indexOf('application/signed-exchange') < 0;
// https://github.com/ProjectEvergreen/greenwood/issues/492
const isCssInJs = url.searchParams.has('type') && url.searchParams.get('type') === this.extensions[0]
|| isCssFile && notFromBrowser
|| isCssFile && notFromBrowser && pathname.startsWith('/node_modules/');
return isCssInJs;
}
async intercept(url, request, response) {
const body = await response.text();
const cssInJsBody = `const css = \`${body.replace(/\r?\n|\r/g, ' ').replace(/\\/g, '\\\\')}\`;\nexport default css;`;
return new Response(cssInJsBody, {
headers: new Headers({
'Content-Type': this.contentType
})
});
}
shouldOptimize
/ optimize
- Function signature now provides an instance of
URL
andResponse
- Expects a
Response
to be returned
// before
async shouldOptimize(url) {
return path.extname(url) === '.css' && this.compilation.config.optimization !== 'none'
}
async optimize(url, body) {
return bundleCss(body);
}
// after
async shouldOptimize(url, response) {
const { protocol, pathname } = url;
const isValidCss = pathname.split('.').pop() === 'css'
&& protocol === 'file:'
&& response.headers.get('Content-Type').indexOf('text/css') >= 0;
return this.compilation.config.optimization !== 'none' && isValidCss;
}
async optimize(url, response) {
const body = await response.text();
const optimizedBody = bundleCss(body);
return new Response(optimizedBody);
}
Copy
Now the Copy plugin expects to the to
and from
properties to be URLs
.
For directories, make sure to add a trailing
/
!
// before
[{
// copy a file
from: path.join(context.userWorkspace, 'robots.txt'),
to: path.join(context.outputDir, 'robots.txt')
}, {
// copy a directory
from: path.join(context.userWorkspace, 'pdfs'),
to: path.join(context.outputDir, 'pdfs')
}];
// after
[{
// copy a file
from: new URL('./robots.txt', context.userWorkspace),
to: new URL('./robots.txt', context.outputDir)
}, {
// copy a directory
from: new URL('./pdfs/', context.userWorkspace),
to: new URL('./pdfs/', context.outputDir)
}];
Context (Theme Packs)
For Context plugins, a URL
is now expected for template locations.
// before
import { fileURLToPath } from 'urt';
return {
templates: [
fileURLToPath(new URL('./dist/layouts', import.meta.url))
]
};
// after
return {
templates: [
new URL('./dist/layouts/', import.meta.url)
]
};
Integrations (e.g. WTR)
For custom 3rd party tools like WTR (Web Test Runner), you can still use your resource plugins, updated for the new API. Below are a couple examples for supporting TypeScript and importing CSS-in-JS.
import fs from 'fs/promises';
import { greenwoodPluginImportCss } from '@greenwood/plugin-import-css/src/index.js';
import { greenwoodPluginTypeScript } from '@greenwood/plugin-typescript/src/index.js';
// create a direct instance of ImportCssResource
const importCssResource = greenwoodPluginImportCss()[0].provider({});
// create a direct instance of TypeScriptResource
const typeScriptResource = greenwoodPluginTypeScript()[0].provider({
context: {
projectDirectory: new URL(import.meta.url)
}
});
export default {
plugins: [{
name: 'transpile-typescript',
async transform(context) {
const { url } = context.request;
if (url.endsWith('.ts')) {
const response = await typeScriptResource.serve(new URL(`.${url}`, import.meta.url));
// https://github.com/ProjectEvergreen/greenwood/issues/661
const body = (await response.text()).replace(/\/\/# sourceMappingURL=module.js.map/, '');
return {
body,
type: 'js'
};
}
}
}, {
name: 'import-css',
async transform(context) {
const url = new URL(`.${context.request.url}`, import.meta.url);
const request = new Request(url, { headers: new Headers(context.headers) });
const shouldIntercept = await importCssResource.shouldIntercept(url, request);
if (shouldIntercept) {
const contents = await fs.readFile(url);
const initResponse = new Response(contents, { headers: new Headers(context.headers) });
const response = await importCssResource.intercept(url, request, initResponse.clone());
return {
body: await response.text(),
headers: {
'Content-Type': response.headers.get('Content-Type')
}
};
}
}
}]
};
ESM
To be compliant with the ESM spec, all your files referenced within JavaScript must start with a .
. Using a /
is not part of the spec.
<script>
// before
import { foo } from '/some/thing.js';
// after
import { foo } from '../some/thing.js';
</script>
greenwood serve
It will now be required to run greenwood build
before running greenwood serve
.
Known Issues
- SSR pages are not getting their resources (
<link>
and<script>
tags) bundled and optimized with the serve command - SSR pages with
prerender
configuration are not getting served from static HTML - not found static assets are returning 500 status code when running the
serve
command - imported modules in API routes not reloading changes in development mode
- browsers inconsistently serving incorrect / stale content from dev server
Diff
$ git diff v0.27.0 v0.28.0 --stat | grep -v "www"
.eslintrc.cjs | 1 +
.gitattributes | 13 +-
.github/CONTRIBUTING.md | 24 +-
.github/ISSUE_TEMPLATE.md | 6 +-
.github/workflows/ci-exp.yml | 6 +-
.github/workflows/ci-win-exp.yml | 4 +-
.github/workflows/ci-win.yml | 4 +-
.github/workflows/ci.yml | 6 +-
.github/workflows/master.yml | 6 +-
.github/workflows/release.yml | 6 +-
.gitignore | 1 +
.nvmrc | 2 +-
greenwood.config.js | 10 +-
lerna.json | 2 +-
netlify.toml | 2 +-
package.json | 5 +-
packages/cli/package.json | 21 +-
packages/cli/src/commands/build.js | 13 +-
packages/cli/src/commands/eject.js | 20 +-
packages/cli/src/commands/serve.js | 6 +-
packages/cli/src/config/rollup.config.js | 159 +++--
packages/cli/src/index.js | 14 +-
packages/cli/src/lib/node-modules-utils.js | 24 +-
packages/cli/src/lib/resource-interface.js | 88 ---
packages/cli/src/lib/resource-utils.js | 116 +++-
packages/cli/src/lib/router.js | 41 +-
packages/cli/src/lib/ssr-route-worker.js | 7 +-
packages/cli/src/lib/templating-utils.js | 201 ++++++
packages/cli/src/lifecycles/bundle.js | 252 ++++++--
packages/cli/src/lifecycles/compile.js | 99 ++-
packages/cli/src/lifecycles/config.js | 76 +--
packages/cli/src/lifecycles/context.js | 31 +-
packages/cli/src/lifecycles/copy.js | 112 ++--
packages/cli/src/lifecycles/graph.js | 111 ++--
packages/cli/src/lifecycles/prerender.js | 156 ++---
packages/cli/src/lifecycles/serve.js | 395 ++++++------
packages/cli/src/loader.js | 92 +--
.../cli/src/plugins/copy/plugin-copy-assets.js | 15 +-
.../cli/src/plugins/copy/plugin-copy-favicon.js | 15 +-
.../cli/src/plugins/copy/plugin-copy-graph-json.js | 8 +-
.../src/plugins/copy/plugin-copy-manifest-json.js | 14 +
.../cli/src/plugins/copy/plugin-copy-robots.js | 15 +-
.../src/plugins/copy/plugin-copy-user-templates.js | 21 +
.../cli/src/plugins/resource/plugin-api-routes.js | 42 ++
.../cli/src/plugins/resource/plugin-dev-proxy.js | 26 +-
.../src/plugins/resource/plugin-node-modules.js | 151 ++---
.../cli/src/plugins/resource/plugin-source-maps.js | 26 +-
.../src/plugins/resource/plugin-standard-audio.js | 76 +++
.../src/plugins/resource/plugin-standard-css.js | 65 +-
.../src/plugins/resource/plugin-standard-font.js | 30 +-
.../src/plugins/resource/plugin-standard-html.js | 648 +++++++-------------
.../src/plugins/resource/plugin-standard-image.js | 57 +-
.../plugins/resource/plugin-standard-javascript.js | 25 +-
.../src/plugins/resource/plugin-standard-json.js | 42 +-
.../src/plugins/resource/plugin-standard-video.js | 78 +++
.../src/plugins/resource/plugin-static-router.js | 179 +++---
.../src/plugins/resource/plugin-user-workspace.js | 33 +-
.../cli/src/plugins/server/plugin-livereload.js | 49 +-
.../greenwood.config.js | 4 +-
.../build.config.error-workspace.spec.js | 4 +-
.../build.config-optimization-default.spec.js | 28 +-
.../fixtures/expected.css | 2 +-
.../src/pages/index.html | 11 +-
.../src/styles/main.css | 2 +
.../build.config-optimization-inline.spec.js | 4 +-
.../greenwood.config.js | 4 +-
.../build.default.ssr-prerender.spec.js | 4 +-
.../build.default.ssr-static-export.spec.js | 21 +-
.../src/pages/artists.js | 2 -
.../cases/build.default.ssr/greenwood.config.js | 3 -
.../build.default.workspace-javascript-css.spec.js | 6 +-
.../theme-pack-context-plugin.js | 11 +-
.../build.plugins.copy/build.plugins.copy.spec.js | 70 +++
.../cases/build.plugins.copy/greenwood.config.js | 14 +
.../build.plugins.resource/greenwood.config.js | 34 +-
.../cases/develop.default/develop.default.spec.js | 172 ++++--
.../test/cases/develop.default/src/api/greeting.js | 11 +
.../develop.default/src/assets/song-sample.mp3 | Bin 0 -> 5709921 bytes
.../develop.default/src/assets/splash-clip.mp4 | Bin 0 -> 2636174 bytes
.../develop.plugins.context.spec.js | 4 +-
.../develop.plugins.context/greenwood.config.js | 14 +-
.../cli/test/cases/develop.spa/develop.spa.spec.js | 40 +-
packages/cli/test/cases/develop.spa/src/main.css | 3 +
.../test/cases/develop.ssr/src/pages/artists.js | 2 -
.../serve.config.static-router.spec.js | 65 +-
.../serve.default.api/serve.default.api.spec.js | 137 +++++
.../cases/serve.default.api/src/api/fragment.js | 18 +
.../cases/serve.default.api/src/api/greeting.js | 11 +
.../cases/serve.default.api/src/components/card.js | 11 +
.../serve.default.error.spec.js | 52 ++
.../greenwood.config.js | 3 +
.../serve.default.ssr-prerender.spec.js | 119 ++++
.../src/components/footer.js | 16 +
.../serve.default.ssr-prerender/src/pages/index.js | 7 +
.../src/templates/app.html | 13 +
.../serve.default.ssr-static-export/package.json | 6 +
.../serve.default.ssr-static-export.spec.js | 246 ++++++++
.../src/components/counter.js | 0
.../src/components/footer.js | 0
.../src/pages/artists.js | 90 +++
.../src/pages/index.md | 3 +
.../src/templates/app.html | 13 +
.../cases/serve.default.ssr/greenwood.config.js | 3 +
.../serve.default.ssr.spec.js} | 19 +-
.../src/components/card.js | 0
.../serve.default.ssr/src/components/counter.js | 42 ++
.../src/pages/about.md | 0
.../src/pages/artists.js | 2 -
.../src/pages/index.js | 0
.../src/pages/users.js | 1 -
.../src/templates/app.html | 0
.../test/cases/serve.default/serve.default.spec.js | 136 ++++-
.../cases/serve.default/src/assets/song-sample.mp3 | Bin 0 -> 5709921 bytes
.../cases/serve.default/src/assets/splash-clip.mp4 | Bin 0 -> 2636174 bytes
.../cli/test/cases/serve.spa/serve.spa.spec.js | 193 ++++++
packages/cli/test/cases/serve.spa/src/index.html | 12 +
.../cli/test/cases/theme-pack/greenwood.config.js | 13 +-
.../cli/test/cases/theme-pack/my-theme-pack.js | 10 +-
.../test/cases/theme-pack/theme-pack.build.spec.js | 10 +-
.../cases/theme-pack/theme-pack.develop.spec.js | 4 +-
packages/init/package.json | 10 +-
packages/init/src/index.js | 1 -
packages/plugin-babel/README.md | 4 +-
packages/plugin-babel/package.json | 11 +-
packages/plugin-babel/src/index.js | 38 +-
packages/plugin-google-analytics/README.md | 1 +
packages/plugin-google-analytics/package.json | 9 +-
packages/plugin-google-analytics/src/index.js | 55 +-
.../test/cases/default/default.spec.js | 24 +-
.../option-anonymous/option-anonymous.spec.js | 4 +-
packages/plugin-graphql/README.md | 10 +-
packages/plugin-graphql/package.json | 14 +-
packages/plugin-graphql/src/core/cache.js | 14 +-
packages/plugin-graphql/src/index.js | 77 +--
packages/plugin-graphql/src/schema/schema.js | 15 +-
.../cases/develop.default/develop.default.spec.js | 4 +-
.../cases/qraphql-server/graphql-server.spec.js | 2 +-
.../cases/query-children/query-children.spec.js | 2 +-
.../test/cases/query-config/query-config.spec.js | 2 +-
.../query-custom-frontmatter.spec.js | 2 +-
.../query-custom-schema.spec.js | 2 +-
.../cases/query-custom-schema/src/pages/index.html | 2 +-
.../test/cases/query-graph/query-graph.spec.js | 2 +-
.../test/cases/query-menu/query-menu.spec.js | 2 +-
packages/plugin-import-commonjs/package.json | 13 +-
packages/plugin-import-commonjs/src/index.js | 35 +-
.../test/cases/default/default.spec.js | 2 +-
packages/plugin-import-css/README.md | 17 +-
packages/plugin-import-css/package.json | 13 +-
packages/plugin-import-css/src/index.js | 54 +-
.../cases/develop.default/develop.default.spec.js | 4 +-
packages/plugin-import-json/README.md | 11 +-
packages/plugin-import-json/package.json | 11 +-
packages/plugin-import-json/src/index.js | 38 +-
.../test/cases/default/default.spec.js | 2 +-
.../cases/develop.default/develop.default.spec.js | 4 +-
.../test/cases/develop.default/src/main.json | 5 +-
packages/plugin-import-jsx/README.md | 61 ++
packages/plugin-import-jsx/package.json | 35 ++
packages/plugin-import-jsx/src/index.js | 43 ++
.../test/cases/default/default.prerender.spec.js | 87 +++
.../test/cases/default/greenwood.config.js | 7 +
.../test/cases/default/package.json | 5 +
.../test/cases/default/src/components/footer.jsx | 15 +
.../test/cases/default/src/pages/index.md | 3 +
.../test/cases/default/src/templates/app.html | 12 +
.../exp-build.prerender.spec.js | 89 +++
.../cases/exp-build.prerender/greenwood.config.js | 8 +
.../test/cases/exp-build.prerender/package.json | 5 +
.../exp-build.prerender/src/components/footer.jsx | 17 +
.../cases/exp-build.prerender/src/pages/index.md | 3 +
.../exp-build.prerender/src/templates/app.html | 12 +
packages/plugin-include-html/package.json | 9 +-
packages/plugin-include-html/src/index.js | 72 ++-
.../src/components/footer.js | 8 +-
.../build.default.link-tag.spec.js | 2 +-
packages/plugin-polyfills/package.json | 9 +-
packages/plugin-polyfills/src/index.js | 104 ++--
packages/plugin-postcss/README.md | 4 +-
packages/plugin-postcss/package.json | 9 +-
packages/plugin-postcss/src/index.js | 52 +-
packages/plugin-renderer-lit/package.json | 10 +-
.../src/ssr-route-worker-lit.js | 6 +-
.../{build.default => serve.default}/artists.json | 0
.../greenwood.config.js | 0
.../{build.default => serve.default}/package.json | 0
.../serve.default.spec.js} | 5 +-
.../cases/serve.default/src/components/footer.js | 49 ++
.../src/components/greeting.js | 0
.../src/pages/artists.js | 0
.../src/pages/users.js | 0
.../src/templates/app.html | 0
packages/plugin-renderer-puppeteer/package.json | 9 +-
.../src/plugins/resource.js | 17 +-
packages/plugin-typescript/README.md | 8 +-
packages/plugin-typescript/package.json | 9 +-
packages/plugin-typescript/src/index.js | 40 +-
.../test/cases/default/default.spec.js | 2 +-
.../cases/develop.default/develop.default.spec.js | 2 +-
test/smoke-test.js | 4 +-
test/test-loader.js | 10 +-
yarn.lock | 672 ++++++++++++---------
236 files changed, 5495 insertions(+), 2829 deletions(-)