diff --git a/.c8rc.json b/.c8rc.json index 93f8eac6e..fe91f7331 100644 --- a/.c8rc.json +++ b/.c8rc.json @@ -18,10 +18,10 @@ "checkCoverage": true, - "statements": 85, + "statements": 80, "branches": 85, "functions": 90, - "lines": 85, + "lines": 80, "watermarks": { "statements": [75, 85], diff --git a/README.md b/README.md index fb9fe5bcf..86d3a5e93 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/) ## Overview -Greenwood is a modern and performant static site generator supporting Web Component based development. For more information about how to get started, read our docs, or learn more about the project and how it works, please visit our [website](https://www.greenwoodjs.io/). +Greenwood is a framework focused on supporting modern web standards and development to help you build your next project. For more information about how to get started, read our docs, or learn more about the project and how it works, please visit our [website](https://www.greenwoodjs.io/). Features: - ⚡ [No bundle development](https://www.greenwoodjs.io/about/how-it-works/). Pages are built on the fly. @@ -16,7 +16,7 @@ Features: - 🚫 No JavaScript by default. - 📖 Prerendering support for Web Components. - ⚒️ Extensible via [plugins](https://www.greenwoodjs.io/plugins/). -- ⚙️ Supports [SSG, MPA, and SPA](https://www.greenwoodjs.io/docs/configuration/#mode). ([SSR support](https://github.com/ProjectEvergreen/greenwood/discussions/576) coming soon!) +- ⚙️ Supports [SSG, MPA, SPA, and SSR* (or a hybrid!) project types](https://www.greenwoodjs.io/docs/configuration/#mode). > Greenwood is currently working towards a [1.0 release](https://github.com/ProjectEvergreen/greenwood/milestone/3) with our recent [`v0.10.0`](https://github.com/ProjectEvergreen/greenwood/releases/tag/v0.10.0) introducing some exciting new changes and concepts to the project. If you're interested in learning more about the web and web development (at any skill level!), or interested in checking out our high level roadmap and how Greenwood got where it is today, please see our [Open Beta + RFC Google doc](https://docs.google.com/document/d/1MwDkszKvq81QgIYa8utJgyUgSpLZQx9eKCWjIikvfHU/). We would love to have your help making Greenwood! ✌️ @@ -49,7 +49,7 @@ Then in your _package.json_, add the `type` field and `scripts` for the CLI, lik - `greenwood build`: Generates a production build of your project - `greenwood develop`: Starts a local development server for your project -- `greenwood serve`: Generates a production build of the project and serves it locally on a simple web server. +- `greenwood serve`: Generates a production build of your project and runs it on a NodeJS based web server ## Documentation All of our documentation is on our [website](https://www.greenwoodjs.io/) (which itself is built by Greenwood!). See our website documentation to learn more about: @@ -63,7 +63,7 @@ All of our documentation is on our [website](https://www.greenwoodjs.io/) (which We would love your [contribution](.github/CONTRIBUTING.md) to Greenwood! Please check out our issue tracker for "good first issue" labels or feel to reach out to us on [Slack](https://join.slack.com/t/thegreenhouseio/shared_invite/enQtMzcyMzE2Mjk1MjgwLTU5YmM1MDJiMTg0ODk4MjA4NzUwNWFmZmMxNDY5MTcwM2I0MjYxN2VhOTEwNDU2YWQwOWQzZmY1YzY4MWRlOGI) in the room _"Greenwood"_ or on [Twitter](https://twitter.com/PrjEvergreen). ## Built With Greenwood -| Site | Repo | Project Details | +| Site | Repo | Project Details | |---|---|---| | [The Greenhouse I/O](https://www.thegreenhouse.io/) | [thegreenhouseio/www.thegreenhouse.io](https://github.com/thegreenhouseio/www.thegreenhouse.io) | Personal portfolio / blog website for @thescientist13 (Greenwood maintainer). | | [Contributary](https://www.contributary.community/) | [ContributaryCommunity/www.contributary.community](https://github.com/ContributaryCommunity/www.contributary.community) | A website (SPA) for browsing open source projects that are looking for contributions. | diff --git a/greenwood.config.js b/greenwood.config.js index 2887ac537..1df102cac 100644 --- a/greenwood.config.js +++ b/greenwood.config.js @@ -7,13 +7,13 @@ import { greenwoodPluginPostCss } from '@greenwood/plugin-postcss'; import rollupPluginAnalyzer from 'rollup-plugin-analyzer'; import { fileURLToPath, URL } from 'url'; -const META_DESCRIPTION = 'A modern and performant static site generator supporting Web Component based development'; +const META_DESCRIPTION = 'A modern framework focused on web standards to help you build your next project.'; const FAVICON_HREF = '/favicon.ico'; export default { workspace: fileURLToPath(new URL('./www', import.meta.url)), - mode: 'mpa', optimization: 'inline', + staticRouter: true, title: 'Greenwood', meta: [ { name: 'description', content: META_DESCRIPTION }, diff --git a/lerna.json b/lerna.json index 898114477..c6793ca00 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.22.1", + "version": "0.23.0-alpha.1", "packages": [ "packages/*", "www" diff --git a/package.json b/package.json index 0142c94af..2ea9be76c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "greenwood", "private": true, - "description": "A modern and performant static site generator supporting Web Component based development.", + "description": "A modern framework focused on web standards to help you build your next project.", "repository": "https://github.com/ProjectEvergreen/greenwood", "author": "Owen Buckley ", "license": "MIT", @@ -27,6 +27,9 @@ "lint:css": "stylelint \"./www/**/*.js\", \"./www/**/*.css\"", "lint": "ls-lint && yarn lint:js && yarn lint:ts && yarn lint:css" }, + "resolutions": { + "lit": "^2.1.1" + }, "devDependencies": { "@ls-lint/ls-lint": "^1.10.0", "@typescript-eslint/eslint-plugin": "^4.28.2", diff --git a/packages/cli/package.json b/packages/cli/package.json index ad0470e3d..0eb347951 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/cli", - "version": "0.22.1", + "version": "0.23.0-alpha.1", "description": "Greenwood CLI.", "type": "module", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli", diff --git a/packages/cli/src/commands/build.js b/packages/cli/src/commands/build.js index 87d72d9db..8935d659f 100644 --- a/packages/cli/src/commands/build.js +++ b/packages/cli/src/commands/build.js @@ -1,6 +1,6 @@ import { bundleCompilation } from '../lifecycles/bundle.js'; import { copyAssets } from '../lifecycles/copy.js'; -import { devServer } from '../lifecycles/serve.js'; +import { getDevServer } from '../lifecycles/serve.js'; import fs from 'fs'; import { generateCompilation } from '../lifecycles/compile.js'; import { preRenderCompilation, staticRenderCompilation } from '../lifecycles/prerender.js'; @@ -23,7 +23,7 @@ const runProductionBuild = async () => { if (prerender) { await new Promise(async (resolve, reject) => { try { - (await devServer(compilation)).listen(port, async () => { + (await getDevServer(compilation)).listen(port, async () => { console.info(`Started local development server at localhost:${port}`); const servers = [...compilation.config.plugins.filter((plugin) => { diff --git a/packages/cli/src/commands/develop.js b/packages/cli/src/commands/develop.js index 311a64bde..f085ed39e 100644 --- a/packages/cli/src/commands/develop.js +++ b/packages/cli/src/commands/develop.js @@ -1,6 +1,6 @@ import { generateCompilation } from '../lifecycles/compile.js'; import { ServerInterface } from '../lib/server-interface.js'; -import { devServer } from '../lifecycles/serve.js'; +import { getDevServer } from '../lifecycles/serve.js'; const runDevServer = async () => { @@ -10,7 +10,7 @@ const runDevServer = async () => { const compilation = await generateCompilation(); const { port } = compilation.config.devServer; - (await devServer(compilation)).listen(port, () => { + (await getDevServer(compilation)).listen(port, () => { console.info(`Started local development server at localhost:${port}`); diff --git a/packages/cli/src/commands/serve.js b/packages/cli/src/commands/serve.js index ce512e232..b9ac9ef99 100644 --- a/packages/cli/src/commands/serve.js +++ b/packages/cli/src/commands/serve.js @@ -1,5 +1,5 @@ import { generateCompilation } from '../lifecycles/compile.js'; -import { prodServer } from '../lifecycles/serve.js'; +import { getStaticServer, getHybridServer } from '../lifecycles/serve.js'; const runProdServer = async () => { @@ -7,10 +7,12 @@ const runProdServer = async () => { try { const compilation = await generateCompilation(); - const port = 8080; - - (await prodServer(compilation)).listen(port, () => { - console.info(`Started production test server at localhost:${port}`); + const port = compilation.config.port; + const hasRoutes = compilation.graph.find(page => page.isSSR); + const server = hasRoutes ? getHybridServer : getStaticServer; + + (await server(compilation)).listen(port, () => { + console.info(`Started server at localhost:${port}`); }); } catch (err) { reject(err); diff --git a/packages/cli/src/lib/browser.js b/packages/cli/src/lib/browser.js index 2dc65458e..8092a4b87 100644 --- a/packages/cli/src/lib/browser.js +++ b/packages/cli/src/lib/browser.js @@ -72,8 +72,6 @@ class BrowserRunner { // Serialize page. const content = await page.content(); - // console.debug('content????', content); - await page.close(); return content; diff --git a/packages/cli/src/lib/resource-interface.js b/packages/cli/src/lib/resource-interface.js index e1e10df89..bebb96a57 100644 --- a/packages/cli/src/lib/resource-interface.js +++ b/packages/cli/src/lib/resource-interface.js @@ -79,13 +79,13 @@ class ResourceInterface { // ex: add a "banner" to all .js files with a timestamp of the build, or minifying files // return true | false // eslint-disable-next-line no-unused-vars - async shouldOptimize(url, body) { + async shouldOptimize(url, body, headers) { return Promise.resolve(false); } // return the new body // eslint-disable-next-line no-unused-vars - async optimize (url, body) { + async optimize (url, body, headers) { return Promise.resolve(body); } } diff --git a/packages/cli/src/lib/ssr-route-worker.js b/packages/cli/src/lib/ssr-route-worker.js new file mode 100644 index 000000000..9f859575b --- /dev/null +++ b/packages/cli/src/lib/ssr-route-worker.js @@ -0,0 +1,29 @@ +// https://github.com/nodejs/modules/issues/307#issuecomment-858729422 +import { pathToFileURL } from 'url'; +import { workerData, parentPort } from 'worker_threads'; + +async function executeRouteModule({ modulePath, compilation, route, label, id }) { + const { getTemplate = null, getBody = null, getFrontmatter = null } = await import(pathToFileURL(modulePath)).then(module => module); + const parsedCompilation = JSON.parse(compilation); + const data = { + template: null, + body: null, + frontmatter: null + }; + + if (getTemplate) { + data.template = await getTemplate(parsedCompilation, route); + } + + if (getBody) { + data.body = await getBody(parsedCompilation, route); + } + + if (getFrontmatter) { + data.frontmatter = await getFrontmatter(parsedCompilation, route, label, id); + } + + parentPort.postMessage(data); +} + +executeRouteModule(workerData); \ No newline at end of file diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js index 7ee0dad60..3326d9df2 100644 --- a/packages/cli/src/lifecycles/bundle.js +++ b/packages/cli/src/lifecycles/bundle.js @@ -5,8 +5,9 @@ const bundleCompilation = async (compilation) => { return new Promise(async (resolve, reject) => { try { - // https://rollupjs.org/guide/en/#differences-to-the-javascript-api + compilation.graph = compilation.graph.filter(page => !page.isSSR); + // https://rollupjs.org/guide/en/#differences-to-the-javascript-api if (compilation.graph.length > 0) { const rollupConfigs = await getRollupConfig(compilation); const bundle = await rollup(rollupConfigs[0]); diff --git a/packages/cli/src/lifecycles/config.js b/packages/cli/src/lifecycles/config.js index bcafb42c0..1154fb447 100644 --- a/packages/cli/src/lifecycles/config.js +++ b/packages/cli/src/lifecycles/config.js @@ -7,7 +7,8 @@ import { fileURLToPath, pathToFileURL, URL } from 'url'; const greenwoodPluginsBasePath = fileURLToPath(new URL('../plugins', import.meta.url)); const greenwoodPlugins = (await Promise.all([ - path.join(greenwoodPluginsBasePath, 'copy'), + path.join(greenwoodPluginsBasePath, 'copy'), + path.join(greenwoodPluginsBasePath, 'renderer'), path.join(greenwoodPluginsBasePath, 'resource'), path.join(greenwoodPluginsBasePath, 'server') ].map(async (pluginDirectory) => { @@ -29,9 +30,8 @@ const greenwoodPlugins = (await Promise.all([ }; }); -const modes = ['ssg', 'mpa', 'spa']; const optimizations = ['default', 'none', 'static', 'inline']; -const pluginTypes = ['copy', 'context', 'resource', 'rollup', 'server', 'source']; +const pluginTypes = ['copy', 'context', 'resource', 'rollup', 'server', 'source', 'renderer']; const defaultConfig = { workspace: path.join(process.cwd(), 'src'), devServer: { @@ -39,8 +39,9 @@ const defaultConfig = { port: 1984, extensions: [] }, - mode: modes[0], + port: 8080, optimization: optimizations[0], + interpolateFrontmatter: false, title: 'My App', meta: [], plugins: greenwoodPlugins, @@ -56,10 +57,10 @@ const readAndMergeConfig = async() => { try { // deep clone of default config let customConfig = Object.assign({}, defaultConfig); - + if (fs.existsSync(path.join(process.cwd(), 'greenwood.config.js'))) { const userCfgFile = (await import(pathToFileURL(path.join(process.cwd(), 'greenwood.config.js')))).default; - const { workspace, devServer, title, markdown, meta, mode, optimization, plugins, prerender, pagesDirectory, templatesDirectory } = userCfgFile; + const { workspace, devServer, title, markdown, meta, optimization, plugins, port, prerender, staticRouter, pagesDirectory, templatesDirectory, interpolateFrontmatter } = userCfgFile; // workspace validation if (workspace) { @@ -82,7 +83,7 @@ const readAndMergeConfig = async() => { 'common issues to check might be: \n' + '- typo in your workspace directory name, or in greenwood.config.js \n' + '- if using relative paths, make sure your workspace is in the same cwd as _greenwood.config.js_ \n' + - '- consider using an absolute path, e.g. path.join(__dirname, \'my\', \'custom\', \'path\') // <__dirname>/my/custom/path/ '); + '- consider using an absolute path, e.g. new URL(\'/your/relative/path/\', import.meta.url)'); } } @@ -97,18 +98,19 @@ const readAndMergeConfig = async() => { customConfig.meta = meta; } - if (typeof mode === 'string' && modes.indexOf(mode.toLowerCase()) >= 0) { - customConfig.mode = mode; - } else if (mode) { - reject(`Error: provided mode "${mode}" is not supported. Please use one of: ${modes.join(', ')}.`); - } - if (typeof optimization === 'string' && optimizations.indexOf(optimization.toLowerCase()) >= 0) { customConfig.optimization = optimization; } else if (optimization) { reject(`Error: provided optimization "${optimization}" is not supported. Please use one of: ${optimizations.join(', ')}.`); } + if (interpolateFrontmatter) { + if (typeof interpolateFrontmatter !== 'boolean') { + reject('Error: greenwood.config.js interpolateFrontmatter must be a boolean'); + } + customConfig.interpolateFrontmatter = interpolateFrontmatter; + } + if (plugins && plugins.length > 0) { plugins.forEach(plugin => { if (!plugin.type || pluginTypes.indexOf(plugin.type) < 0) { @@ -128,6 +130,13 @@ const readAndMergeConfig = async() => { } }); + // if user provides a custom renderer, replace ours with theirs + if (plugins.filter(plugin => plugin.type === 'renderer').length === 1) { + customConfig.plugins = customConfig.plugins.filter((plugin) => { + return plugin.type !== 'renderer'; + }); + } + customConfig.plugins = customConfig.plugins.concat(plugins); } @@ -168,18 +177,14 @@ const readAndMergeConfig = async() => { customConfig.markdown.settings = markdown.settings ? markdown.settings : {}; } - if (prerender !== undefined) { - if (typeof prerender === 'boolean') { - customConfig.prerender = prerender; + if (port) { + // eslint-disable-next-line max-depth + if (!Number.isInteger(port)) { + reject(`Error: greenwood.config.js port must be an integer. Passed value was: ${port}`); } else { - reject(`Error: greenwood.config.js prerender must be a boolean; true or false. Passed value was typeof: ${typeof prerender}`); + customConfig.port = port; } } - - // SPA should _not_ prerender if user has specified prerender should be true - if (prerender === undefined && mode === 'spa') { - customConfig.prerender = false; - } if (pagesDirectory && typeof pagesDirectory === 'string') { customConfig.pagesDirectory = pagesDirectory; @@ -192,6 +197,27 @@ const readAndMergeConfig = async() => { } else if (templatesDirectory) { reject(`Error: provided templatesDirectory "${templatesDirectory}" is not supported. Please make sure to pass something like 'layouts/'`); } + + if (prerender !== undefined) { + if (typeof prerender === 'boolean') { + customConfig.prerender = prerender; + } else { + reject(`Error: greenwood.config.js prerender must be a boolean; true or false. Passed value was typeof: ${typeof prerender}`); + } + } + + // SPA should _not_ prerender unless if user has specified prerender should be true + if (prerender === undefined && fs.existsSync(path.join(customConfig.workspace, 'index.html'))) { + customConfig.prerender = false; + } + + if (staticRouter !== undefined) { + if (typeof staticRouter === 'boolean') { + customConfig.staticRouter = staticRouter; + } else { + reject(`Error: greenwood.config.js staticRouter must be a boolean; true or false. Passed value was typeof: ${typeof staticRouter}`); + } + } } resolve({ ...defaultConfig, ...customConfig }); diff --git a/packages/cli/src/lifecycles/graph.js b/packages/cli/src/lifecycles/graph.js index 93b06fee8..de46062e7 100644 --- a/packages/cli/src/lifecycles/graph.js +++ b/packages/cli/src/lifecycles/graph.js @@ -1,14 +1,15 @@ -/* eslint-disable complexity */ +/* eslint-disable complexity, max-depth */ import fs from 'fs'; import fm from 'front-matter'; import path from 'path'; import toc from 'markdown-toc'; +import { Worker } from 'worker_threads'; const generateGraph = async (compilation) => { return new Promise(async (resolve, reject) => { try { - const { context, config } = compilation; + const { context } = compilation; const { pagesDir, userWorkspace } = context; let graph = [{ outputPath: 'index.html', @@ -21,89 +22,162 @@ const generateGraph = async (compilation) => { imports: [] }]; - const walkDirectoryForPages = function(directory, pages = []) { - - fs.readdirSync(directory).forEach((filename) => { + const walkDirectoryForPages = async function(directory, pages = []) { + const files = fs.readdirSync(directory); + + for (const filename of files) { const fullPath = path.normalize(`${directory}${path.sep}${filename}`); if (fs.statSync(fullPath).isDirectory()) { - pages = walkDirectoryForPages(fullPath, pages); + pages = await walkDirectoryForPages(fullPath, pages); } else { - const fileContents = fs.readFileSync(fullPath, 'utf8'); - const { attributes } = fm(fileContents); + const extension = path.extname(filename); + const isStatic = extension === '.md' || extension === '.html'; const relativePagePath = fullPath.substring(pagesDir.length - 1, fullPath.length); const relativeWorkspacePath = directory.replace(process.cwd(), '').replace(path.sep, ''); - const template = attributes.template || 'page'; - const title = attributes.title || compilation.config.title || ''; - const id = attributes.label || filename.split(path.sep)[filename.split(path.sep).length - 1].replace('.md', '').replace('.html', ''); - const imports = attributes.imports || []; - const label = id.split('-') - .map((idPart) => { - return `${idPart.charAt(0).toUpperCase()}${idPart.substring(1)}`; - }).join(' '); let route = relativePagePath - .replace('.md', '') - .replace('.html', '') + .replace(extension, '') .replace(/\\/g, '/'); - - /* - * check if additional nested directories exist to correctly determine route (minus filename) - * examples: - * - pages/index.{html,md} -> / - * - pages/about.{html,md} -> /about/ - * - pages/blog/index.{html,md} -> /blog/ - * - pages/blog/some-post.{html,md} -> /blog/some-post/ - */ - if (relativePagePath.lastIndexOf(path.sep) > 0) { - // https://github.com/ProjectEvergreen/greenwood/issues/455 - route = id === 'index' || route.replace('/index', '') === `/${id}` - ? route.replace('index', '') - : `${route}/`; - } else { - route = route === '/index' - ? '/' - : `${route}/`; - } + let template = 'page'; + let title = ''; + let imports = []; + let label = ''; + let id; + let customData = {}; + let filePath; - // prune "reserved" attributes that are supported by Greenwood - // https://www.greenwoodjs.io/docs/front-matter - const customData = attributes; - - delete customData.label; - delete customData.imports; - delete customData.title; - delete customData.template; - - /* Menu Query - * Custom front matter - Variable Definitions - * -------------------------------------------------- - * menu: the name of the menu in which this item can be listed and queried - * index: the index of this list item within a menu - * linkheadings: flag to tell us where to add page's table of contents as menu items - * tableOfContents: json object containing page's table of contents(list of headings) - */ - // set specific menu to place this page - customData.menu = customData.menu || ''; + if (isStatic) { + const fileContents = fs.readFileSync(fullPath, 'utf8'); + const { attributes } = fm(fileContents); + + template = attributes.template || 'page'; + title = attributes.title || compilation.config.title || ''; + id = attributes.label || filename.split(path.sep)[filename.split(path.sep).length - 1].replace(extension, ''); + imports = attributes.imports || []; + label = id.split('-') + .map((idPart) => { + return `${idPart.charAt(0).toUpperCase()}${idPart.substring(1)}`; + }).join(' '); + + /* + * check if additional nested directories exist to correctly determine route (minus filename) + * examples: + * - pages/index.{html,md} -> / + * - pages/about.{html,md} -> /about/ + * - pages/blog/index.{html,md} -> /blog/ + * - pages/blog/some-post.{html,md} -> /blog/some-post/ + */ + if (relativePagePath.lastIndexOf(path.sep) > 0) { + // https://github.com/ProjectEvergreen/greenwood/issues/455 + route = id === 'index' || route.replace('/index', '') === `/${id}` + ? route.replace('index', '') + : `${route}/`; + } else { + route = route === '/index' + ? '/' + : `${route}/`; + } + + filePath = route === '/' || relativePagePath.lastIndexOf(path.sep) === 0 + ? `${relativeWorkspacePath}${filename}` + : `${relativeWorkspacePath}${path.sep}${filename}`, + + // prune "reserved" attributes that are supported by Greenwood + // https://www.greenwoodjs.io/docs/front-matter + customData = attributes; + + delete customData.label; + delete customData.imports; + delete customData.title; + delete customData.template; - // set specific index list priority of this item within a menu - customData.index = customData.index || ''; + /* Menu Query + * Custom front matter - Variable Definitions + * -------------------------------------------------- + * menu: the name of the menu in which this item can be listed and queried + * index: the index of this list item within a menu + * linkheadings: flag to tell us where to add page's table of contents as menu items + * tableOfContents: json object containing page's table of contents(list of headings) + */ + // set specific menu to place this page + customData.menu = customData.menu || ''; - // set flag whether to gather a list of headings on a page as menu items - customData.linkheadings = customData.linkheadings || 0; - customData.tableOfContents = []; + // set specific index list priority of this item within a menu + customData.index = customData.index || ''; - if (customData.linkheadings > 0) { - // parse markdown for table of contents and output to json - customData.tableOfContents = toc(fileContents).json; - customData.tableOfContents.shift(); + // set flag whether to gather a list of headings on a page as menu items + customData.linkheadings = customData.linkheadings || 0; + customData.tableOfContents = []; - // parse table of contents for only the pages user wants linked - if (customData.tableOfContents.length > 0 && customData.linkheadings > 0) { - customData.tableOfContents = customData.tableOfContents - .filter((item) => item.lvl === customData.linkheadings); + if (customData.linkheadings > 0) { + // parse markdown for table of contents and output to json + customData.tableOfContents = toc(fileContents).json; + customData.tableOfContents.shift(); + + // parse table of contents for only the pages user wants linked + if (customData.tableOfContents.length > 0 && customData.linkheadings > 0) { + customData.tableOfContents = customData.tableOfContents + .filter((item) => item.lvl === customData.linkheadings); + } } + /* ---------End Menu Query-------------------- */ + } else { + const routeWorkerUrl = compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider().workerUrl; + // const relativePagePath = fullPath.substring(pagesDir.length - 1, fullPath.length); + id = filename.split(path.sep)[filename.split(path.sep).length - 1].replace(extension, ''); + label = id.split('-') + .map((idPart) => { + return `${idPart.charAt(0).toUpperCase()}${idPart.substring(1)}`; + }).join(' '); + route = relativePagePath + .replace(extension, '') + .replace(/\\/g, '/') + .concat('/'); + title = `${compilation.config.title} - ${label}`; + let ssrFrontmatter; + + filePath = route; + + await new Promise((resolve, reject) => { + const worker = new Worker(routeWorkerUrl, { + workerData: { + modulePath: fullPath, + compilation: JSON.stringify(compilation), + route + } + }); + worker.on('message', (result) => { + if (result.frontmatter) { + ssrFrontmatter = result.frontmatter; + } + resolve(); + }); + worker.on('error', reject); + worker.on('exit', (code) => { + if (code !== 0) { + reject(new Error(`Worker stopped with exit code ${code}`)); + } + }); + }); + + if (ssrFrontmatter) { + template = ssrFrontmatter.template || template; + title = ssrFrontmatter.title || title; + imports = ssrFrontmatter.imports || imports; + customData = ssrFrontmatter.data || customData; + + /* Menu Query + * Custom front matter - Variable Definitions + * -------------------------------------------------- + * menu: the name of the menu in which this item can be listed and queried + * index: the index of this list item within a menu + * linkheadings: flag to tell us where to add page's table of contents as menu items + * tableOfContents: json object containing page's table of contents(list of headings) + */ + customData.menu = ssrFrontmatter.menu || ''; + customData.index = ssrFrontmatter.index || ''; + } } - /* ---------End Menu Query-------------------- */ /* * Graph Properties (per page) @@ -128,31 +202,29 @@ const generateGraph = async (compilation) => { outputPath: route === '/404/' ? '404.html' : `${route}index.html`, - path: route === '/' || relativePagePath.lastIndexOf(path.sep) === 0 - ? `${relativeWorkspacePath}${filename}` - : `${relativeWorkspacePath}${path.sep}${filename}`, + path: filePath, route, template, - title + title, + isSSR: !isStatic }); } - }); + } return pages; }; console.debug('building from local sources...'); - if (config.mode === 'spa') { + if (fs.existsSync(path.join(userWorkspace, 'index.html'))) { // SPA graph = [{ ...graph[0], - path: `${userWorkspace}${path.sep}index.html` + path: `${userWorkspace}${path.sep}index.html`, + isSPA: true }]; } else { const oldGraph = graph[0]; - - graph = fs.existsSync(pagesDir) - ? walkDirectoryForPages(pagesDir) - : graph; + + graph = fs.existsSync(pagesDir) ? await walkDirectoryForPages(pagesDir) : graph; const has404Page = graph.filter(page => page.route === '/404/').length === 1; diff --git a/packages/cli/src/lifecycles/prerender.js b/packages/cli/src/lifecycles/prerender.js index 17ab14449..5a3ba88d4 100644 --- a/packages/cli/src/lifecycles/prerender.js +++ b/packages/cli/src/lifecycles/prerender.js @@ -1,6 +1,9 @@ import { BrowserRunner } from '../lib/browser.js'; import fs from 'fs'; +import htmlparser from 'node-html-parser'; import path from 'path'; +import { Worker } from 'worker_threads'; +import { pathToFileURL } from 'url'; async function interceptPage(compilation, contents, route) { const headers = { @@ -22,7 +25,7 @@ async function interceptPage(compilation, contents, route) { return shouldIntercept ? resource.intercept(route, html, headers) : htmlPromise; - }, Promise.resolve(contents)); + }, Promise.resolve({ body: contents })); return htmlIntercepted; } @@ -50,8 +53,8 @@ async function optimizePage(compilation, contents, route, outputPath, outputDir) recursive: true }); } - - await fs.promises.writeFile(path.join(outputDir, outputPath), htmlOptimized); + + return htmlOptimized; } async function preRenderCompilation(compilation) { @@ -68,7 +71,8 @@ async function preRenderCompilation(compilation) { .then(async (indexHtml) => { console.info(`prerendering complete for page ${route}.`); - await optimizePage(compilation, indexHtml, route, outputPath, outputDir); + const html = await optimizePage(compilation, indexHtml, route, outputPath, outputDir); + await fs.promises.writeFile(path.join(outputDir, outputPath), html); }); })); } catch (e) { @@ -100,18 +104,85 @@ async function preRenderCompilation(compilation) { return new Promise(async (resolve, reject) => { try { - const pages = compilation.graph; + const pages = compilation.graph.filter(page => !page.isSSR); const port = compilation.config.devServer.port; const outputDir = compilation.context.scratchDir; const serverAddress = `http://127.0.0.1:${port}`; + const customPrerender = (compilation.config.plugins.filter(plugin => plugin.type === 'renderer' && !plugin.isGreenwoodDefaultPlugin) || []).length === 1 + ? compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider(compilation) + : {}; - console.info(`Prerendering pages at ${serverAddress}`); console.debug('pages to render', `\n ${pages.map(page => page.route).join('\n ')}`); - await runBrowser(serverAddress, pages, outputDir); + if (customPrerender.prerender) { + for (const page of pages) { + const { outputPath, route } = page; + const outputPathDir = path.join(outputDir, route); + const htmlResource = compilation.config.plugins.filter((plugin) => { + return plugin.name === 'plugin-standard-html'; + }).map((plugin) => { + return plugin.provider(compilation); + })[0]; + let html; + + html = (await htmlResource.serve(page.route)).body; + html = (await interceptPage(compilation, html, route)).body; + + const root = htmlparser.parse(html, { + script: true, + style: true + }); + + const headScripts = root.querySelectorAll('script') + .filter(script => { + return script.getAttribute('type') === 'module' + && script.getAttribute('src') && script.getAttribute('src').indexOf('http') < 0; + }).map(script => { + return pathToFileURL(path.join(compilation.context.userWorkspace, script.getAttribute('src').replace(/\.\.\//g, '').replace('./', ''))); + }); + + await new Promise((resolve, reject) => { + const worker = new Worker(customPrerender.workerUrl, { + workerData: { + modulePath: null, + compilation: JSON.stringify(compilation), + route, + prerender: true, + htmlContents: html, + scripts: JSON.stringify(headScripts) + } + }); + worker.on('message', (result) => { + if (result.html) { + html = result.html; + } + resolve(); + }); + worker.on('error', reject); + worker.on('exit', (code) => { + if (code !== 0) { + reject(new Error(`Worker stopped with exit code ${code}`)); + } + }); + }); + + html = await optimizePage(compilation, html, route, outputPath, outputDir); + + if (!fs.existsSync(outputPathDir)) { + fs.mkdirSync(outputPathDir, { + recursive: true + }); + } + + await fs.promises.writeFile(path.join(outputDir, outputPath), html); + } + } else { + console.info(`Prerendering pages at ${serverAddress}`); + await runBrowser(serverAddress, pages, outputDir); + browserRunner.close(); + } console.info('done prerendering all pages'); - browserRunner.close(); resolve(); } catch (err) { @@ -121,7 +192,7 @@ async function preRenderCompilation(compilation) { } async function staticRenderCompilation(compilation) { - const pages = compilation.graph; + const pages = compilation.graph.filter(page => !page.isSSR); const scratchDir = compilation.context.scratchDir; const htmlResource = compilation.config.plugins.filter((plugin) => { return plugin.name === 'plugin-standard-html'; @@ -133,11 +204,12 @@ async function staticRenderCompilation(compilation) { await Promise.all(pages.map(async (page) => { const { route, outputPath } = page; - let response = await htmlResource.serve(route); + let html = (await htmlResource.serve(route)).body; - response = await interceptPage(compilation, response, route); + html = (await interceptPage(compilation, html, route)).body; + html = await optimizePage(compilation, html, route, outputPath, scratchDir); - await optimizePage(compilation, response.body, route, outputPath, scratchDir); + await fs.promises.writeFile(path.join(scratchDir, outputPath), html); return Promise.resolve(); })); diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js index 094a1da7d..d132e3874 100644 --- a/packages/cli/src/lifecycles/serve.js +++ b/packages/cli/src/lifecycles/serve.js @@ -1,9 +1,12 @@ +import { BrowserRunner } from '../lib/browser.js'; import fs from 'fs'; import path from 'path'; import Koa from 'koa'; import { ResourceInterface } from '../lib/resource-interface.js'; +import { getRollupConfig } from '../config/rollup.config.js'; +import { rollup } from 'rollup'; -const getDevServer = async(compilation) => { +async function getDevServer(compilation) { const app = new Koa(); const compilationCopy = Object.assign({}, compilation); const resources = [ @@ -120,9 +123,9 @@ const getDevServer = async(compilation) => { }); return Promise.resolve(app); -}; +} -const getProdServer = async(compilation) => { +async function getStaticServer(compilation, composable) { const app = new Koa(); const standardResources = compilation.config.plugins.filter((plugin) => { // html is intentionally omitted @@ -136,20 +139,23 @@ const getProdServer = async(compilation) => { }); app.use(async (ctx, next) => { - const { outputDir } = compilation.context; - const { mode } = compilation.config; + const { outputDir, userWorkspace } = compilation.context; const url = ctx.request.url.replace(/\?(.*)/, ''); // get rid of things like query string parameters + // only handle static output routes, eg. public/about.html if (url.endsWith('/') || url.endsWith('.html')) { - const barePath = mode === 'spa' + const barePath = fs.existsSync(path.join(userWorkspace, 'index.html')) // SPA ? 'index.html' : url.endsWith('/') ? path.join(url, 'index.html') : url; - const contents = await fs.promises.readFile(path.join(outputDir, barePath), 'utf-8'); - - ctx.set('content-type', 'text/html'); - ctx.body = contents; + + if (fs.existsSync(path.join(outputDir, barePath))) { + const contents = await fs.promises.readFile(path.join(outputDir, barePath), 'utf-8'); + + ctx.set('content-type', 'text/html'); + ctx.body = contents; + } } await next(); @@ -173,7 +179,7 @@ const getProdServer = async(compilation) => { await next(); }); - app.use(async (ctx) => { + app.use(async (ctx, next) => { const responseAccumulator = { body: ctx.body, contentType: ctx.response.header['content-type'] @@ -206,12 +212,103 @@ const getProdServer = async(compilation) => { ctx.set('content-type', reducedResponse.contentType); ctx.body = reducedResponse.body; + + if (composable) { + await next(); + } }); - return Promise.resolve(app); -}; + return app; +} + +async function getHybridServer(compilation) { + const app = await getStaticServer(compilation, true); + const { prerender } = compilation.config; + let browserRunner; + + if (prerender) { + browserRunner = new BrowserRunner(); + + await browserRunner.init(); + } + + app.use(async (ctx) => { + const url = ctx.request.url.replace(/\?(.*)/, ''); // get rid of things like query string parameters + const matchingRoute = compilation.graph.filter((node) => { + return node.route === url; + })[0] || { data: {} }; + + if (matchingRoute.isSSR) { + const headers = { + request: { 'accept': 'text/html', 'content-type': 'text/html' }, + response: { 'content-type': 'text/html' } + }; + const standardHtmlResource = compilation.config.plugins.filter((plugin) => { + return plugin.isGreenwoodDefaultPlugin + && plugin.type === 'resource' + && plugin.name.indexOf('plugin-standard-html') === 0; + }).map((plugin) => { + return plugin.provider(compilation); + })[0]; + let body; + + const interceptResources = compilation.config.plugins.filter((plugin) => { + return plugin.type === 'resource' && !plugin.isGreenwoodDefaultPlugin; + }).map((plugin) => { + return plugin.provider(compilation); + }).filter((provider) => { + return provider.shouldIntercept && provider.intercept; + }); + + body = (await standardHtmlResource.serve(url)).body; + body = (await interceptResources.reduce(async (htmlPromise, resource) => { + const html = (await htmlPromise).body; + const shouldIntercept = await resource.shouldIntercept(url, html, headers); + + return shouldIntercept + ? resource.intercept(url, html, headers) + : htmlPromise; + }, Promise.resolve({ url, body }))).body; + + const optimizeResources = compilation.config.plugins.filter((plugin) => { + return plugin.type === 'resource'; + }).map((plugin) => { + return plugin.provider(compilation); + }).filter((provider) => { + return provider.shouldOptimize && provider.optimize; + }); + + body = await optimizeResources.reduce(async (htmlPromise, resource) => { + const html = await htmlPromise; + const shouldOptimize = await resource.shouldOptimize(url, html, headers); + + return shouldOptimize + ? resource.optimize(url, html, headers) + : Promise.resolve(html); + }, Promise.resolve(body)); + + await fs.promises.mkdir(path.join(compilation.context.scratchDir, url), { recursive: true }); + await fs.promises.writeFile(path.join(compilation.context.scratchDir, url, 'index.html'), body); + + compilation.graph = compilation.graph.filter(page => page.isSSR && page.route === url); + + const rollupConfigs = await getRollupConfig(compilation); + const bundle = await rollup(rollupConfigs[0]); + await bundle.write(rollupConfigs[0].output); + + body = await fs.promises.readFile(path.join(compilation.context.outputDir, url, 'index.html'), 'utf-8'); + + ctx.status = 200; + ctx.set('content-type', 'text/html'); + ctx.body = body; + } + }); + + return app; +} export { - getDevServer as devServer, - getProdServer as prodServer + getDevServer, + getStaticServer, + getHybridServer }; \ No newline at end of file diff --git a/packages/cli/src/plugins/renderer/plugin-renderer-string.js b/packages/cli/src/plugins/renderer/plugin-renderer-string.js new file mode 100644 index 000000000..df120d32a --- /dev/null +++ b/packages/cli/src/plugins/renderer/plugin-renderer-string.js @@ -0,0 +1,11 @@ +const greenwoodPluginRendererString = { + type: 'renderer', + name: 'plugin-renderer-string', + provider: () => { + return { + workerUrl: new URL('../../lib/ssr-route-worker.js', import.meta.url) + }; + } +}; + +export { greenwoodPluginRendererString }; \ No newline at end of file diff --git a/packages/cli/src/plugins/resource/plugin-optimization-mpa.js b/packages/cli/src/plugins/resource/plugin-optimization-mpa.js index 37b5c7f02..99daa7f54 100644 --- a/packages/cli/src/plugins/resource/plugin-optimization-mpa.js +++ b/packages/cli/src/plugins/resource/plugin-optimization-mpa.js @@ -33,8 +33,10 @@ class OptimizationMPAResource extends ResourceInterface { }); } - async shouldOptimize(url) { - return Promise.resolve(url !== '404.html' && path.extname(url) === '.html' && this.compilation.config.mode === 'mpa'); + async shouldOptimize(url, body, headers) { + return Promise.resolve(this.compilation.config.staticRouter + && url !== '404.html' + && (path.extname(url) === '.html' || (headers.request && headers.request['content-type'].indexOf('text/html') >= 0))); } async optimize(url, body) { @@ -47,6 +49,7 @@ class OptimizationMPAResource extends ResourceInterface { .replace(`.greenwood${path.sep}`, ''); const routeTags = this.compilation.graph + .filter(page => !page.isSSR) .filter(page => page.route !== '/404/') .map((page) => { const template = page.filename && path.extname(page.filename) === '.html' diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js index 57106f18d..4f0a23757 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-html.js +++ b/packages/cli/src/plugins/resource/plugin-standard-html.js @@ -1,6 +1,6 @@ -/* eslint-disable complexity */ +/* eslint-disable complexity, max-depth */ /* - * + * * Manages web standard resource related operations for HTML and markdown. * This is a Greenwood default plugin. * @@ -16,7 +16,8 @@ import remarkParse from 'remark-parse'; import remarkRehype from 'remark-rehype'; import { ResourceInterface } from '../../lib/resource-interface.js'; import unified from 'unified'; -import { fileURLToPath, URL } from 'url'; +import { fileURLToPath } from 'url'; +import { Worker } from 'worker_threads'; function getCustomPageTemplates(contextPlugins, templateName) { return contextPlugins @@ -39,7 +40,7 @@ const getPageTemplate = (fullPath, templatesDir, template, contextPlugins = [], ? fs.readFileSync(`${customPluginPageTemplates[0]}/${template}.html`, 'utf-8') : fs.readFileSync(`${templatesDir}/${template}.html`, 'utf-8'); } else if (path.extname(fullPath) === '.html' && fs.existsSync(fullPath)) { - // if the page is already HTML, use that as the template, NOT accounting for 404 pages + // if the page is already HTML, use that as the template, NOT accounting for 404 pages contents = fs.readFileSync(fullPath, 'utf-8'); } else if (customPluginDefaultPageTemplates.length > 0 || (!is404Page && fs.existsSync(`${templatesDir}/page.html`))) { // else look for default page template from the user @@ -267,14 +268,18 @@ const getUserScripts = (contents, context) => { return contents; }; -const getMetaContent = (url, config, contents) => { +const getMetaContent = (url, config, contents, ssrFrontmatter = {}) => { const existingTitleMatch = contents.match(/(.*)<\/title>/); const existingTitleCheck = !!(existingTitleMatch && existingTitleMatch[1] && existingTitleMatch[1] !== ''); const title = existingTitleCheck ? existingTitleMatch[1] - : config.title; - const metaContent = config.meta.map(item => { + : ssrFrontmatter.title + ? ssrFrontmatter.title + : config.title + ? config.title + : ''; + const metaContent = [...config.meta || []].map(item => { let metaHtml = ''; for (const [key, value] of Object.entries(item)) { @@ -285,7 +290,7 @@ const getMetaContent = (url, config, contents) => { ? `${value}${url.replace('/', '')}` : `${value}${url === '/' ? '' : url}` : value; - + metaHtml += ` ${key}="${contextualValue}"`; } @@ -308,7 +313,7 @@ const getMetaContent = (url, config, contents) => { class StandardHtmlResource extends ResourceInterface { constructor(compilation, options) { super(compilation, options); - + this.extensions = ['.html', '.md']; this.contentType = 'text/html'; } @@ -319,7 +324,7 @@ class StandardHtmlResource extends ResourceInterface { async shouldServe(url, headers) { const relativeUrl = this.getRelativeUserworkspaceUrl(url).replace(/\\/g, '/'); // and handle for windows - const isClientSideRoute = this.compilation.config.mode === 'spa' && path.extname(url) === '' && (headers.request.accept || '').indexOf(this.contentType) >= 0; + const isClientSideRoute = this.compilation.graph[0].isSPA && path.extname(url) === '' && (headers.request.accept || '').indexOf(this.contentType) >= 0; const hasMatchingRoute = this.compilation.graph.filter((node) => { return node.route === relativeUrl; }).length === 1; @@ -332,9 +337,10 @@ class StandardHtmlResource extends ResourceInterface { try { const config = Object.assign({}, this.compilation.config); const { pagesDir, userTemplatesDir } = this.compilation.context; - const { mode } = this.compilation.config; + const { interpolateFrontmatter } = this.compilation.config; const relativeUrl = this.getRelativeUserworkspaceUrl(url).replace(/\\/g, '/'); // and handle for windows; - const matchingRoute = mode === 'spa' + const isClientSideRoute = this.compilation.graph[0].isSPA; + const matchingRoute = isClientSideRoute ? this.compilation.graph[0] : this.compilation.graph.filter((node) => { return node.route === relativeUrl; @@ -342,9 +348,13 @@ class StandardHtmlResource extends ResourceInterface { const fullPath = !matchingRoute.external ? matchingRoute.path : ''; const isMarkdownContent = path.extname(fullPath) === '.md'; + let customImports = []; let body = ''; let template = null; - let customImports; + let frontMatter = {}; + let ssrBody; + let ssrTemplate; + let ssrFrontmatter; let processedMarkdown = null; if (matchingRoute.external) { @@ -368,6 +378,7 @@ class StandardHtmlResource extends ResourceInterface { const settings = config.markdown.settings || {}; const fm = frontmatter(markdownContents); + processedMarkdown = await unified() .use(remarkParse, settings) // parse markdown into AST .use(remarkFrontmatter) // extract frontmatter from AST @@ -380,22 +391,67 @@ class StandardHtmlResource extends ResourceInterface { // configure via frontmatter if (fm.attributes) { - const { attributes } = fm; + frontMatter = fm.attributes; - if (attributes.title) { - config.title = `${config.title} - ${attributes.title}`; + if (frontMatter.title) { + config.title = `${config.title} - ${frontMatter.title}`; } - - if (attributes.template) { - template = attributes.template; + + if (frontMatter.template) { + template = frontMatter.template; } - if (attributes.imports) { - customImports = attributes.imports; + if (frontMatter.imports) { + customImports = frontMatter.imports; } } } + if (matchingRoute.isSSR) { + const routeModuleLocation = path.join(pagesDir, matchingRoute.filename); + const routeWorkerUrl = this.compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider().workerUrl; + + await new Promise((resolve, reject) => { + const worker = new Worker(routeWorkerUrl, { + workerData: { + modulePath: routeModuleLocation, + compilation: JSON.stringify(this.compilation), + route: fullPath + } + }); + worker.on('message', (result) => { + if (result.template) { + ssrTemplate = result.template; + } + if (result.body) { + ssrBody = result.body; + } + if (result.frontmatter) { + ssrFrontmatter = result.frontmatter; + + if (ssrFrontmatter.title) { + config.title = `${config.title} - ${ssrFrontmatter.title}`; + } + + if (ssrFrontmatter.template) { + template = ssrFrontmatter.template; + } + + if (ssrFrontmatter.imports) { + customImports = customImports.concat(ssrFrontmatter.imports); + } + } + resolve(); + }); + worker.on('error', reject); + worker.on('exit', (code) => { + if (code !== 0) { + reject(new Error(`Worker stopped with exit code ${code}`)); + } + }); + }); + } + // get context plugins const contextPlugins = this.compilation.config.plugins.filter((plugin) => { return plugin.type === 'context'; @@ -403,16 +459,16 @@ class StandardHtmlResource extends ResourceInterface { return plugin.provider(this.compilation); }); - if (mode === 'spa') { + if (isClientSideRoute) { body = fs.readFileSync(fullPath, 'utf-8'); } else { - body = getPageTemplate(fullPath, userTemplatesDir, template, contextPlugins, pagesDir); + body = ssrTemplate ? ssrTemplate : getPageTemplate(fullPath, userTemplatesDir, template, contextPlugins, pagesDir, ssrTemplate); } - body = getAppTemplate(body, userTemplatesDir, customImports, contextPlugins, config.devServer.hud); + body = getAppTemplate(body, userTemplatesDir, customImports, contextPlugins, config.devServer.hud); body = getUserScripts(body, this.compilation.context); - body = getMetaContent(matchingRoute.route.replace(/\\/g, '/'), config, body); - + body = getMetaContent(matchingRoute.route.replace(/\\/g, '/'), config, body, ssrFrontmatter); + if (processedMarkdown) { const wrappedCustomElementRegex = /<p><[a-zA-Z]*-[a-zA-Z](.*)>(.*)<\/[a-zA-Z]*-[a-zA-Z](.*)><\/p>/g; const ceTest = wrappedCustomElementRegex.test(processedMarkdown.contents); @@ -430,8 +486,18 @@ class StandardHtmlResource extends ResourceInterface { } body = body.replace(/\<content-outlet>(.*)<\/content-outlet>/s, processedMarkdown.contents); + + if (interpolateFrontmatter) { + for (const fm in frontMatter) { + const interpolatedFrontmatter = '\\$\\{globalThis.page.' + fm + '\\}'; + + body = body.replace(new RegExp(interpolatedFrontmatter, 'g'), frontMatter[fm]); + } + } } else if (matchingRoute.external) { body = body.replace(/\<content-outlet>(.*)<\/content-outlet>/s, matchingRoute.body); + } else if (ssrBody) { + body = body.replace(/\<content-outlet>(.*)<\/content-outlet>/s, ssrBody); } // give the user something to see so they know it works, if they have no content @@ -451,8 +517,8 @@ class StandardHtmlResource extends ResourceInterface { }); } - async shouldOptimize(url) { - return Promise.resolve(path.extname(url) === '.html'); + async shouldOptimize(url = '', body, headers = {}) { + return Promise.resolve(path.extname(url) === '.html' || (headers.request && headers.request['content-type'].indexOf('text/html') >= 0)); } async optimize(url, body) { @@ -471,7 +537,7 @@ class StandardHtmlResource extends ResourceInterface { body = body.replace(/\<head>(.*)<\/head>/s, contents.replace(/\$/g, '$$$')); // https://github.com/ProjectEvergreen/greenwood/issues/656); } - + resolve(body); } catch (e) { reject(e); diff --git a/packages/cli/test/cases/build.config.error-mode/build.config.error-mode.spec.js b/packages/cli/test/cases/build.config.error-mode/build.config.error-mode.spec.js deleted file mode 100644 index 62bda0639..000000000 --- a/packages/cli/test/cases/build.config.error-mode/build.config.error-mode.spec.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Use Case - * Run Greenwood build command with a bad value for mode in a custom config. - * - * User Result - * Should throw an error. - * - * User Command - * greenwood build - * - * User Config - * { - * mode: 'lorumipsum' - * } - * - * User Workspace - * Greenwood default - */ -import chai from 'chai'; -import path from 'path'; -import { Runner } from 'gallinago'; -import { fileURLToPath, URL } from 'url'; - -const expect = chai.expect; - -describe('Build Greenwood With: ', function() { - const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); - const outputPath = fileURLToPath(new URL('.', import.meta.url)); - let runner; - - before(function() { - this.context = { - publicDir: path.join(outputPath, 'public') - }; - runner = new Runner(); - }); - - describe('Custom Configuration with a bad value for mode', function() { - it('should throw an error that provided mode is not valid', async function() { - try { - await runner.setup(outputPath); - await runner.runCommand(cliPath, 'build'); - } catch (err) { - expect(err).to.contain('Error: provided mode "loremipsum" is not supported. Please use one of: ssg, mpa, spa.'); - } - }); - }); - -}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.error-mode/greenwood.config.js b/packages/cli/test/cases/build.config.error-mode/greenwood.config.js deleted file mode 100644 index ee0435bb1..000000000 --- a/packages/cli/test/cases/build.config.error-mode/greenwood.config.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - mode: 'loremipsum' -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.error-optimization/build.config.error-optimization.spec.js b/packages/cli/test/cases/build.config.error-optimization/build.config.error-optimization.spec.js index f1cc86cfc..f4d8c1004 100644 --- a/packages/cli/test/cases/build.config.error-optimization/build.config.error-optimization.spec.js +++ b/packages/cli/test/cases/build.config.error-optimization/build.config.error-optimization.spec.js @@ -1,6 +1,6 @@ /* * Use Case - * Run Greenwood build command with a bad value for mode in a custom config. + * Run Greenwood build command with a bad value for optimization in a custom config. * * User Result * Should throw an error. diff --git a/packages/cli/test/cases/build.config.interpolate-frontmatter/build.config.interpolate-frontmatter.spec.js b/packages/cli/test/cases/build.config.interpolate-frontmatter/build.config.interpolate-frontmatter.spec.js new file mode 100644 index 000000000..e4209e148 --- /dev/null +++ b/packages/cli/test/cases/build.config.interpolate-frontmatter/build.config.interpolate-frontmatter.spec.js @@ -0,0 +1,84 @@ +/* + * Use Case + * Run Greenwood with interpolateFrontmatter configuration enabled. + * + * User Result + * Should generate a bare bones Greenwood build with correctly interpolated frontmatter variables in markdown and HTML. + * + * User Command + * greenwood build + * + * User Config + * { + * interpolateFrontmatter: true + * } + * + * User Workspace + * Greenwood default + * src/ + * pages/ + * blog/ + * first-post.md + * templates/ + * blog.html + */ +import { JSDOM } from 'jsdom'; +import path from 'path'; +import chai from 'chai'; +import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +describe('Build Greenwood With: ', function() { + const LABEL = 'Frontmatter Interpolation'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + let runner; + + before(function() { + this.context = { + publicDir: path.join(outputPath, 'public') + }; + runner = new Runner(); + }); + + describe(LABEL, function() { + + before(async function() { + await runner.setup(outputPath, getSetupFiles(outputPath)); + await runner.runCommand(cliPath, 'build'); + }); + + describe('Frontmatter should be interpolated in the correct places', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './blog/first-post/index.html')); + }); + + it('should have the correct value for author <meta> tag in the <head>', function() { + const authorMeta = dom.window.document.querySelector('head meta[name=author]').getAttribute('content'); + + expect(authorMeta).to.be.equal('Owen Buckley'); + }); + + it('should have the correct value for publised in the <h3> tag', function() { + const heading = dom.window.document.querySelector('body h3').textContent; + + expect(heading).to.be.equal('Published: 11/11/2022'); + }); + + it('should have the correct value for authro in the <h4> tag', function() { + const heading = dom.window.document.querySelector('body h4').textContent; + + expect(heading).to.be.equal('Author: Owen Buckley'); + }); + }); + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + }); +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.interpolate-frontmatter/greenwood.config.js b/packages/cli/test/cases/build.config.interpolate-frontmatter/greenwood.config.js new file mode 100644 index 000000000..41bc6bc88 --- /dev/null +++ b/packages/cli/test/cases/build.config.interpolate-frontmatter/greenwood.config.js @@ -0,0 +1,3 @@ +export default { + interpolateFrontmatter: true +}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.interpolate-frontmatter/src/pages/blog/first-post.md b/packages/cli/test/cases/build.config.interpolate-frontmatter/src/pages/blog/first-post.md new file mode 100644 index 000000000..deae1bb80 --- /dev/null +++ b/packages/cli/test/cases/build.config.interpolate-frontmatter/src/pages/blog/first-post.md @@ -0,0 +1,13 @@ +--- +title: Ny First Post +template: blog +published: 11/11/2022 +author: Owen Buckley +--- + +# My First Post + +### Published: ${globalThis.page.published} +#### Author: ${globalThis.page.author} + +Lorum Ipsum. \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.interpolate-frontmatter/src/templates/blog.html b/packages/cli/test/cases/build.config.interpolate-frontmatter/src/templates/blog.html new file mode 100644 index 000000000..58ce8c069 --- /dev/null +++ b/packages/cli/test/cases/build.config.interpolate-frontmatter/src/templates/blog.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <meta name="author" content="${globalThis.page.author}"> + </head> + + <body> + <content-outlet></content-outlet> + </body> + +</html> \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.mode-mpa/greenwood.config.js b/packages/cli/test/cases/build.config.mode-mpa/greenwood.config.js deleted file mode 100644 index f73323893..000000000 --- a/packages/cli/test/cases/build.config.mode-mpa/greenwood.config.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - mode: 'mpa' -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.mode-spa/greenwood.config.js b/packages/cli/test/cases/build.config.mode-spa/greenwood.config.js deleted file mode 100644 index dc5d78556..000000000 --- a/packages/cli/test/cases/build.config.mode-spa/greenwood.config.js +++ /dev/null @@ -1,4 +0,0 @@ -export default { - title: 'this is the wrong title', - mode: 'spa' -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.mode-mpa/build.config.mode-mpa.spec.js b/packages/cli/test/cases/build.config.static-router/build.config.static-router.spec.js similarity index 95% rename from packages/cli/test/cases/build.config.mode-mpa/build.config.mode-mpa.spec.js rename to packages/cli/test/cases/build.config.static-router/build.config.static-router.spec.js index 81edb80bb..577a18316 100644 --- a/packages/cli/test/cases/build.config.mode-mpa/build.config.mode-mpa.spec.js +++ b/packages/cli/test/cases/build.config.static-router/build.config.static-router.spec.js @@ -1,16 +1,16 @@ /* * Use Case - * Run Greenwood with mode setting in Greenwood config set to mpa. + * Run Greenwood with staticRouter setting in Greenwood to enable MPA like routing. * * User Result - * Should generate a bare bones Greenwood build with bundle JavaScript and routes. + * Should generate a bare bones Greenwood build with support for static router navigation. * * User Command * greenwood build * * User Config * { - * mode: 'mpa' + * staticRouter: true * } * * User Workspace @@ -33,7 +33,7 @@ import { fileURLToPath, URL } from 'url'; const expect = chai.expect; describe('Build Greenwood With: ', function() { - const LABEL = 'Custom Mode'; + const LABEL = 'Static Router'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); let runner; @@ -104,7 +104,7 @@ describe('Build Greenwood With: ', function() { expect(routerOutlets.length).to.be.equal(1); }); - it('should have two <greenwood-route> tags in the <body> for the content', function() { + it('should have expected <greenwood-route> tags in the <body> for each page', function() { const routeTags = dom.window.document.querySelectorAll('body > greenwood-route'); expect(routeTags.length).to.be.equal(3); diff --git a/packages/cli/test/cases/build.config.static-router/greenwood.config.js b/packages/cli/test/cases/build.config.static-router/greenwood.config.js new file mode 100644 index 000000000..f62cc71df --- /dev/null +++ b/packages/cli/test/cases/build.config.static-router/greenwood.config.js @@ -0,0 +1,3 @@ +export default { + staticRouter: true +}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.mode-mpa/src/pages/about.md b/packages/cli/test/cases/build.config.static-router/src/pages/about.md similarity index 100% rename from packages/cli/test/cases/build.config.mode-mpa/src/pages/about.md rename to packages/cli/test/cases/build.config.static-router/src/pages/about.md diff --git a/packages/cli/test/cases/build.config.mode-mpa/src/pages/index.md b/packages/cli/test/cases/build.config.static-router/src/pages/index.md similarity index 100% rename from packages/cli/test/cases/build.config.mode-mpa/src/pages/index.md rename to packages/cli/test/cases/build.config.static-router/src/pages/index.md diff --git a/packages/cli/test/cases/build.config.mode-mpa/src/pages/regex-test.html b/packages/cli/test/cases/build.config.static-router/src/pages/regex-test.html similarity index 100% rename from packages/cli/test/cases/build.config.mode-mpa/src/pages/regex-test.html rename to packages/cli/test/cases/build.config.static-router/src/pages/regex-test.html diff --git a/packages/cli/test/cases/build.default.import-node-modules/build.default.import-node-modules.spec.js b/packages/cli/test/cases/build.default.import-node-modules/build.default.import-node-modules.spec.js index 3501ed41f..c0b588d75 100644 --- a/packages/cli/test/cases/build.default.import-node-modules/build.default.import-node-modules.spec.js +++ b/packages/cli/test/cases/build.default.import-node-modules/build.default.import-node-modules.spec.js @@ -271,13 +271,13 @@ describe('Build Greenwood With: ', function() { expect(inlineScriptTag.textContent.replace('\n', '')).to // eslint-disable-next-line max-len - .equal('import"/lit-element.4727346b.js";import"/lit-html.5ab358db.js";document.getElementsByClassName("output-script-inline")[0].innerHTML="script tag module inline"//# sourceMappingURL=1807818843-scratch.f55017db.js.map'); + .equal('import"/lit-element.ae169679.js";import"/lit-html.7f7a9139.js";document.getElementsByClassName("output-script-inline")[0].innerHTML="script tag module inline"//# sourceMappingURL=1807818843-scratch.ee52d4f0.js.map'); }); it('should have the expected inline node_modules content in the second inline script tag which should include extra code from rollup', async function() { const inlineScriptTag = dom.window.document.querySelectorAll('head > script:not([src])')[1]; - expect(inlineScriptTag.textContent.replace('\n', '')).to.equal('import"/lit-element.4727346b.js";import"/lit-html.5ab358db.js";//# sourceMappingURL=2012376258-scratch.8fadfd92.js.map'); + expect(inlineScriptTag.textContent.replace('\n', '')).to.equal('import"/lit-element.ae169679.js";import"/lit-html.7f7a9139.js";//# sourceMappingURL=2012376258-scratch.0a6fc17c.js.map'); }); it('should have the expected output from the first inline <script> tag in the page output', async function() { diff --git a/packages/cli/test/cases/build.config.mode-spa/build.config.mode-spa.spec.js b/packages/cli/test/cases/build.default.spa/build.default.spa.spec.js similarity index 97% rename from packages/cli/test/cases/build.config.mode-spa/build.config.mode-spa.spec.js rename to packages/cli/test/cases/build.default.spa/build.default.spa.spec.js index 33d6f1f75..6f941a02f 100644 --- a/packages/cli/test/cases/build.config.mode-spa/build.config.mode-spa.spec.js +++ b/packages/cli/test/cases/build.default.spa/build.default.spa.spec.js @@ -1,26 +1,21 @@ /* * Use Case - * Run Greenwood with mode setting in Greenwood config set to spa. + * Run Greenwood with a single index.html file to build a SPA based project. * * User Result - * Should generate a bare bones Greenwood build for hosting a Single Page Application. + * Should generate a Greenwood build for a SPA. * * User Command * greenwood build * * User Config - * { - * mode: 'spa' - * } + * {} * * User Workspace * Greenwood default w/ single index.html file * src/ * components/ * footer.js - * routes/ - * about.js - * home.js * index.js * index.html */ @@ -36,7 +31,7 @@ import { fileURLToPath, URL } from 'url'; const expect = chai.expect; describe('Build Greenwood With: ', function() { - const LABEL = 'Custom Mode'; + const LABEL = 'A Single Page Application (SPA)'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); let runner; diff --git a/packages/cli/test/cases/build.default.spa/greenwood.config.js b/packages/cli/test/cases/build.default.spa/greenwood.config.js new file mode 100644 index 000000000..639fd5676 --- /dev/null +++ b/packages/cli/test/cases/build.default.spa/greenwood.config.js @@ -0,0 +1,3 @@ +export default { + title: 'this is the wrong title' +}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.mode-spa/package.json b/packages/cli/test/cases/build.default.spa/package.json similarity index 100% rename from packages/cli/test/cases/build.config.mode-spa/package.json rename to packages/cli/test/cases/build.default.spa/package.json diff --git a/packages/cli/test/cases/build.config.mode-spa/src/components/footer.js b/packages/cli/test/cases/build.default.spa/src/components/footer.js similarity index 100% rename from packages/cli/test/cases/build.config.mode-spa/src/components/footer.js rename to packages/cli/test/cases/build.default.spa/src/components/footer.js diff --git a/packages/cli/test/cases/build.config.mode-spa/src/index.html b/packages/cli/test/cases/build.default.spa/src/index.html similarity index 100% rename from packages/cli/test/cases/build.config.mode-spa/src/index.html rename to packages/cli/test/cases/build.default.spa/src/index.html diff --git a/packages/cli/test/cases/build.config.mode-spa/src/index.js b/packages/cli/test/cases/build.default.spa/src/index.js similarity index 100% rename from packages/cli/test/cases/build.config.mode-spa/src/index.js rename to packages/cli/test/cases/build.default.spa/src/index.js diff --git a/packages/cli/test/cases/build.config.mode-spa/src/routes/about.js b/packages/cli/test/cases/build.default.spa/src/routes/about.js similarity index 100% rename from packages/cli/test/cases/build.config.mode-spa/src/routes/about.js rename to packages/cli/test/cases/build.default.spa/src/routes/about.js diff --git a/packages/cli/test/cases/build.config.mode-spa/src/routes/home.js b/packages/cli/test/cases/build.default.spa/src/routes/home.js similarity index 100% rename from packages/cli/test/cases/build.config.mode-spa/src/routes/home.js rename to packages/cli/test/cases/build.default.spa/src/routes/home.js diff --git a/packages/cli/test/cases/build.default.ssr/build.default.ssr.spec.js b/packages/cli/test/cases/build.default.ssr/build.default.ssr.spec.js new file mode 100644 index 000000000..14891e712 --- /dev/null +++ b/packages/cli/test/cases/build.default.ssr/build.default.ssr.spec.js @@ -0,0 +1,265 @@ +/* + * Use Case + * Run Greenwood with an SSR route. + * + * User Result + * Should generate a bare bones Greenwood build for hosting a server rendered application. + * + * User Command + * greenwood build + * + * User Config + * None + * + * User Workspace + * src/ + * components/ + * footer.js + * pages/ + * artists.js + * templates/ + * app.html + */ +import chai from 'chai'; +import fs from 'fs'; +import { JSDOM } from 'jsdom'; +import path from 'path'; +import { getSetupFiles, getDependencyFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import request from 'request'; +import { runSmokeTest } from '../../../../../test/smoke-test.js'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +describe('Build Greenwood With: ', function() { + const LABEL = 'A Server Rendered Application (SSR)'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + const hostname = 'http://127.0.0.1:8080'; + let runner; + + before(async function() { + this.context = { + publicDir: path.join(outputPath, 'public') + }; + runner = new Runner(); + }); + + describe(LABEL, function() { + + before(async function() { + const lit = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/*.js`, + `${outputPath}/node_modules/lit/` + ); + const litDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/decorators/*.js`, + `${outputPath}/node_modules/lit/decorators/` + ); + const litDirectives = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/directives/*.js`, + `${outputPath}/node_modules/lit/directives/` + ); + const litPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/package.json`, + `${outputPath}/node_modules/lit/` + ); + const litElement = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/*.js`, + `${outputPath}/node_modules/lit-element/` + ); + const litElementPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/package.json`, + `${outputPath}/node_modules/lit-element/` + ); + const litElementDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/decorators/*.js`, + `${outputPath}/node_modules/lit-element/decorators/` + ); + const litHtml = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/*.js`, + `${outputPath}/node_modules/lit-html/` + ); + const litHtmlPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/package.json`, + `${outputPath}/node_modules/lit-html/` + ); + const litHtmlDirectives = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/directives/*.js`, + `${outputPath}/node_modules/lit-html/directives/` + ); + // lit-html has a dependency on this + // https://github.com/lit/lit/blob/main/packages/lit-html/package.json#L82 + const trustedTypes = await getDependencyFiles( + `${process.cwd()}/node_modules/@types/trusted-types/package.json`, + `${outputPath}/node_modules/@types/trusted-types/` + ); + const litReactiveElement = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/*.js`, + `${outputPath}/node_modules/@lit/reactive-element/` + ); + const litReactiveElementDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/decorators/*.js`, + `${outputPath}/node_modules/@lit/reactive-element/decorators/` + ); + const litReactiveElementPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/package.json`, + `${outputPath}/node_modules/@lit/reactive-element/` + ); + + await runner.setup(outputPath, [ + ...getSetupFiles(outputPath), + ...lit, + ...litPackageJson, + ...litDirectives, + ...litDecorators, + ...litElementPackageJson, + ...litElement, + ...litElementDecorators, + ...litHtmlPackageJson, + ...litHtml, + ...litHtmlDirectives, + ...trustedTypes, + ...litReactiveElement, + ...litReactiveElementDecorators, + ...litReactiveElementPackageJson + ]); + + return new Promise(async (resolve) => { + setTimeout(() => { + resolve(); + }, 10000); + + await runner.runCommand(cliPath, 'serve'); + }); + }); + + runSmokeTest(['public', 'index'], LABEL); + + let response = {}; + let dom; + let aboutPageGraphData; + + before(async function() { + const graph = JSON.parse(await fs.promises.readFile(path.join(outputPath, 'public/graph.json'), 'utf-8')); + + aboutPageGraphData = graph.filter(page => page.route === '/artists/')[0]; + + return new Promise((resolve, reject) => { + request.get(`${hostname}/artists/`, (err, res, body) => { + if (err) { + reject(); + } + + response = res; + response.body = body; + dom = new JSDOM(body); + + resolve(); + }); + }); + }); + + describe('Serve command with HTML route response', function() { + + it('should return a 200 status', function(done) { + expect(response.statusCode).to.equal(200); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers['content-type']).to.contain('text/html'); + done(); + }); + + it('should return a response body', function(done) { + expect(response.body).to.not.be.undefined; + done(); + }); + + it('the response body should be valid HTML from JSDOM', function(done) { + expect(dom).to.not.be.undefined; + done(); + }); + + it('should have one style tags', function() { + const styles = dom.window.document.querySelectorAll('head > style'); + + expect(styles.length).to.equal(1); + }); + + it('should have three script tags', function() { + const scripts = dom.window.document.querySelectorAll('head > script'); + + expect(scripts.length).to.equal(3); + }); + + it('should have expected SSR content from the non module script tag', function() { + const scripts = Array.from(dom.window.document.querySelectorAll('head > script')) + .filter(tag => !tag.getAttribute('type')); + + expect(scripts.length).to.equal(1); + expect(scripts[0].textContent).to.contain('console.log'); + }); + + it('should have a bundled script for the footer component', function() { + const footerScript = Array.from(dom.window.document.querySelectorAll('head > script[type]')) + .filter(script => (/footer.*[a-z0-9].js/).test(script.src)); + + expect(footerScript.length).to.be.equal(1); + expect(footerScript[0].type).to.be.equal('module'); + }); + + it('should have the expected number of table rows of content', function() { + const rows = dom.window.document.querySelectorAll('body > table tr'); + + expect(rows.length).to.equal(11); + }); + + it('should have the expected <title> content in the <head>', function() { + const title = dom.window.document.querySelectorAll('head > title'); + + expect(title.length).to.equal(1); + expect(title[0].textContent).to.equal('My App - /artists/'); + }); + + it('should have custom metadata in the <head>', function() { + const metaDescription = Array.from(dom.window.document.querySelectorAll('head > meta')) + .filter((tag) => tag.getAttribute('name') === 'description'); + + expect(metaDescription.length).to.equal(1); + expect(metaDescription[0].getAttribute('content')).to.equal('My App - /artists/ (this was generated server side!!!)'); + }); + + it('should be a part of graph.json', function() { + expect(aboutPageGraphData).to.not.be.undefined; + }); + + it('should have the expected menu and index values in the graph', function() { + expect(aboutPageGraphData.data.menu).to.equal('navigation'); + expect(aboutPageGraphData.data.index).to.equal(7); + }); + + it('should have expected custom data values in its graph data', function() { + expect(aboutPageGraphData.data.author).to.equal('Project Evergreen'); + expect(aboutPageGraphData.data.date).to.equal('01-01-2021'); + }); + + it('should append the expected <script> tag for a frontmatter import <x-counter> component', function() { + const componentName = 'counter'; + const counterScript = Array.from(dom.window.document.querySelectorAll('head > script[src]')) + .filter((tag) => tag.getAttribute('src').indexOf(`/${componentName}.`) === 0); + + expect(aboutPageGraphData.imports[0]).to.equal(`/components/${componentName}.js`); + expect(counterScript.length).to.equal(1); + }); + }); + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + runner.stopCommand(); + }); + +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.ssr/package.json b/packages/cli/test/cases/build.default.ssr/package.json new file mode 100644 index 000000000..2dee3e768 --- /dev/null +++ b/packages/cli/test/cases/build.default.ssr/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "dependencies": { + "lit": "^2.0.0" + } +} \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.ssr/src/components/counter.js b/packages/cli/test/cases/build.default.ssr/src/components/counter.js new file mode 100644 index 000000000..b102faa27 --- /dev/null +++ b/packages/cli/test/cases/build.default.ssr/src/components/counter.js @@ -0,0 +1,42 @@ +const template = document.createElement('template'); + +template.innerHTML = ` + <style> + :host { + color: blue; + } + </style> + <h3>My Counter</h3> + <button id="dec">-</button> + <span id="count"></span> + <button id="inc">+</button> +`; + +class MyCounter extends HTMLElement { + constructor() { + super(); + this.count = 0; + this.attachShadow({ mode: 'open' }); + } + + async connectedCallback() { + this.shadowRoot.appendChild(template.content.cloneNode(true)); + this.shadowRoot.getElementById('inc').onclick = () => this.inc(); + this.shadowRoot.getElementById('dec').onclick = () => this.dec(); + this.update(); + } + + inc() { + this.update(++this.count); // eslint-disable-line + } + + dec() { + this.update(--this.count); // eslint-disable-line + } + + update(count) { + this.shadowRoot.getElementById('count').innerHTML = count || this.count; + } +} + +customElements.define('x-counter', MyCounter); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.ssr/src/components/footer.js b/packages/cli/test/cases/build.default.ssr/src/components/footer.js new file mode 100644 index 000000000..cad38627a --- /dev/null +++ b/packages/cli/test/cases/build.default.ssr/src/components/footer.js @@ -0,0 +1,49 @@ +import { css, html, LitElement } from 'lit'; + +class FooterComponent extends LitElement { + + constructor() { + super(); + this.version = '0.11.1'; + } + + static get styles() { + return css` + .footer { + background-color: #192a27; + min-height: 30px; + padding-top: 10px; + } + + h4 { + width: 90%; + margin: 0 auto; + padding: 0; + text-align: center; + } + + a { + color: white; + text-decoration: none; + } + + span.separator { + color: white; + } + `; + } + + render() { + const { version } = this; + + return html` + <footer class="footer"> + <h4> + <a href="/">Greenwood v${version}</a> <span class="separator">◈</span> <a href="https://www.netlify.com/">This site is powered by Netlify</a> + </h4> + </footer> + `; + } +} + +customElements.define('app-footer', FooterComponent); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.ssr/src/pages/artists.js b/packages/cli/test/cases/build.default.ssr/src/pages/artists.js new file mode 100644 index 000000000..75d9970e7 --- /dev/null +++ b/packages/cli/test/cases/build.default.ssr/src/pages/artists.js @@ -0,0 +1,111 @@ +import fetch from 'node-fetch'; + +async function getTemplate(compilation, route) { + return ` + <html> + <head> + <meta name="description" content="${compilation.config.title} - ${route} (this was generated server side!!!)"> + + <script> + console.log(${JSON.stringify(compilation.graph.map(page => page.title).join(''))}); + </script> + + <style> + * { + color: blue; + } + + h1 { + width: 50%; + margin: 0 auto; + text-align: center; + color: red; + } + </style> + </head> + <body> + <h1>This heading was rendered server side!</h1> + <content-outlet></content-outlet> + </body> + </html> + `; +} + +async function getBody(compilation) { + const artists = await fetch('http://www.analogstudios.net/api/artists').then(resp => resp.json()); + const timestamp = new Date().getTime(); + const artistsListItems = artists + .filter(artist => artist.isActive === '1') + .map((artist) => { + const { id, name, bio, imageUrl } = artist; + + return ` + <tr> + <td>${id}</td> + <td>${name}</td> + <td>${bio}</td> + <td><img src="${imageUrl}"/></td> + </tr> + `; + }); + + return ` + <html> + <head> + <style> + h1, h6 { + width: 90%; + margin: 0 auto; + text-align: center; + } + table { + width: 80%; + margin: 20px auto; + text-align: left; + } + img { + width: 50%; + } + </style> + </head> + <body> + <h1>Hello from the server rendered artists page! 👋</h1> + <table> + <tr> + <th>ID</th> + <th>Name</th> + <th>Decription</th> + <th>Genre</th> + </tr> + ${artistsListItems.join('')} + </table> + <h6>Fetched at: ${timestamp}</h6> + <pre> + ${JSON.stringify(compilation.graph.map(page => page.title).join(''))} + </pre> + </body> + </html> + `; +} + +async function getFrontmatter(compilation, route) { + return { + menu: 'navigation', + index: 7, + title: `${compilation.config.title} - ${route}`, + imports: [ + '/components/counter.js' + ], + data: { + author: 'Project Evergreen', + date: '01-01-2021', + prerender: true + } + }; +} + +export { + getTemplate, + getBody, + getFrontmatter +}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.ssr/src/pages/index.md b/packages/cli/test/cases/build.default.ssr/src/pages/index.md new file mode 100644 index 000000000..5fa8f5c5d --- /dev/null +++ b/packages/cli/test/cases/build.default.ssr/src/pages/index.md @@ -0,0 +1,3 @@ +## Hello SSR + +Lorum Ipsum. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.ssr/src/templates/app.html b/packages/cli/test/cases/build.default.ssr/src/templates/app.html new file mode 100644 index 000000000..ca1ed274f --- /dev/null +++ b/packages/cli/test/cases/build.default.ssr/src/templates/app.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <meta-outlet></meta-outlet> + <script type="module" src="../components/footer.js"></script> + </head> + + <body> + <page-outlet></page-outlet> + <app-footer></app-footer> + </body> +</html> \ 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 b80e973a3..983d8d053 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 @@ -43,11 +43,13 @@ describe('Build Greenwood With: ', function() { describe('Custom Configuration with a bad value for plugin type', function() { it('should throw an error that plugin.type is not a valid value', async function() { + const pluginTypes = ['copy', 'context', 'resource', 'rollup', 'server', 'source', 'renderer']; + try { await runner.setup(outputPath); await runner.runCommand(cliPath, 'build'); } catch (err) { - expect(err).to.contain('Error: greenwood.config.js plugins must be one of type "copy, context, resource, rollup, server, source". got "indexxx" instead.'); + expect(err).to.contain(`Error: greenwood.config.js plugins must be one of type "${pluginTypes.join(', ')}". got "indexxx" instead.`); } }); }); diff --git a/packages/cli/test/cases/develop.spa/develop.spa.spec.js b/packages/cli/test/cases/develop.spa/develop.spa.spec.js index 3213bfb2f..4421341b1 100644 --- a/packages/cli/test/cases/develop.spa/develop.spa.spec.js +++ b/packages/cli/test/cases/develop.spa/develop.spa.spec.js @@ -1,17 +1,15 @@ /* * Use Case - * Run Greenwood develop command with SPA mode setting. + * Run Greenwood develop command for SPA based project. * * User Result - * Should start the development server in SPA mode with client side routing support. + * Should start the development server for a SPA with client side routing support. * * User Command * greenwood develop * * User Config - * { - * mode: 'spa' - * } + * {} * * User Workspace * src/ @@ -36,7 +34,7 @@ function removeWhiteSpace(string = '') { } describe('Develop Greenwood With: ', function() { - const LABEL = 'SPA Mode'; + const LABEL = 'A Single Page Application (SPA)'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); const hostname = 'http://localhost'; diff --git a/packages/cli/test/cases/develop.spa/greenwood.config.js b/packages/cli/test/cases/develop.spa/greenwood.config.js deleted file mode 100644 index 9cb101c85..000000000 --- a/packages/cli/test/cases/develop.spa/greenwood.config.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - mode: 'spa' -}; \ No newline at end of file diff --git a/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js b/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js new file mode 100644 index 000000000..538a063ac --- /dev/null +++ b/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js @@ -0,0 +1,246 @@ +/* + * Use Case + * Run Greenwood for development with a server side route. + * + * User Result + * Should serve a bare bones Greenwood build for developing a server rendered application. + * + * User Command + * greenwood develop + * + * User Config + * {} + * + * User Workspace + * src/ + * components/ + * footer.js + * pages/ + * artists.js + * templates/ + * app.html + */ +import chai from 'chai'; +import fs from 'fs'; +import { JSDOM } from 'jsdom'; +import path from 'path'; +import { getSetupFiles, getDependencyFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import request from 'request'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +describe('Develop Greenwood With: ', function() { + const LABEL = 'A Server Rendered Project (SSR)'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + const hostname = 'http://127.0.0.1:1984'; + let runner; + + before(async function() { + this.context = { + publicDir: path.join(outputPath, 'public') + }; + runner = new Runner(); + }); + + describe(LABEL, function() { + + before(async function() { + const lit = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/*.js`, + `${outputPath}/node_modules/lit/` + ); + const litDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/decorators/*.js`, + `${outputPath}/node_modules/lit/decorators/` + ); + const litDirectives = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/directives/*.js`, + `${outputPath}/node_modules/lit/directives/` + ); + const litPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/package.json`, + `${outputPath}/node_modules/lit/` + ); + const litElement = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/*.js`, + `${outputPath}/node_modules/lit-element/` + ); + const litElementPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/package.json`, + `${outputPath}/node_modules/lit-element/` + ); + const litElementDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/decorators/*.js`, + `${outputPath}/node_modules/lit-element/decorators/` + ); + const litHtml = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/*.js`, + `${outputPath}/node_modules/lit-html/` + ); + const litHtmlPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/package.json`, + `${outputPath}/node_modules/lit-html/` + ); + const litHtmlDirectives = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/directives/*.js`, + `${outputPath}/node_modules/lit-html/directives/` + ); + // lit-html has a dependency on this + // https://github.com/lit/lit/blob/main/packages/lit-html/package.json#L82 + const trustedTypes = await getDependencyFiles( + `${process.cwd()}/node_modules/@types/trusted-types/package.json`, + `${outputPath}/node_modules/@types/trusted-types/` + ); + const litReactiveElement = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/*.js`, + `${outputPath}/node_modules/@lit/reactive-element/` + ); + const litReactiveElementDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/decorators/*.js`, + `${outputPath}/node_modules/@lit/reactive-element/decorators/` + ); + const litReactiveElementPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/package.json`, + `${outputPath}/node_modules/@lit/reactive-element/` + ); + + await runner.setup(outputPath, [ + ...getSetupFiles(outputPath), + ...lit, + ...litPackageJson, + ...litDirectives, + ...litDecorators, + ...litElementPackageJson, + ...litElement, + ...litElementDecorators, + ...litHtmlPackageJson, + ...litHtml, + ...litHtmlDirectives, + ...trustedTypes, + ...litReactiveElement, + ...litReactiveElementDecorators, + ...litReactiveElementPackageJson + ]); + + return new Promise(async (resolve) => { + setTimeout(() => { + resolve(); + }, 10000); + + await runner.runCommand(cliPath, 'develop'); + }); + }); + + let response = {}; + let dom; + let artistsPageGraphData; + + before(async function() { + const graph = JSON.parse(await fs.promises.readFile(path.join(outputPath, '.greenwood/graph.json'), 'utf-8')); + + artistsPageGraphData = graph.filter(page => page.route === '/artists/')[0]; + + return new Promise((resolve, reject) => { + request.get(`${hostname}/artists/`, (err, res, body) => { + if (err) { + reject(); + } + + response = res; + response.body = body; + dom = new JSDOM(body); + + resolve(); + }); + }); + }); + + describe('Serve command with HTML route response', function() { + + it('should return a 200 status', function(done) { + expect(response.statusCode).to.equal(200); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers['content-type']).to.contain('text/html'); + done(); + }); + + it('should return a response body', function(done) { + expect(response.body).to.not.be.undefined; + done(); + }); + + it('the response body should be valid HTML from JSDOM', function(done) { + expect(dom).to.not.be.undefined; + done(); + }); + + it('should have one style tag', function() { + const styles = dom.window.document.querySelectorAll('head > style'); + + expect(styles.length).to.equal(1); + }); + + it('should have the expected number of table rows of content', function() { + const rows = dom.window.document.querySelectorAll('body > table tr'); + + expect(rows.length).to.equal(11); + }); + + it('should have the <app-footer> tag in the body', function() { + const footer = dom.window.document.querySelectorAll('body > app-footer'); + + expect(footer.length).to.equal(1); + }); + + it('should have the expected <title> content in the <head>', function() { + const title = dom.window.document.querySelectorAll('head > title'); + + expect(title.length).to.equal(1); + expect(title[0].textContent).to.equal('My App - /artists/'); + }); + + it('should have custom metadata in the <head>', function() { + const metaDescription = Array.from(dom.window.document.querySelectorAll('head > meta')) + .filter((tag) => tag.getAttribute('name') === 'description'); + + expect(metaDescription.length).to.equal(1); + expect(metaDescription[0].getAttribute('content')).to.equal('My App - /artists/ (this was generated server side!!!)'); + }); + + it('should be a part of graph.json', function() { + expect(artistsPageGraphData).to.not.be.undefined; + }); + + it('should have the expected menu and index values in the graph', function() { + expect(artistsPageGraphData.data.menu).to.equal('navigation'); + expect(artistsPageGraphData.data.index).to.equal(7); + }); + + it('should have expected custom data values in its graph data', function() { + expect(artistsPageGraphData.data.author).to.equal('Project Evergreen'); + expect(artistsPageGraphData.data.date).to.equal('01-01-2021'); + }); + + it('should append the expected <script> tag for a frontmatter import <x-counter> component', function() { + const componentName = 'counter'; + const counterScript = Array.from(dom.window.document.querySelectorAll('head > script[src]')) + .filter((tag) => tag.getAttribute('src').indexOf(`${componentName}.js`) >= 0); + + expect(artistsPageGraphData.imports[0]).to.equal(`/components/${componentName}.js`); + expect(counterScript.length).to.equal(1); + }); + }); + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + runner.stopCommand(); + }); + +}); \ No newline at end of file diff --git a/packages/cli/test/cases/develop.ssr/package.json b/packages/cli/test/cases/develop.ssr/package.json new file mode 100644 index 000000000..2dee3e768 --- /dev/null +++ b/packages/cli/test/cases/develop.ssr/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "dependencies": { + "lit": "^2.0.0" + } +} \ No newline at end of file diff --git a/packages/cli/test/cases/develop.ssr/src/components/counter.js b/packages/cli/test/cases/develop.ssr/src/components/counter.js new file mode 100644 index 000000000..b102faa27 --- /dev/null +++ b/packages/cli/test/cases/develop.ssr/src/components/counter.js @@ -0,0 +1,42 @@ +const template = document.createElement('template'); + +template.innerHTML = ` + <style> + :host { + color: blue; + } + </style> + <h3>My Counter</h3> + <button id="dec">-</button> + <span id="count"></span> + <button id="inc">+</button> +`; + +class MyCounter extends HTMLElement { + constructor() { + super(); + this.count = 0; + this.attachShadow({ mode: 'open' }); + } + + async connectedCallback() { + this.shadowRoot.appendChild(template.content.cloneNode(true)); + this.shadowRoot.getElementById('inc').onclick = () => this.inc(); + this.shadowRoot.getElementById('dec').onclick = () => this.dec(); + this.update(); + } + + inc() { + this.update(++this.count); // eslint-disable-line + } + + dec() { + this.update(--this.count); // eslint-disable-line + } + + update(count) { + this.shadowRoot.getElementById('count').innerHTML = count || this.count; + } +} + +customElements.define('x-counter', MyCounter); \ No newline at end of file diff --git a/packages/cli/test/cases/develop.ssr/src/components/footer.js b/packages/cli/test/cases/develop.ssr/src/components/footer.js new file mode 100644 index 000000000..cad38627a --- /dev/null +++ b/packages/cli/test/cases/develop.ssr/src/components/footer.js @@ -0,0 +1,49 @@ +import { css, html, LitElement } from 'lit'; + +class FooterComponent extends LitElement { + + constructor() { + super(); + this.version = '0.11.1'; + } + + static get styles() { + return css` + .footer { + background-color: #192a27; + min-height: 30px; + padding-top: 10px; + } + + h4 { + width: 90%; + margin: 0 auto; + padding: 0; + text-align: center; + } + + a { + color: white; + text-decoration: none; + } + + span.separator { + color: white; + } + `; + } + + render() { + const { version } = this; + + return html` + <footer class="footer"> + <h4> + <a href="/">Greenwood v${version}</a> <span class="separator">◈</span> <a href="https://www.netlify.com/">This site is powered by Netlify</a> + </h4> + </footer> + `; + } +} + +customElements.define('app-footer', FooterComponent); \ No newline at end of file diff --git a/packages/cli/test/cases/develop.ssr/src/pages/artists.js b/packages/cli/test/cases/develop.ssr/src/pages/artists.js new file mode 100644 index 000000000..cb31b181d --- /dev/null +++ b/packages/cli/test/cases/develop.ssr/src/pages/artists.js @@ -0,0 +1,110 @@ +import fetch from 'node-fetch'; + +async function getTemplate(compilation, route) { + return ` + <html> + <head> + <meta name="description" content="${compilation.config.title} - ${route} (this was generated server side!!!)"> + + <script> + console.log(${JSON.stringify(compilation.graph.map(page => page.title).join(''))}); + </script> + + <style> + * { + color: blue; + } + + h1 { + width: 50%; + margin: 0 auto; + text-align: center; + color: red; + } + </style> + </head> + <body> + <h1>This heading was rendered server side!</h1> + <content-outlet></content-outlet> + </body> + </html> + `; +} + +async function getBody(compilation) { + const artists = await fetch('http://www.analogstudios.net/api/artists').then(resp => resp.json()); + const timestamp = new Date().getTime(); + const artistsListItems = artists + .filter(artist => artist.isActive === '1') + .map((artist) => { + const { id, name, bio, imageUrl } = artist; + + return ` + <tr> + <td>${id}</td> + <td>${name}</td> + <td>${bio}</td> + <td><img src="${imageUrl}"/></td> + </tr> + `; + }); + + return ` + <html> + <head> + <style> + h1, h6 { + width: 90%; + margin: 0 auto; + text-align: center; + } + table { + width: 80%; + margin: 20px auto; + text-align: left; + } + img { + width: 50%; + } + </style> + </head> + <body> + <h1>Hello from the server rendered artists page! 👋</h1> + <table> + <tr> + <th>ID</th> + <th>Name</th> + <th>Decription</th> + <th>Genre</th> + </tr> + ${artistsListItems.join('')} + </table> + <h6>Fetched at: ${timestamp}</h6> + <pre> + ${JSON.stringify(compilation.graph.map(page => page.title).join(''))} + </pre> + </body> + </html> + `; +} + +async function getFrontmatter(compilation, route) { + return { + menu: 'navigation', + index: 7, + title: `${compilation.config.title} - ${route}`, + imports: [ + '/components/counter.js' + ], + data: { + author: 'Project Evergreen', + date: '01-01-2021' + } + }; +} + +export { + getTemplate, + getBody, + getFrontmatter +}; \ No newline at end of file diff --git a/packages/cli/test/cases/develop.ssr/src/templates/app.html b/packages/cli/test/cases/develop.ssr/src/templates/app.html new file mode 100644 index 000000000..ca1ed274f --- /dev/null +++ b/packages/cli/test/cases/develop.ssr/src/templates/app.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <meta-outlet></meta-outlet> + <script type="module" src="../components/footer.js"></script> + </head> + + <body> + <page-outlet></page-outlet> + <app-footer></app-footer> + </body> +</html> \ No newline at end of file diff --git a/packages/cli/test/cases/serve.default/greenwood.config.js b/packages/cli/test/cases/serve.default/greenwood.config.js index 4f9219bb1..6eb3fe410 100644 --- a/packages/cli/test/cases/serve.default/greenwood.config.js +++ b/packages/cli/test/cases/serve.default/greenwood.config.js @@ -3,5 +3,6 @@ export default { proxy: { '/api': 'https://www.analogstudios.net' } - } + }, + port: 8181 }; \ No newline at end of file diff --git a/packages/cli/test/cases/serve.default/serve.default.spec.js b/packages/cli/test/cases/serve.default/serve.default.spec.js index 838401d02..762862f94 100644 --- a/packages/cli/test/cases/serve.default/serve.default.spec.js +++ b/packages/cli/test/cases/serve.default/serve.default.spec.js @@ -9,7 +9,14 @@ * greenwood serve * * User Config - * None (Greenwood Default) + * { + * devServer: { + * proxy: { + '/api': 'https://www.analogstudios.net' + } + * }, + * port: 8181 + * } * * User Workspace * Greenwood default (src/) @@ -28,7 +35,7 @@ describe('Serve Greenwood With: ', function() { const LABEL = 'Default Greenwood Configuration and Workspace'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); - const hostname = 'http://127.0.0.1:8080'; + const hostname = 'http://127.0.0.1:8181'; let runner; before(function() { diff --git a/packages/init/package.json b/packages/init/package.json index 3f60c39cd..aaeec9b66 100644 --- a/packages/init/package.json +++ b/packages/init/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/init", - "version": "0.22.1", + "version": "0.23.0-alpha.1", "description": "A package for scaffolding a new Greenwood project.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/init", "author": "Grant Hutchinson <grant@hutchdev.ca>", diff --git a/packages/plugin-babel/README.md b/packages/plugin-babel/README.md index 27d68e96d..d032c4e66 100644 --- a/packages/plugin-babel/README.md +++ b/packages/plugin-babel/README.md @@ -43,12 +43,12 @@ module.exports = { }; ``` -This will then process your JavaScript with Babel with the configurated plugins / settings you provide. +This will then process your JavaScript with Babel using the configured plugins and settings you provide. > _For now Babel configuration needs to be in CJS. Will we be adding ESM support soon!_ ## Options -This plugin provides a default _babel.config.js_ that includes support for [**@babel/preset-env**](https://babeljs.io/docs/en/babel-preset-env) using [**browserslist**](https://github.com/browserslist/browserslist) with reasonable [default configs](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-babel/src/) for each. +This plugin provides a default _babel.config.js_ that includes support for [**@babel/preset-env**](https://babeljs.io/docs/en/babel-preset-env) using [**browserslist**](https://github.com/browserslist/browserslist) with reasonable [default configs](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-babel/src/) for each. If you would like to use it, either standalone or with your own custom _babel.config.js_, you will need to take the following extra steps: diff --git a/packages/plugin-babel/package.json b/packages/plugin-babel/package.json index e7f59520f..5e62cb0c6 100644 --- a/packages/plugin-babel/package.json +++ b/packages/plugin-babel/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-babel", - "version": "0.22.1", + "version": "0.23.0-alpha.1", "description": "A Greenwood plugin for using Babel and applying it to your JavaScript.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-babel", "author": "Owen Buckley <owen@thegreenhouse.io>", @@ -35,6 +35,6 @@ "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-private-methods": "^7.10.4", "@babel/runtime": "^7.10.4", - "@greenwood/cli": "^0.22.1" + "@greenwood/cli": "^0.23.0-alpha.1" } } diff --git a/packages/plugin-google-analytics/README.md b/packages/plugin-google-analytics/README.md index cdb7f1826..999a2fa4e 100644 --- a/packages/plugin-google-analytics/README.md +++ b/packages/plugin-google-analytics/README.md @@ -1,12 +1,12 @@ # @greenwood/plugin-google-analytics ## Overview -A Greenwood plugin adding support for [Google Analytics](https://developers.google.com/analytics/) JavaScript tracker. It assumes you already have your own Tracking ID(s) and [can eiterh filter out tracking for everything but your production environment](https://stackoverflow.com/a/1251931/417806) so that local testing doesn't interfere with production data, or use a conditional based `analyticsId` using an environment variable, ex. +A Greenwood plugin adding support for [Google Analytics](https://developers.google.com/analytics/) JavaScript tracker. It assumes you already have your own Tracking ID(s) and [can either filter out tracking for everything but your production environment](https://stackoverflow.com/a/1251931/417806) so that local testing doesn't interfere with production data, or use a conditional based `analyticsId` using an environment variable, ex. ```js const analyticsId = process.env.NODE_ENV === 'xxx' ? 'UA-123...' : 'UA-345...'; ``` -> _For more information and complete docs about Greenwood, please visit the [Greenwood website](https://www.greenwoodjs.io/)._ +> _For more information and complete docs about Greenwood, please visit the [Greenwood website](https://www.greenwoodjs.io/)._ ## Installation @@ -49,14 +49,14 @@ This will then add the Google Analytics [JavaScript tracker snippet](https://dev - `anonymous` (optional) - Sets if tracking of IPs should be done anonymously. Default is `true` ### Outbound Links -For links that go outside of your domain, the global function [`getOutboundLink`](https://support.google.com/analytics/answer/7478520) is available for you to use. +For links that go outside of your domain, the global function [`getOutboundLink`](https://support.google.com/analytics/answer/7478520) is available for you to use. Example: ```html -<a - target="_blank" +<a + target="_blank" rel="noopener" - onclick="getOutboundLink('www.mylink.com');" + onclick="getOutboundLink('www.mylink.com');" href="www.mylink.com">My Link </a> ``` \ No newline at end of file diff --git a/packages/plugin-google-analytics/package.json b/packages/plugin-google-analytics/package.json index 99311654a..a4a291569 100644 --- a/packages/plugin-google-analytics/package.json +++ b/packages/plugin-google-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-google-analytics", - "version": "0.22.1", + "version": "0.23.0-alpha.1", "description": "A Greenwood plugin adding support for Google Analytics JavaScript tracker.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-google-analytics", "author": "Owen Buckley <owen@thegreenhouse.io>", @@ -23,6 +23,6 @@ "@greenwood/cli": "^0.4.0" }, "devDependencies": { - "@greenwood/cli": "^0.22.1" + "@greenwood/cli": "^0.23.0-alpha.1" } } diff --git a/packages/plugin-graphql/README.md b/packages/plugin-graphql/README.md index ddd2583cc..7dfd077a0 100644 --- a/packages/plugin-graphql/README.md +++ b/packages/plugin-graphql/README.md @@ -1,4 +1,4 @@ -# @greenwood/plugin-graphl +# @greenwood/plugin-graphql ## Overview A plugin for Greenwood to support using [GraphQL](https://graphql.org/) to query your content graph. It runs [**apollo-server**](https://www.apollographql.com/docs/apollo-server/) on the backend and provides an [**@apollo/client** like](https://www.apollographql.com/docs/react/api/core/ApolloClient/#ApolloClient.readQuery) interface for the frontend. @@ -48,7 +48,7 @@ class HeaderComponent extends HTMLElement { connectedCallback() { const response = await client.query({ - query: MenuQuery, + query: MenuQuery, variables: { name: 'navigation', order: 'index_asc' @@ -67,7 +67,7 @@ class HeaderComponent extends HTMLElement { </li> `; }).join(); - + return ` <header> <nav> diff --git a/packages/plugin-graphql/package.json b/packages/plugin-graphql/package.json index d9a589f1c..a66450e58 100644 --- a/packages/plugin-graphql/package.json +++ b/packages/plugin-graphql/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-graphql", - "version": "0.22.1", + "version": "0.23.0-alpha.1", "description": "A plugin for using GraphQL for querying your content.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-graphql", "author": "Owen Buckley <owen@thegreenhouse.io>", @@ -32,6 +32,6 @@ "node-fetch": "^2.6.1" }, "devDependencies": { - "@greenwood/cli": "^0.22.1" + "@greenwood/cli": "^0.23.0-alpha.1" } } diff --git a/packages/plugin-graphql/src/index.js b/packages/plugin-graphql/src/index.js index 3e4ad7a78..3f550cd27 100644 --- a/packages/plugin-graphql/src/index.js +++ b/packages/plugin-graphql/src/index.js @@ -58,8 +58,8 @@ class GraphQLResource extends ResourceInterface { }); } - async shouldOptimize(url) { - return Promise.resolve(path.extname(url) === '.html'); + async shouldOptimize(url = '', body, headers = {}) { + return Promise.resolve((url && path.extname(url) === '.html') || (headers.request && headers.request['content-type'].indexOf('text/html') >= 0)); } async optimize(url, body) { diff --git a/packages/plugin-graphql/src/queries/config.gql b/packages/plugin-graphql/src/queries/config.gql index 92937bb34..7d2580bc6 100644 --- a/packages/plugin-graphql/src/queries/config.gql +++ b/packages/plugin-graphql/src/queries/config.gql @@ -11,7 +11,7 @@ query { value, href }, - mode, + staticRouter, optimization, prerender, title, diff --git a/packages/plugin-graphql/src/schema/config.js b/packages/plugin-graphql/src/schema/config.js index b1fca32ad..5f28f7a67 100644 --- a/packages/plugin-graphql/src/schema/config.js +++ b/packages/plugin-graphql/src/schema/config.js @@ -22,7 +22,7 @@ const configTypeDefs = gql` type Config { devServer: DevServer, meta: [Meta], - mode: String, + staticRouter: Boolean, optimization: String, prerender: Boolean, title: String, diff --git a/packages/plugin-import-commonjs/README.md b/packages/plugin-import-commonjs/README.md index eab045399..e1020e127 100644 --- a/packages/plugin-import-commonjs/README.md +++ b/packages/plugin-import-commonjs/README.md @@ -32,7 +32,7 @@ export default { } ``` -This will then allow you to use a CommonJS based modules in the browser. For example, here is how you could use [**lodash**](https://lodash.com/) (although as mentioend above, in this case, you would want to use [**lodash-es**](https://www.npmjs.com/package/lodash-es) instead.) +This will then allow you to use a CommonJS based modules in the browser. For example, here is how you could use [**lodash**](https://lodash.com/) (although as mentioned above, in this case, you would want to use [**lodash-es**](https://www.npmjs.com/package/lodash-es) instead.) ```javascript // <script src="my-file.js"> diff --git a/packages/plugin-import-commonjs/package.json b/packages/plugin-import-commonjs/package.json index ab4800810..304736c6a 100644 --- a/packages/plugin-import-commonjs/package.json +++ b/packages/plugin-import-commonjs/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-import-commonjs", - "version": "0.22.1", + "version": "0.23.0-alpha.1", "description": "A plugin for loading CommonJS based modules in the browser using ESM (import / export) syntax.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-commonjs", "author": "Owen Buckley <owen@thegreenhouse.io>", @@ -28,7 +28,7 @@ "cjs-module-lexer": "^1.0.0" }, "devDependencies": { - "@greenwood/cli": "^0.22.1", + "@greenwood/cli": "^0.23.0-alpha.1", "lodash": "^4.17.20" } } diff --git a/packages/plugin-import-css/README.md b/packages/plugin-import-css/README.md index 475c97456..65c6f19ea 100644 --- a/packages/plugin-import-css/README.md +++ b/packages/plugin-import-css/README.md @@ -32,7 +32,7 @@ export default { } ``` -> 👉 _If you are using this along with [**PostCSS plugin**](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-postcss), make sure **plugin-postcss** comes first! All non standard transformations need to come last._ +> 👉 _If you are using this along with [**PostCSS plugin**](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-postcss), make sure **plugin-postcss** comes first! All non standard transformations need to come last._ This will then allow you use `import` to include CSS in your JavaScript files by appending `?type=css` to the end of the `import` statement. @@ -64,4 +64,4 @@ module.exports = { require('postcss-import') ] }; -``` +``` \ No newline at end of file diff --git a/packages/plugin-import-css/package.json b/packages/plugin-import-css/package.json index 4bb722c3d..c6da6a65a 100644 --- a/packages/plugin-import-css/package.json +++ b/packages/plugin-import-css/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-import-css", - "version": "0.22.1", + "version": "0.23.0-alpha.1", "description": "A Greenwood plugin to allow you to use ESM (import) syntax to load your CSS.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-css", "author": "Owen Buckley <owen@thegreenhouse.io>", @@ -28,6 +28,6 @@ "rollup-plugin-postcss": "^4.0.2" }, "devDependencies": { - "@greenwood/cli": "^0.22.1" + "@greenwood/cli": "^0.23.0-alpha.1" } } diff --git a/packages/plugin-import-json/package.json b/packages/plugin-import-json/package.json index baa3dbf4b..2a561b475 100644 --- a/packages/plugin-import-json/package.json +++ b/packages/plugin-import-json/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-import-json", - "version": "0.22.1", + "version": "0.23.0-alpha.1", "description": "A Greenwood plugin to allow you to use ESM (import) syntax to load your JSON.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-json", "author": "Owen Buckley <owen@thegreenhouse.io>", @@ -27,6 +27,6 @@ "@rollup/plugin-json": "^4.1.0" }, "devDependencies": { - "@greenwood/cli": "^0.22.1" + "@greenwood/cli": "^0.23.0-alpha.1" } } diff --git a/packages/plugin-include-html/README.md b/packages/plugin-include-html/README.md index 508e9bade..699714505 100644 --- a/packages/plugin-include-html/README.md +++ b/packages/plugin-include-html/README.md @@ -1,7 +1,7 @@ # @greenwood/plugin-include-html ## Overview -In the spirit of the since [abandoned HTML Imports spec](https://www.html5rocks.com/en/tutorials/webcomponents/imports/) that was originally part of the init Web Components "feature suite", and given the renewed [interest in bringing it back](https://github.com/whatwg/html/issues/2791), this plugin adds expiremental support to realize the HTML Includes "spec" as a build time templating system for HTML. The goal here is to enable developers the ability to ship more static HTML while allowing the authoring context to be JavaScript **and** leveraging standard semantics and web expectations. 💚 +In the spirit of the since [abandoned HTML Imports spec](https://www.html5rocks.com/en/tutorials/webcomponents/imports/) that was originally part of the init Web Components "feature suite", and given the renewed [interest in bringing it back](https://github.com/whatwg/html/issues/2791), this plugin adds experimental support to realize the HTML Includes "spec" as a build time templating system for HTML. The goal here is to enable developers the ability to ship more static HTML while allowing the authoring context to be JavaScript **and** leveraging standard semantics and web expectations. 💚 > **Note**: I think if you want this feature in its most strictest sense of the word, I would recommend the [**<html-include>**](https://github.com/justinfagnani/html-include-element) custom element, which provides a runtime implementation of this as a Web Component. @@ -37,7 +37,7 @@ So given a snippet of HTML, e.g. <header class="my-include"> <h1>Welcome to my website!<h1> -</header> +</header> ``` In a page template, you could then do this @@ -49,7 +49,7 @@ In a page template, you could then do this <link rel="html" href="/includes/header.html"></link> <h2>Hello 👋</h2> - + </body> <html> @@ -69,10 +69,10 @@ And Greenwood will statically generate this <header class="my-include"> <h1>Welcome to my website!<h1> - </header> + </header> <h2>Hello 👋</h2> - + </body> <html> @@ -113,7 +113,7 @@ const getData = async () => { export { getTemplate, getData -}; +}; ``` In a page template, you can now do this @@ -123,7 +123,7 @@ In a page template, you can now do this <body> <h2>Hello 👋</h2> - <app-footer src="../includes/footer.js"></app-footer> + <app-footer src="../includes/footer.js"></app-footer> </body> <html> @@ -148,10 +148,10 @@ And Greenwood would statically generate this <a href="/">Greenwood v0.19.0-alpha.2</a> </h4> </footer> - </app-footer> + </app-footer> </body> <html> ``` -> We think the JS flavor will really come to shine more when Greenwood adds support for [SSR](https://github.com/ProjectEvergreen/greenwood/issues/708), and then you could use this TECHNIQUE for displaying user / session data, or serverlessly at the edge! +> We think the JS flavor will really come to shine more when Greenwood adds support for [SSR](https://github.com/ProjectEvergreen/greenwood/issues/708), and then you could use this TECHNIQUE for displaying user / session data, or serverless at the edge! diff --git a/packages/plugin-include-html/package.json b/packages/plugin-include-html/package.json index 691d00b54..f916a2678 100644 --- a/packages/plugin-include-html/package.json +++ b/packages/plugin-include-html/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-include-html", - "version": "0.22.1", + "version": "0.23.0-alpha.1", "description": "A Greenwood plugin to let you render server side JS from HTML or JS at build time as HTML.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-include-html", "author": "Owen Buckley <owen@thegreenhouse.io>", @@ -24,6 +24,6 @@ "@greenwood/cli": "^0.4.0" }, "devDependencies": { - "@greenwood/cli": "^0.22.1" + "@greenwood/cli": "^0.23.0-alpha.1" } } diff --git a/packages/plugin-include-html/test/cases/build.default-custom-element/build.default.custom-element.spec.js b/packages/plugin-include-html/test/cases/build.default-custom-element/build.default.custom-element.spec.js index f6abe2ac8..290867a2e 100644 --- a/packages/plugin-include-html/test/cases/build.default-custom-element/build.default.custom-element.spec.js +++ b/packages/plugin-include-html/test/cases/build.default-custom-element/build.default.custom-element.spec.js @@ -36,7 +36,7 @@ import { fileURLToPath, URL } from 'url'; const expect = chai.expect; describe('Build Greenwood With HTML Include Plugin: ', function() { - const LABEL = 'Default Greenwood Configuration and Workspace'; + const LABEL = 'Using Custom Element feature'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); let runner; diff --git a/packages/plugin-include-html/test/cases/build.default-link-tag/build.default.link-tag.spec.js b/packages/plugin-include-html/test/cases/build.default-link-tag/build.default.link-tag.spec.js index 6c6e3a4ed..252e241ef 100644 --- a/packages/plugin-include-html/test/cases/build.default-link-tag/build.default.link-tag.spec.js +++ b/packages/plugin-include-html/test/cases/build.default-link-tag/build.default.link-tag.spec.js @@ -36,7 +36,7 @@ import { fileURLToPath, URL } from 'url'; const expect = chai.expect; describe('Build Greenwood With HTML Include Plugin: ', function() { - const LABEL = 'Default Greenwood Configuration and Workspace'; + const LABEL = 'Using Link Tag featuree'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); let runner; diff --git a/packages/plugin-polyfills/README.md b/packages/plugin-polyfills/README.md index 35f3b723a..bb9955683 100644 --- a/packages/plugin-polyfills/README.md +++ b/packages/plugin-polyfills/README.md @@ -1,7 +1,7 @@ # @greenwood/plugin-polyfills ## Overview -A Greenwood plugin adding support for [Web Component related polyfills](https://github.com/webcomponents/polyfills) for browsers that need support for part of the Web Component spec like **Custom Elements** and **Shadow DOM**. It uses [feature detection](https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs#using-webcomponents-loaderjs) to determine what polyfills are actually needed based on the user's browser, to ensure only the minumum extra code is loaded. If you are using **Lit@2**, it also loads the needed [_polyfill-support.js_](https://lit.dev/docs/tools/requirements/#polyfills) file. +A Greenwood plugin adding support for [Web Component related polyfills](https://github.com/webcomponents/polyfills) for browsers that need support for part of the Web Component spec like **Custom Elements** and **Shadow DOM**. It uses [feature detection](https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs#using-webcomponents-loaderjs) to determine what polyfills are actually needed based on the user's browser, to ensure only the minimum extra code is loaded. If you are using **Lit@2**, it also loads the needed [_polyfill-support.js_](https://lit.dev/docs/tools/requirements/#polyfills) file. As of right now, you will likely need this plugin to load additional polyfills if you want to support these browser(s): diff --git a/packages/plugin-polyfills/package.json b/packages/plugin-polyfills/package.json index bd5a52831..413d60d84 100644 --- a/packages/plugin-polyfills/package.json +++ b/packages/plugin-polyfills/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-polyfills", - "version": "0.22.1", + "version": "0.23.0-alpha.1", "description": "A Greenwood plugin adding support for Web Component related polyfills like Custom Elements and Shadow DOM.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-polyfills", "author": "Owen Buckley <owen@thegreenhouse.io>", @@ -26,6 +26,6 @@ "@webcomponents/webcomponentsjs": "^2.6.0" }, "devDependencies": { - "@greenwood/cli": "^0.22.1" + "@greenwood/cli": "^0.23.0-alpha.1" } } diff --git a/packages/plugin-polyfills/src/index.js b/packages/plugin-polyfills/src/index.js index 3ad36309c..37fc24fad 100644 --- a/packages/plugin-polyfills/src/index.js +++ b/packages/plugin-polyfills/src/index.js @@ -8,8 +8,8 @@ class PolyfillsResource extends ResourceInterface { super(compilation, options); } - async shouldOptimize(url) { - return Promise.resolve(path.extname(url) === '.html'); + async shouldOptimize(url = '', body, headers = {}) { + return Promise.resolve(path.extname(url) === '.html' || (headers.request && headers.request['content-type'].indexOf('text/html') >= 0)); } async optimize(url, body) { diff --git a/packages/plugin-postcss/README.md b/packages/plugin-postcss/README.md index 257f1fe2c..15bad3c95 100644 --- a/packages/plugin-postcss/README.md +++ b/packages/plugin-postcss/README.md @@ -32,7 +32,7 @@ export default { } ``` -> 👉 _If you are using this along with [**plugin-import-css**](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-css), make sure **plugin-postcss** comes first. All non stanrd transformation need to come last._ +> 👉 _If you are using this along with [**plugin-import-css**](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-css), make sure **plugin-postcss** comes first. All non standard transformations need to come last._ Optionally, to use your own PostCSS configuration, you'll need to create _two (2)_ config files in the root of your project, by which you can provide your own custom plugins / settings that you've installed. - _postcss.config.js_ @@ -58,7 +58,7 @@ export default { _Eventually once [PostCSS adds support for ESM configuration files](https://github.com/postcss/postcss-cli/issues/387), then this will drop to only needing one file._ ## Options -This plugin provides a default _postcss.config.js_ that includes support for [**postcss-preset-env**](https://github.com/csstools/postcss-preset-env) using [**browserslist**](https://github.com/browserslist/browserslist) with reasonable [default configs](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-postcss/src/) for each. +This plugin provides a default _postcss.config.js_ that includes support for [**postcss-preset-env**](https://github.com/csstools/postcss-preset-env) using [**browserslist**](https://github.com/browserslist/browserslist) with reasonable [default configs](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-postcss/src/) for each. If you would like to use it with your own custom _postcss.config.js_, you will need to enable the `extendConfig` option ```js @@ -85,4 +85,4 @@ export default { }; ``` -This will then process your CSS with PostCSS with the configurated plugins / settings you provide, merged after the default `plugins` listed above. \ No newline at end of file +This will then process your CSS with PostCSS using the configured plugins / settings you provide, merged after the default `plugins` listed above. \ No newline at end of file diff --git a/packages/plugin-postcss/package.json b/packages/plugin-postcss/package.json index 2f67e8001..2bf3662b3 100644 --- a/packages/plugin-postcss/package.json +++ b/packages/plugin-postcss/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-postcss", - "version": "0.22.1", + "version": "0.23.0-alpha.1", "description": "A Greenwood plugin for loading PostCSS configuration and applying it to your CSS.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-postcss", "author": "Owen Buckley <owen@thegreenhouse.io>", @@ -29,6 +29,6 @@ "postcss-preset-env": "^7.0.1" }, "devDependencies": { - "@greenwood/cli": "^0.22.1" + "@greenwood/cli": "^0.23.0-alpha.1" } } diff --git a/packages/plugin-renderer-lit/README.md b/packages/plugin-renderer-lit/README.md new file mode 100644 index 000000000..b7d085785 --- /dev/null +++ b/packages/plugin-renderer-lit/README.md @@ -0,0 +1,116 @@ +# @greenwood/plugin-renderer-lit + +## Overview + +A Greenwood plugin for using [**Lit**'s SSR capabilities](https://github.com/lit/lit/tree/main/packages/labs/ssr) as a custom server-side renderer. Although support is experimental at this time, this plugin also gives the ability to statically render entire pages and templates (instead of puppeteer) to output completely static sites. + +_We are still actively working on SSR features and enhancements for Greenwood [as part of our 1.0 release](https://github.com/ProjectEvergreen/greenwood/issues?q=is%3Aissue+is%3Aopen+label%3Assr+milestone%3A1.0) so please feel free to test it out and report your feedback._ 🙏 + +> This package assumes you already have `@greenwood/cli` installed. + + +## Prerequisite + +This packages depends on the Lit package as a `peerDependency`. This means you must have Lit already installed in your project. You can install anything following the `2.x` release line. + +```sh +# npm +$ npm install lit --dev + +# yarn +$ yarn add lit --dev +``` + +## Installation + +You can use your favorite JavaScript package manager to install this package. + +```bash +# npm +npm install @greenwood/plugin-renderer-lit --save-dev + +# yarn +yarn add @greenwood/plugin-renderer-lit --dev +``` + +## Usage +Add this plugin to your _greenwood.config.js_. + +```javascript +import { greenwoodPluginRendererLit } from '@greenwood/plugin-renderer-lit'; + +export default { + ... + + plugins: [ + greenwoodPluginRendererLit() + ] +} +``` + +Now, you can write some [SSR routes](/docs/server-rendering/) using Lit! The below example even uses the standard [SimpleGreeting](https://lit.dev/playground/) component from the Lit docs. +```js +import fetch from 'node-fetch'; +import { html } from 'lit'; +import '../components/greeting.js'; + +async function getBody() { + const artists = await fetch('http://www.mydomain.com/api/artists').then(resp => resp.json()); + + return html` + <h1>Lit SSR response</h1> + <table> + <tr> + <th>ID</th> + <th>Name</th> + <th>Description</th> + <th>Message</th> + <th>Picture</th> + </tr> + ${ + artists.map((artist) => { + const { id, name, bio, imageUrl } = artist; + + return html` + <tr> + <td>${id}</td> + <td>${name}</td> + <td>${bio}</td> + <td> + <a href="http://www.mydomain.com/artists/${id}" target="_blank"> + <simple-greeting .name="${name}"></simple-greeting> + </a> + </td> + <td><img src="${imageUrl}"/></td> + </tr> + `; + }) + } + </table> + `; +} + +export { getBody }; +``` + +## Options + +### Prerender (experimental) + +The plugin provides a setting that can be used to override Greenwood's [default _prerender_](/docs/configuration/#prerender) which is Puppeteer, and to instead use Lit. + +```javascript +import { greenwoodPluginRendererLit } from '@greenwood/plugin-renderer-lit'; + +export default { + ... + + plugins: [ + greenwoodPluginRendererLit({ + prerender: true + }) + ] +} +``` + +> _Keep in mind you will need to make sure your Lit Web Components are isomorphic and [properly leveraging `LitElement`'s lifecycles](https://github.com/lit/lit/tree/main/packages/labs/ssr#notes-and-limitations) and browser / Node APIs accordingly for maximum compatibility and portability._ \ No newline at end of file diff --git a/packages/plugin-renderer-lit/package.json b/packages/plugin-renderer-lit/package.json new file mode 100644 index 000000000..30e1b0837 --- /dev/null +++ b/packages/plugin-renderer-lit/package.json @@ -0,0 +1,33 @@ +{ + "name": "@greenwood/plugin-renderer-lit", + "version": "0.23.0-alpha.1", + "description": "A server-side renderering plugin for Lit based Greenwood projects.", + "type": "module", + "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-renderer-lit", + "author": "Owen Buckley <owen@thegreenhouse.io>", + "license": "MIT", + "keywords": [ + "Greenwood", + "Web Components", + "Lit", + "SSR" + ], + "main": "./src/index.js", + "files": [ + "src/" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@greenwood/cli": "^0.22.1", + "lit": "^2.1.1" + }, + "dependencies": { + "@lit-labs/ssr": "^2.0.1" + }, + "devDependencies": { + "@greenwood/cli": "^0.23.0-alpha.1", + "lit": "^2.1.1" + } +} diff --git a/packages/plugin-renderer-lit/src/index.js b/packages/plugin-renderer-lit/src/index.js new file mode 100755 index 000000000..4c06c0692 --- /dev/null +++ b/packages/plugin-renderer-lit/src/index.js @@ -0,0 +1,16 @@ +const greenwoodPluginRendererLit = (options = {}) => { + return { + type: 'renderer', + name: 'plugin-renderer-lit', + provider: () => { + return { + workerUrl: new URL('./ssr-route-worker-lit.js', import.meta.url), + prerender: options.prerender + }; + } + }; +}; + +export { + greenwoodPluginRendererLit +}; \ No newline at end of file diff --git a/packages/plugin-renderer-lit/src/ssr-route-worker-lit.js b/packages/plugin-renderer-lit/src/ssr-route-worker-lit.js new file mode 100644 index 000000000..dedd61883 --- /dev/null +++ b/packages/plugin-renderer-lit/src/ssr-route-worker-lit.js @@ -0,0 +1,66 @@ +// this needs to come first +import { render } from '@lit-labs/ssr/lib/render-with-global-dom-shim.js'; +import { Buffer } from 'buffer'; +import { html } from 'lit'; +import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; +import { pathToFileURL } from 'url'; +import { Readable } from 'stream'; +import { workerData, parentPort } from 'worker_threads'; + +async function streamToString (stream) { + const chunks = []; + + for await (let chunk of stream) { + chunks.push(Buffer.from(chunk)); + } + + return Buffer.concat(chunks).toString('utf-8'); +} + +async function getTemplateResultString(template) { + return await streamToString(Readable.from(render(template))); +} + +async function executeRouteModule({ modulePath, compilation, route, label, id, prerender, htmlContents, scripts }) { + const parsedCompilation = JSON.parse(compilation); + const parsedScripts = scripts ? JSON.parse(scripts) : []; + const data = { + template: null, + body: null, + frontmatter: null, + html: null + }; + + // prerender static content + if (prerender) { + for (const script of parsedScripts) { + await import(script); + } + + const templateResult = html`${unsafeHTML(htmlContents)}`; + + data.html = await getTemplateResultString(templateResult); + } else { + const { getTemplate = null, getBody = null, getFrontmatter = null } = await import(pathToFileURL(modulePath)).then(module => module); + + if (getTemplate) { + const templateResult = await getTemplate(parsedCompilation, route); + + data.template = await getTemplateResultString(templateResult); + } + + if (getBody) { + const templateResult = await getBody(parsedCompilation, route); + + data.body = await getTemplateResultString(templateResult); + } + + if (getFrontmatter) { + data.frontmatter = await getFrontmatter(parsedCompilation, route, label, id); + } + } + + parentPort.postMessage(data); +} + +executeRouteModule(workerData); \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.default/artists.json b/packages/plugin-renderer-lit/test/cases/build.default/artists.json new file mode 100644 index 000000000..a6e94f271 --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.default/artists.json @@ -0,0 +1,134 @@ +[ + { + "id":"1", + "name":"Analog", + "imageUrl":"\/\/d34k5cjnk2rcze.cloudfront.net\/images\/artists\/analog.jpg", + "genre":"Rock and Roll", + "location":"Block Island. RI", + "label":"Analog Studios", + "contactPhone":null, + "contactEmail":"dave@analogstudios.net", + "bio":"<p>Analog, a power duo consisting of guitar and drums, formed and has performed on Block Island for over 2 years.  Dave Flamand (vocals and guitar) and Eli Sprague (drums) deliver an original, powerful rock sound showcasing Flamand’s songwriting.  Strong rock beats, smooth vocals, and loud guitar tone meld together to create Analog’s unique sound.  The band currently has begun recording for its new album where it considers home base; Analog Studios in Newport, RI.<\/p>\n", + "isActive":"1" + }, + { + "id":"2", + "name":"Electro Calrissian", + "imageUrl":"\/\/d34k5cjnk2rcze.cloudfront.net\/images\/artists\/electro-calrissian.jpg", + "genre":"Punk Rock", + "location":"North Conway, NH", + "label":"Analog Studios", + "contactPhone":null, + "contactEmail":"electrocalrissian@gmail.com", + "bio":"A hard rock band from Conway, NH, Electro Calrissian knows how to crank out the tunes. Once a solid three piece, these days the lineup features songwriter Zack Smith on guitars, bass, and vocals and Nat MacDonald also playing guitar, bass, and singing. Aside from their usual gigs, Zack can often be found playing down at Open Mic on Mondays at the Red Parka Pub or Matty Bs Pizza. Check out their MySpace page for more up to date info, music, and news, maintained by their good friend and manager, Aldon Miller.", + "isActive":"1" + }, + { + "id":"3", + "name":"Rory Boyan", + "imageUrl":"\/\/d34k5cjnk2rcze.cloudfront.net\/images\/artists\/rory.jpg", + "genre":"Jam\/Instrumental", + "location":"Lowell, MA", + "label":"Analog Studios", + "contactPhone":null, + "contactEmail":"roryboyan@yahoo.com", + "bio":"One of my best friends, Rory plays instrumental music in a genre all to his own. Combining elements of blues, reggae, and percussion into his guitar playing, Rory manages to create something unique to him, and him alone. If you like chilled out and inspiring music, then you found your man.", + "isActive":"1" + }, + { + "id":"4", + "name":"Laurent Bonetto", + "imageUrl":"\/\/d34k5cjnk2rcze.cloudfront.net\/images\/artists\/laurent-bonetto.jpg", + "genre":"Classical", + "location":"Providence. RI", + "label":"Analog Studios", + "contactPhone":"0", + "contactEmail":"lbonetto-at-yahoo.com", + "bio":"<p>While pursuing a scientific career, Laurent has always kept playing the piano as one of the main occupations of his life; a passion he has had since the age of 5. Since the age of 15, he has practiced with concert pianist Nathalie Bera-Tagrine, with whom he studies when he returns to France. Laurent has taken numerous masters classes in Europe and the US, participated in many concerts, and competitions, and has recorded two piano CDs.<\/p>\n", + "isActive":"1" + }, + { + "id":"5", + "name":"The Silks", + "imageUrl":"\/\/d34k5cjnk2rcze.cloudfront.net\/images\/artists\/the-silks.jpg", + "genre":"Blues\/Rock", + "location":"Providence, RI", + "label":null, + "contactPhone":null, + "contactEmail":"T.J.@email.com", + "bio":"The Silks are cool jazz rock band originating out of the Providence area.", + "isActive":"0" + }, + { + "id":"6", + "name":"Dave Flamand", + "imageUrl":"\/\/d34k5cjnk2rcze.cloudfront.net\/images\/artists\/dave-flamand.jpg", + "genre":"Acoustic\/Rock", + "location":"Block Island, RI", + "label":"Analog Studios", + "contactPhone":null, + "contactEmail":"dave@analogstudios.net", + "bio":"<p>Dave Flamand, a talented songwriter and lead singer for Analog, has performed on Block Island, RI and Newport, RI for a few years.  Original acoustic rock paired with powerful vocals best describes his tailored sound.  Dave accompanies himself with either an acoustic guitar or the piano when he sings his dynamic original songs.   Dave also has a craft of selecting and performing cover songs that define and truly envelope his own style.  New music releases from Dave Flamand can be expected soon.  He is currently enjoying the lovely comforts of Analog Studios recording studio in Newport, RI.<\/p>", + "isActive":"1" + }, + { + "id":"7", + "name":"Audio Kickstand", + "imageUrl":"\/\/d34k5cjnk2rcze.cloudfront.net\/images\/artists\/audio-kickstand.jpg", + "genre":"Jam\/Rock", + "location":"Glen, NH", + "label":null, + "contactPhone":null, + "contactEmail":null, + "bio":"A great rock and jam band from Glen, NH, Audio Kickstand really knows how to get you out of your seat and dancing! This band is a prominent fixture down at the Red Parka Pub and you can almost always hear them playing Monday nights there for Open Mic. You can visit their myspace page for more news and info.", + "isActive":"1" + }, + { + "id":"8", + "name":"Jay St", + "imageUrl":"\/\/d34k5cjnk2rcze.cloudfront.net\/images\/artists\/jay-st.jpg", + "genre":"Rock and Roll", + "location":"Fitchburg, MA", + "label":"Analog Studios", + "contactPhone":null, + "contactEmail":null, + "bio":"Hailing from the basement of the coolest house on the craziest cobblestone hill, in Fitchburg, MA, Jay St. was the party band of the Fitchburg scene during the years of 2003-2005. While actively playing out at Hoolingans bar, they were also well known for throwing some of the best parties. (Even during the winter!) Even though they are no more, their infamy lives on thanks to their recordings being unearthed and posted for all to enjoy. So, if the dude abides, then so do we. Oh yeah, mind if I do a J?", + "isActive":"1" + }, + { + "id":"9", + "name":"Various Artists", + "imageUrl":"\/\/d34k5cjnk2rcze.cloudfront.net\/images\/artists\/various-artists.jpg", + "genre":"Rock and Roll", + "location":null, + "label":"Analog Studios", + "contactPhone":null, + "contactEmail":null, + "bio":"This is a compilation profile for various recordings and musical compilations", + "isActive":"1" + }, + { + "id":"10", + "name":"Metal Wings", + "imageUrl":"\/\/d34k5cjnk2rcze.cloudfront.net\/images\/artists\/metal-wings.png", + "genre":"Hip Hop", + "location":"Boston, MA", + "label":"", + "contactPhone":"0", + "contactEmail":"steezsp@yahoo.com", + "bio":"<p>Metal Wings is Ryan Mialler, a Hip Hop producer, MC, and Yoga teacher.  He wrote "Another Fall Harvest" in a single week in August 2012. After years of dabbling in producing he finally committed to making a solo album when I decided to make "Spiritual Warfare" and had it produced down at Analog Studios.  Ryan is one of our good friends so please check out all his work on iTunes.<\/p>\n", + "isActive":"1" + }, + { + "id":"11", + "name":"FAVE", + "imageUrl":"\/\/d34k5cjnk2rcze.cloudfront.net\/images\/artists\/fave-band.png", + "genre":"Rock and Roll", + "location":"Newport, RI", + "label":"Analog Studios", + "contactPhone":"0", + "contactEmail":"davef.analog@gmail.com", + "bio":"FAVE is a three piece band featuring Dave Flamand on piano, Bill Bartholomew on drums, and Mason Dubois on bass. The group has a cinematic and orchestral sound that ushers listeners along on a soundscape journey. Their new album \"Nowadays\" was released on 6\/20\/2021. Check out their singles \"Nowadays\" and \"Zodiac\" where ever you get your music.", + "isActive":"1" + } +] \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.default/build.default.spec.js b/packages/plugin-renderer-lit/test/cases/build.default/build.default.spec.js new file mode 100644 index 000000000..cfedda961 --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.default/build.default.spec.js @@ -0,0 +1,253 @@ +/* + * Use Case + * Run Greenwood with an SSR route. + * + * User Result + * Should generate a Greenwood build for hosting a server rendered application. + * + * User Command + * greenwood build + * + * User Config + * {} + * + * User Workspace + * src/ + * components/ + * counter.js + * footer.js + * greeting.js + * pages/ + * artists.js + * templates/ + * app.html + */ +import chai from 'chai'; +import fs from 'fs'; +import { JSDOM } from 'jsdom'; +import path from 'path'; +import { getSetupFiles, getDependencyFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import request from 'request'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +describe('Build Greenwood With: ', function() { + const LABEL = 'Custom Lit Renderer for SSR'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + const hostname = 'http://127.0.0.1:8080'; + let runner; + + before(async function() { + this.context = { + publicDir: path.join(outputPath, 'public') + }; + runner = new Runner(); + }); + + describe(LABEL, function() { + + before(async function() { + const lit = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/*.js`, + `${outputPath}/node_modules/lit/` + ); + const litDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/decorators/*.js`, + `${outputPath}/node_modules/lit/decorators/` + ); + const litDirectives = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/directives/*.js`, + `${outputPath}/node_modules/lit/directives/` + ); + const litPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/package.json`, + `${outputPath}/node_modules/lit/` + ); + const litElement = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/*.js`, + `${outputPath}/node_modules/lit-element/` + ); + const litElementPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/package.json`, + `${outputPath}/node_modules/lit-element/` + ); + const litElementDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/decorators/*.js`, + `${outputPath}/node_modules/lit-element/decorators/` + ); + const litHtml = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/*.js`, + `${outputPath}/node_modules/lit-html/` + ); + const litHtmlPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/package.json`, + `${outputPath}/node_modules/lit-html/` + ); + const litHtmlDirectives = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/directives/*.js`, + `${outputPath}/node_modules/lit-html/directives/` + ); + // lit-html has a dependency on this + // https://github.com/lit/lit/blob/main/packages/lit-html/package.json#L82 + const trustedTypes = await getDependencyFiles( + `${process.cwd()}/node_modules/@types/trusted-types/package.json`, + `${outputPath}/node_modules/@types/trusted-types/` + ); + const litReactiveElement = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/*.js`, + `${outputPath}/node_modules/@lit/reactive-element/` + ); + const litReactiveElementDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/decorators/*.js`, + `${outputPath}/node_modules/@lit/reactive-element/decorators/` + ); + const litReactiveElementPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/package.json`, + `${outputPath}/node_modules/@lit/reactive-element/` + ); + + await runner.setup(outputPath, [ + ...getSetupFiles(outputPath), + ...lit, + ...litPackageJson, + ...litDirectives, + ...litDecorators, + ...litElementPackageJson, + ...litElement, + ...litElementDecorators, + ...litHtmlPackageJson, + ...litHtml, + ...litHtmlDirectives, + ...trustedTypes, + ...litReactiveElement, + ...litReactiveElementDecorators, + ...litReactiveElementPackageJson + ]); + + return new Promise(async (resolve) => { + setTimeout(() => { + resolve(); + }, 10000); + + await runner.runCommand(cliPath, 'serve'); + }); + }); + + let response = {}; + let artists = []; + let dom; + let aboutPageGraphData; + + before(async function() { + const graph = JSON.parse(await fs.promises.readFile(path.join(outputPath, 'public/graph.json'), 'utf-8')); + artists = JSON.parse(await fs.promises.readFile(new URL('./artists.json', import.meta.url), 'utf-8')); + + aboutPageGraphData = graph.filter(page => page.route === '/artists/')[0]; + + return new Promise((resolve, reject) => { + request.get(`${hostname}/artists/`, (err, res, body) => { + if (err) { + reject(); + } + + response = res; + response.body = body; + dom = new JSDOM(body); + + resolve(); + }); + }); + }); + + describe('Serve command with HTML route response', function() { + + it('should return a 200 status', function(done) { + expect(response.statusCode).to.equal(200); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers['content-type']).to.contain('text/html'); + done(); + }); + + it('should return a response body', function(done) { + expect(response.body).to.not.be.undefined; + done(); + }); + + it('the response body should be valid HTML from JSDOM', function(done) { + expect(dom).to.not.be.undefined; + done(); + }); + + it('should have one <style> tag in the <head>', function() { + const styles = dom.window.document.querySelectorAll('head > style'); + + expect(styles.length).to.equal(1); + }); + + it('should have no <script> tags in the <head>', function() { + const scripts = dom.window.document.querySelectorAll('head > script'); + + expect(scripts.length).to.equal(0); + }); + + it('should have the expected number of <tr> tags of content', function() { + const rows = dom.window.document.querySelectorAll('body > table tr'); + + // one heading and 11 for content + expect(rows.length).to.equal(12); + }); + + it('should have the expected number of <simple-greeting> components with expected text content', function() { + const greetings = dom.window.document.querySelectorAll('body > table tr simple-greeting'); + + expect(greetings.length).to.equal(11); + + greetings.forEach((greeting, index) => { + // it should not be the default of Somebody, since real data should be in there + expect(greeting.innerHTML).to.contain(`Hello, <!--lit-part-->${artists[index].name}`); + }); + }); + + it('should have the expected <title> content in the <head>', function() { + const title = dom.window.document.querySelectorAll('head > title'); + + expect(title.length).to.equal(1); + expect(title[0].textContent).to.equal('My App - /artists/'); + }); + + it('should have custom metadata in the <head>', function() { + const metaDescription = Array.from(dom.window.document.querySelectorAll('head > meta')) + .filter((tag) => tag.getAttribute('name') === 'description'); + + expect(metaDescription.length).to.equal(1); + expect(metaDescription[0].getAttribute('content')).to.equal('My App - /artists/ (this was generated server side!!!)'); + }); + + it('should be a part of graph.json', function() { + expect(aboutPageGraphData).to.not.be.undefined; + }); + + it('should have the expected menu and index values in the graph', function() { + expect(aboutPageGraphData.data.menu).to.equal('navigation'); + expect(aboutPageGraphData.data.index).to.equal(7); + }); + + it('should have expected custom data values in its graph data', function() { + expect(aboutPageGraphData.data.author).to.equal('Project Evergreen'); + expect(aboutPageGraphData.data.date).to.equal('01-01-2021'); + }); + }); + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + runner.stopCommand(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.default/greenwood.config.js b/packages/plugin-renderer-lit/test/cases/build.default/greenwood.config.js new file mode 100644 index 000000000..24dafff94 --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.default/greenwood.config.js @@ -0,0 +1,7 @@ +import { greenwoodPluginRendererLit } from '../../../src/index.js'; + +export default { + plugins: [ + greenwoodPluginRendererLit() + ] +}; \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.default/package.json b/packages/plugin-renderer-lit/test/cases/build.default/package.json new file mode 100644 index 000000000..15b06e848 --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.default/package.json @@ -0,0 +1,7 @@ +{ + "name": "plugin-prerender-lit-build-default", + "type": "module", + "dependencies": { + "lit": "^2.1.1" + } +} \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.default/src/components/footer.js b/packages/plugin-renderer-lit/test/cases/build.default/src/components/footer.js new file mode 100644 index 000000000..cad38627a --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.default/src/components/footer.js @@ -0,0 +1,49 @@ +import { css, html, LitElement } from 'lit'; + +class FooterComponent extends LitElement { + + constructor() { + super(); + this.version = '0.11.1'; + } + + static get styles() { + return css` + .footer { + background-color: #192a27; + min-height: 30px; + padding-top: 10px; + } + + h4 { + width: 90%; + margin: 0 auto; + padding: 0; + text-align: center; + } + + a { + color: white; + text-decoration: none; + } + + span.separator { + color: white; + } + `; + } + + render() { + const { version } = this; + + return html` + <footer class="footer"> + <h4> + <a href="/">Greenwood v${version}</a> <span class="separator">◈</span> <a href="https://www.netlify.com/">This site is powered by Netlify</a> + </h4> + </footer> + `; + } +} + +customElements.define('app-footer', FooterComponent); \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.default/src/components/greeting.js b/packages/plugin-renderer-lit/test/cases/build.default/src/components/greeting.js new file mode 100644 index 000000000..d871a7fda --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.default/src/components/greeting.js @@ -0,0 +1,24 @@ +import { html, css, LitElement } from 'lit'; + +export class SimpleGreeting extends LitElement { + static get styles() { + return css`p { color: blue }`; + } + + static get properties() { + return { + name: { type: String } + }; + } + + constructor() { + super(); + this.name = 'Somebody'; + } + + render() { + return html`<p>Hello, ${this.name}!</p>`; + } +} + +customElements.define('simple-greeting', SimpleGreeting); \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.default/src/pages/artists.js b/packages/plugin-renderer-lit/test/cases/build.default/src/pages/artists.js new file mode 100644 index 000000000..ad6792128 --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.default/src/pages/artists.js @@ -0,0 +1,84 @@ +import fs from 'fs'; +import { html } from 'lit'; +import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; +import '../components/greeting.js'; + +async function getTemplate(compilation, route) { + return html` + <html> + <head> + <meta name="description" content="${compilation.config.title} - ${route} (this was generated server side!!!)"/> + <style> + * { + color: blue; + } + + h1 { + width: 50%; + margin: 0 auto; + text-align: center; + color: red; + } + </style> + </head> + <body> + <h1>This heading was rendered server side!</h1> + <content-outlet></content-outlet> + </body> + </html> + `; +} + +async function getBody() { + const artists = JSON.parse(await fs.promises.readFile(new URL('../../artists.json', import.meta.url), 'utf-8')); + + return html` + <h1>Lit SSR response</h1> + <table> + <tr> + <th>ID</th> + <th>Name</th> + <th>Decription</th> + <th>Message</th> + <th>Picture</th> + </tr> + ${ + artists.map((artist) => { + const { id, name, bio, imageUrl } = artist; + + return html` + <tr> + <td>${id}</td> + <td>${name}</td> + <td>${unsafeHTML(bio)}</td> + <td> + <a href="http://www.analogstudios.net/artists/${id}" target="_blank"> + <simple-greeting .name="${name}"></simple-greeting> + </a> + </td> + <td><img src="${imageUrl}"/></td> + </tr> + `; + }) + } + </table> + `; +} + +async function getFrontmatter(compilation, route) { + return { + menu: 'navigation', + index: 7, + title: `${compilation.config.title} - ${route}`, + data: { + author: 'Project Evergreen', + date: '01-01-2021' + } + }; +} + +export { + getTemplate, + getBody, + getFrontmatter +}; \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.default/src/templates/app.html b/packages/plugin-renderer-lit/test/cases/build.default/src/templates/app.html new file mode 100644 index 000000000..645e6f305 --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.default/src/templates/app.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <meta-outlet></meta-outlet> + </head> + + <body> + <page-outlet></page-outlet> + </body> +</html> \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/build.prerender.getting-started.spec.js b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/build.prerender.getting-started.spec.js new file mode 100644 index 000000000..ce543163f --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/build.prerender.getting-started.spec.js @@ -0,0 +1,205 @@ +/* + * Use Case + * Run Greenwood build command with a static site and only prerendering the content (no JS!). Modeled after the + * Greenwood Getting Started repo. + * + * User Result + * Should generate a bare bones Greenwood build with correctly templated out HTML from a LitElement. + * + * User Command + * greenwood build + * + * User Config + * import { greenwoodPluginIncludeHTML } from '@greenwod/plugin-include-html'; + * + * { + * plugins: [{ + * greenwoodPluginRendererLit({ + * prerender: true + * }) + * }] + * } + * + * User Workspace + * src/ + * assets/ + * greenwood-logo.png + * components/ + * footer.js + * header.js + * pages/ + * blog/ + * first-post.md + * second-post.md + * index.md + * styles/ + * theme.css + * templates/ + * app.html + * blog.html + */ +import chai from 'chai'; +import { JSDOM } from 'jsdom'; +import path from 'path'; +import { runSmokeTest } from '../../../../../test/smoke-test.js'; +import { getDependencyFiles, getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +describe('Build Greenwood With Custom Lit Renderer for SSG prerendering: ', function() { + const LABEL = 'For SSG prerendering of Getting Started example'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + let runner; + + before(function() { + this.context = { + publicDir: path.join(outputPath, 'public') + }; + runner = new Runner(); + }); + + describe(LABEL, function() { + + before(async function() { + const lit = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/*.js`, + `${outputPath}/node_modules/lit/` + ); + const litDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/decorators/*.js`, + `${outputPath}/node_modules/lit/decorators/` + ); + const litDirectives = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/directives/*.js`, + `${outputPath}/node_modules/lit/directives/` + ); + const litPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/package.json`, + `${outputPath}/node_modules/lit/` + ); + const litElement = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/*.js`, + `${outputPath}/node_modules/lit-element/` + ); + const litElementPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/package.json`, + `${outputPath}/node_modules/lit-element/` + ); + const litElementDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/decorators/*.js`, + `${outputPath}/node_modules/lit-element/decorators/` + ); + const litHtml = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/*.js`, + `${outputPath}/node_modules/lit-html/` + ); + const litHtmlPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/package.json`, + `${outputPath}/node_modules/lit-html/` + ); + const litHtmlDirectives = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/directives/*.js`, + `${outputPath}/node_modules/lit-html/directives/` + ); + const litReactiveElement = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/*.js`, + `${outputPath}/node_modules/@lit/reactive-element/` + ); + const litReactiveElementDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/decorators/*.js`, + `${outputPath}/node_modules/@lit/reactive-element/decorators/` + ); + const litReactiveElementPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/package.json`, + `${outputPath}/node_modules/@lit/reactive-element/` + ); + const litHtmlSourceMap = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/lit-html.js.map`, + `${outputPath}/node_modules/lit-html/` + ); + const trustedTypes = await getDependencyFiles( + `${process.cwd()}/node_modules/@types/trusted-types/package.json`, + `${outputPath}/node_modules/@types/trusted-types/` + ); + + await runner.setup(outputPath, [ + ...getSetupFiles(outputPath), + ...lit, + ...litPackageJson, + ...litDirectives, + ...litDecorators, + ...litElementPackageJson, + ...litElement, + ...litElementDecorators, + ...litHtmlPackageJson, + ...litHtml, + ...litHtmlDirectives, + ...trustedTypes, + ...litReactiveElement, + ...litReactiveElementDecorators, + ...litReactiveElementPackageJson, + ...litHtmlSourceMap + ]); + await runner.runCommand(cliPath, 'build'); + }); + + runSmokeTest(['public', 'index'], LABEL); + + describe('<head> of the page with data-gwd-opt="static" script tags removed', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); + }); + + it('should have expected footer <h4> tag content in the <body>', function() { + const scripTags = dom.window.document.querySelectorAll('body script'); + + expect(scripTags.length).to.be.equal(0); + }); + }); + + describe('LitElement <app-header> statically rendered into index.html', function() { + let body; + + before(async function() { + const dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); + + body = dom.window.document.querySelector('body'); + }); + + it('should have expected footer <h4> tag content in the <body>', function() { + const html = body.innerHTML.trim(); + + expect(html).to.contain('<header>'); + expect(html).to.contain('This is the header component.'); + }); + }); + + describe('LitElement <app-footer> statically rendered into index.html', function() { + let body; + + before(async function() { + const dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); + + body = dom.window.document.querySelector('body'); + }); + + it('should have expected footer <h4> tag content in the <body>', function() { + const html = body.innerHTML.trim(); + + expect(html).to.contain('<footer>'); + expect(html).to.contain('My Blog'); + expect(html).to.contain('2022'); + }); + }); + + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + }); +}); \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/greenwood.config.js b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/greenwood.config.js new file mode 100644 index 000000000..4b8dff4c9 --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/greenwood.config.js @@ -0,0 +1,9 @@ +import { greenwoodPluginRendererLit } from '../../../src/index.js'; + +export default { + plugins: [ + greenwoodPluginRendererLit({ + prerender: true + }) + ] +}; \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/package.json b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/package.json new file mode 100644 index 000000000..fe421bf5b --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/package.json @@ -0,0 +1,7 @@ +{ + "name": "plugin-prerender-lit-build-prerender-getting-started", + "type": "module", + "dependencies": { + "lit": "^2.1.1" + } +} \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/assets/greenwood-logo.png b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/assets/greenwood-logo.png new file mode 100644 index 000000000..810038a9d Binary files /dev/null and b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/assets/greenwood-logo.png differ diff --git a/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/components/footer.js b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/components/footer.js new file mode 100644 index 000000000..991a9ebb3 --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/components/footer.js @@ -0,0 +1,16 @@ +import { html, LitElement } from 'lit'; + +class FooterComponent extends LitElement { + + render() { + const year = '2022'; + + return html` + <footer> + <h4>My Blog ${year}</h4> + </footer> + `; + } +} + +customElements.define('app-footer', FooterComponent); \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/components/header.js b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/components/header.js new file mode 100644 index 000000000..ee80bec65 --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/components/header.js @@ -0,0 +1,15 @@ +import { html, LitElement } from 'lit'; + +class HeaderComponent extends LitElement { + + render() { + return html` + <header> + <h1>This is the header component.</h1> + </header> + `; + } + +} + +customElements.define('app-header', HeaderComponent); \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/pages/blog/first-post.md b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/pages/blog/first-post.md new file mode 100644 index 000000000..f22e2ce8f --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/pages/blog/first-post.md @@ -0,0 +1,8 @@ +--- +template: 'blog' +--- + +## My First Blog Post +Lorem Ipsum + +[back](/) \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/pages/blog/second-post.md b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/pages/blog/second-post.md new file mode 100644 index 000000000..02a7c548e --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/pages/blog/second-post.md @@ -0,0 +1,8 @@ +--- +template: 'blog' +--- + +## My Second Blog Post +Lorem Ipsum + +[back](/) \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/pages/index.md b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/pages/index.md new file mode 100644 index 000000000..b38fc3cfd --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/pages/index.md @@ -0,0 +1,7 @@ +## Home Page + +This is the Getting Started home page! + +### My Posts +- [my-second-post](/blog/second-post/) +- [my-first-post](/blog/first-post/) diff --git a/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/styles/theme.css b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/styles/theme.css new file mode 100644 index 000000000..855187042 --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/styles/theme.css @@ -0,0 +1,7 @@ +@import url('//fonts.googleapis.com/css?family=Source+Sans+Pro&display=swap'); + +* { + margin: 0; + padding: 0; + font-family: 'Source Sans Pro', sans-serif; +} \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/templates/blog.html b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/templates/blog.html new file mode 100644 index 000000000..a93c27bde --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/templates/blog.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/components/footer.js" data-gwd-opt="static"></script> + <script type="module" src="/components/header.js" data-gwd-opt="static"></script> + + <link rel="stylesheet" href="/styles/theme.css"></link> + </head> + + <body> + + <div class="gwd-content-outlet"> + <div class='container'> + <app-header></app-header> + <h1>A Blog Post Page</h1> + <content-outlet></content-outlet> + <app-footer></app-footer> + </div> + </div> + + </body> + +</html> \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/templates/page.html b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/templates/page.html new file mode 100644 index 000000000..764f3f014 --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/templates/page.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/components/footer.js" data-gwd-opt="static"></script> + <script type="module" src="/components/header.js" data-gwd-opt="static"></script> + + <link rel="stylesheet" href="/styles/theme.css"></link> + + <style> + section { + margin: 0 auto; + width: 70%; + } + </style> + + </head> + + <body> + + <div class="gwd-content-outlet"> + <div> + <app-header></app-header> + <content-outlet></content-outlet> + <app-footer></app-footer> + </div> + </div> + + </body> + +</html> \ No newline at end of file diff --git a/packages/plugin-typescript/README.md b/packages/plugin-typescript/README.md index f5e275d5e..20a2792ba 100644 --- a/packages/plugin-typescript/README.md +++ b/packages/plugin-typescript/README.md @@ -65,7 +65,7 @@ This plugin provides the following default `compilerOptions`. "moduleResolution": "node", "sourceMap": true } -} +} ``` If you would like to extend / override these options: @@ -74,7 +74,7 @@ If you would like to extend / override these options: ```json { "compilerOptions": { - "expirementalDecorators": true + "experimentalDecorators": true } } ``` @@ -94,4 +94,4 @@ If you would like to extend / override these options: } ``` -This will then process your JavaScript with TypeScript with the additional configurated settings you provide. This also allows you to configure the rest of _tsconfig.json_ to support your IDE and local development environment settings. \ No newline at end of file +This will then process your JavaScript with TypeScript with the additional configuration settings you provide. This also allows you to configure the rest of _tsconfig.json_ to support your IDE and local development environment settings. \ No newline at end of file diff --git a/packages/plugin-typescript/package.json b/packages/plugin-typescript/package.json index 6313585dd..49a9ca9a1 100644 --- a/packages/plugin-typescript/package.json +++ b/packages/plugin-typescript/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-typescript", - "version": "0.22.1", + "version": "0.23.0-alpha.1", "description": "A Greenwood plugin for writing TypeScript.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-typescript", "author": "Owen Buckley <owen@thegreenhouse.io>", @@ -28,6 +28,6 @@ "typescript": "^4.3.5" }, "devDependencies": { - "@greenwood/cli": "^0.22.1" + "@greenwood/cli": "^0.23.0-alpha.1" } } diff --git a/test/smoke-test.js b/test/smoke-test.js index d0d9970ff..33a4ccc7e 100644 --- a/test/smoke-test.js +++ b/test/smoke-test.js @@ -21,7 +21,7 @@ function commonIndexSpecs(dom, html, label) { describe('document <html>', function() { it('should have an <html> tag with the DOCTYPE attribute', function() { - expect(html.indexOf('<!DOCTYPE html>')).to.be.equal(0); + expect(html.replace(/<!--*.*-->/, '').indexOf('<!DOCTYPE html>')).to.be.equal(0); }); it('should have a <head> tag with the lang attribute on it', function() { diff --git a/www/assets/blog-images/ssr.webp b/www/assets/blog-images/ssr.webp new file mode 100644 index 000000000..6c207f59c Binary files /dev/null and b/www/assets/blog-images/ssr.webp differ diff --git a/www/components/banner/banner.js b/www/components/banner/banner.js index ca1e54179..8981ae0d1 100644 --- a/www/components/banner/banner.js +++ b/www/components/banner/banner.js @@ -54,8 +54,8 @@ class Banner extends LitElement { <div class='banner'> <eve-container> <div class='content'> - <img - src="../../assets/greenwood-logo-300w.png" + <img + src="../../assets/greenwood-logo-300w.png" alt="Greenwood Logo" srcset="../../assets/greenwood-logo-300w.png 1x, ../../assets/greenwood-logo-500w.png 2x, @@ -63,7 +63,7 @@ class Banner extends LitElement { ../../assets/greenwood-logo-1000w.png 4x, ../../assets/greenwood-logo-1500w.png 5x"/> - <h3>The static site generator for your. . . <br /><span class="${this.animateState}">${currentProjectType}.</span></h3> + <h3>Ready to help you build your next. . . <br /><span class="${this.animateState}">${currentProjectType}.</span></h3> <eve-button size="md" href="/getting-started/" style="${buttonCss}">Get Started</eve-button> </div> diff --git a/www/package.json b/www/package.json index 3c79eb42c..b5d547571 100644 --- a/www/package.json +++ b/www/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/www", - "version": "0.22.1", + "version": "0.23.0-alpha.1", "private": true, "type": "module", "description": "Greenwood website workspace.", @@ -10,7 +10,7 @@ "dependencies": { "@evergreen-wc/eve-button": "^0.1.1", "@evergreen-wc/eve-container": "^0.1.1", - "lit": "^2.0.0", + "lit": "^2.1.1", "prismjs": "^1.21.0" }, "devDependencies": { diff --git a/www/pages/about/community.md b/www/pages/about/community.md index e547e0039..30b02970f 100644 --- a/www/pages/about/community.md +++ b/www/pages/about/community.md @@ -7,12 +7,13 @@ index: 3 ## Community -Greenwood understands the role of community in open source, technology, and learning and we want to embrace that in this project as well. There are many great tools and projects available in the web ecosystem that we want to highlight here for their importance and influence towards the development of Greenwood (in no particular order). +Greenwood understands the role of community in open source, technology, and learning, and so we want to embrace that in this project as well. There are many great tools and projects available in the web ecosystem that we want to highlight here for their importance and influence towards the development of Greenwood and the web dev community at large. - [**MDN**](https://developer.mozilla.org/) - Mozilla Developer Network, the one reference documentation site to rule them all. - [**Rollup**](https://rollupjs.org/) - For helping push the bundler world forward towards (shipping) ESM - [**Snowpack**](https://www.snowpack.dev/) - Evangelizing and advocacy for ESM (particularly in development) has really helped push the industry forward in such a short amount of time - [**OpenWC**](https://open-wc.org/) - Taking web developers from zero to hero with Web Components. +- [Web Components Community Group](https://github.com/w3c/webcomponents-cg) - This group is helping to bridge the gap for users of Web Components through its efforts to catalogue and promote developer needs like presenting at TPAC and promoting discussion around community protocols that could bring us "standards" around HMR, routing, and more! - [**Netlify**](https://www.netlify.com/) - One of the innovators in the Jamstack era! - [**Rome**](https://rome.tools/) - As about as non meta-framework as they come. No doubt the spirit of this project is contagious. - [**Parcel**](https://parceljs.org/) - HTML as the entry point. Huh, wish we had thought of that. @@ -21,4 +22,4 @@ Greenwood understands the role of community in open source, technology, and lear And of course we can't forget our [Contributors](https://github.com/ProjectEvergreen/greenwood/graphs/contributors)! -So please come join our community and help us grow and advance the web together! We can't wait to see what you come up with! 💡 +So please come join our community and help us grow and advance the web together! We can't wait to see what you come up with! 💡 \ No newline at end of file diff --git a/www/pages/about/features.md b/www/pages/about/features.md index 225cda9e9..16207a4c4 100644 --- a/www/pages/about/features.md +++ b/www/pages/about/features.md @@ -9,20 +9,19 @@ linkheadings: 3 ## Features ### Easy Onboarding -We built Greenwood in the hopes that getting started would be easy. By default Greenwood will build an app for you. Just simply start adding pages and customizing templates as needed and you're good to go! Greenwood makes as few assumptions as needed to deliver an optimal development experience with minimum configuration needed. +We built Greenwood in the hopes that getting started would be easy. By default Greenwood will build an app for you. Just start with some HTML by adding pages and customizing templates and you're good to go! Greenwood makes as few assumptions as needed to deliver an optimal development experience with minimum configuration needed or work from you. -We strive to provide good documentation, intuitive developer experiences, and stable workflows. Even if you don't know anything about Modules or Web Components, if you can learn a little markdown and some HTML / CSS, you can get started making a modern website right away! +We strive to provide good documentation, intuitive developer experiences, and stable workflows. Even if you don't know anything about ESM or Web Components, if you can learn a little markdown and some HTML / CSS, you can get started making a modern website right away! ### Modern Apps, Modern Workflows -At the heart of Greenwood is an "evergreen" build, that aims to deliver the most optimized user experience through a combination of techniques likes web hints, modern JavaScript and CSS, and sensible defaults. +At the heart of Greenwood is an "evergreen" build, that aims to deliver the most optimized user experience through a combination of techniques likes web hints, modern JavaScript and CSS, and sensible defaults. -During development, we keep things lean and tooling free (relatively) by crawling your project's _package.json_ for all your dependencies and then generating an [`importMap`](https://github.com/WICG/import-maps) from that to resolve dependencies on the fly without the need for bundling. During production, we optimize and minify your code and get it ready to deploy to the web. +For example, during development, we keep things lean and tooling free (relatively) by crawling your project's _package.json_ for all your dependencies and then generating an [`importMap`](https://github.com/WICG/import-maps) from that to resolve dependencies on the fly without the need for up front bundling. During production, we optimize and minify your code and get it ready to deploy to the web. > _You can vist [this page](/about/how-it-works/) to learn more about how Greenwood works under the hood._ ### Performance We believe delivering a great user experience is above all else the most crucial element to a successful web project and part of that means performance out of the box. Greenwood wants to help your site be one of the fastest out there and so we'll take care of all those optimizations for you, ensuring your site gets a great score in tools like [Lighthouse](https://developers.google.com/web/tools/lighthouse/), one of our primary performance benchmarking tools. - -Haven't given Greenwood a try yet? Check out our [Getting Started](/getting-started) guide and start building your next modern web experience! 💯 \ No newline at end of file +Haven't given Greenwood a try yet? Check out our [Getting Started](/getting-started/) guide and start building your next modern web experience! 💯 \ No newline at end of file diff --git a/www/pages/about/goals.md b/www/pages/about/goals.md index 0a710911b..a5daf908a 100644 --- a/www/pages/about/goals.md +++ b/www/pages/about/goals.md @@ -7,16 +7,18 @@ index: 0 ## Goals -#### It's Not About Us -First and foremost, Greenwood aims to be developer friendly. We want to provide you the capabilities and the right amount of tooling and options to help accelerate your workflow and build your project. We value documentation and transparency into our project's roadmap and decision making, and encourage all to participate in our [GitHub](https://github.com/ProjectEvergreen/greenwood) issues, discussions, and pull requests. We want to hear from you about what you think would make Greenwood better! +#### It's About The Web +First and foremost, Greenwood aims to be welcoming and user friendly and for us that means leveraging the web platform for everything it provides. Too much magic and trust in _node_modules_ can be both a blessing and a curse, and Greenwood wants to help you walk that line a little more safely. If there is a web standard, you can be assured it's supported by Greenwood out of the box and if there isn't, you can probably make a plugin for it! +With the right amount of tooling and options to help accelerate your workflow and projects, _all you need is web_. #### Continuous Learning -In addition, we want Greenwood to be a project that everyone can have access to and be able to use knowing only the shared knowledge base of the web platform. We love Web Components, Flex Box, EMCAScript modules and everything else the web has to offer, and want to make sure that delivering modern apps for a modern web is painless and easy for everyone. As the web grows and evolves, so will Greenwood. Our desire to learn all about the web will help ensure Greenwood is always able to deliver the best experiences for users Greenwood, and of the web. +We want Greenwood to be a project that everyone can use without pre-existing knowledge of libraries and frameworks. Start with that _index.html_ and work your way up to a completely server rendered application if you need! We love Web Components, Flex Box, EMCAScript Modules and everything else the web has to offer, and want to make sure that delivering modern apps for a modern web is painless and easy for everyone. + +As the web grows and evolves, so will Greenwood. The team's desire to learn all about the web will help ensure Greenwood is always able to deliver the best experience for users and creators on the web. #### Collaborative -We greatly encourage contributions to our documentation and website to make sure everything is as clear and useful as possible and keep us honest in providing the the best developer experience possible. Please feel free to engage us in GitHub, Twitter, or any of our other outlets. (check out the site header for ways you can connect with the team, we'd love to hear from you! ❤️) -Please see our [roadmap](https://github.com/ProjectEvergreen/greenwood/projects) on GitHub to checkout what we have planned next! \ No newline at end of file +We value documentation and transparency into our project's [roadmap and decision making](https://github.com/ProjectEvergreen/greenwood/projects), and encourage all to participate in our [GitHub repo's](https://github.com/ProjectEvergreen/greenwood) issues, discussions, and pull requests. We greatly encourage contributions to our documentation and website to make sure everything is as clear and useful as possible and to keep us honest in providing the best developer experience possible. Please feel free to engage us in GitHub, Twitter, or any of our other outlets. We want to hear from you about what you think would make Greenwood better! \ No newline at end of file diff --git a/www/pages/about/how-it-works.md b/www/pages/about/how-it-works.md index ecb30500c..2cc66c401 100644 --- a/www/pages/about/how-it-works.md +++ b/www/pages/about/how-it-works.md @@ -10,42 +10,43 @@ linkheadings: 3 ### Philosophy -At its heart, Greenwood is all about web standards. With the browser becoming such a powerful tool, especially with the advent of [ECMAScript Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) (ESM) now having ubiquitous support in modern browsers, an entirely new workflow paradigm has emerged in which the browser can do more of the heavy lifting in our web dev workflows. In this way, less tooling and dependencies are needed to achieve excellent local development workflows as well as needing less overhead to maintain that stack. +At its heart, Greenwood is all about web standards. With the browser becoming such a powerful tool, especially with the advent of [ECMAScript Modules (ESM)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) now having ubiquitous support in modern browsers, an entirely new workflow paradigm has emerged in which the browser can do more of the heavy lifting in our web dev workflows. In this way, less tooling and dependencies are needed to achieve excellent local development workflows as well as needing less overhead to maintain that stack. Greenwood wants to take advantage of this opportunity to join in with other projects that are re-evaluating the landscape and thinking of a more unbundled world. One with less reliance on across the board bundling and transpilation, and one that just transforms on the fly only when needed. This paradigm lends itself really well to speedy local development workflows as well as benefiting end users who can be shipped more modern code. And for developers, now the code you write might actually look familiar while debugging in your browser! ### CLI -To actually use Greenwood, everyone is required to install the CLI. The CLI is what powers all the workflows available by Greenwood and builds your project for local development and production builds. It is plugin based so that it can be extended by users to support additional workflows not intended to be maintained in core. +To actually use Greenwood, users will interact with the Greenwood CLI. The CLI is what powers all the workflows available by Greenwood and builds your project for local development and production builds. It supports a configuration file that can be extended with plugin so that it can be extended to support additional workflows not intended to be maintained in core. During _development_ the CLI will: - Instantaneously start a local web server with live reload. - Process requests on the fly only for the content or code you need for a given page. - Supports loading dependencies from _node_modules_ using an [`importMap`](https://github.com/WICG/import-maps) to avoid bundling. -- While Greenwood is ESM first, we have a [plugin](/plugins/custom-plugins) to transform CommonJS into ESM (🤞) +- While Greenwood is ESM first, we have a [plugin](/plugins/custom-plugins/) to transform CommonJS into ESM (🤞) For _production_ builds: - Combine all your code and dependencies into efficient modern bundles including minifying your JavaScript and CSS. - Optimizes loading of JavaScript and CSS assets using web hints like `preload` and `prefetch`. -- All JavaScript (Web Components) are pre-rendered to static HTML (using **puppeteer**) for web standards templating with no runtime cost. -- Can [output](docs/config#mode) a standard static site (SSG), a multi-page application (MPA), or single-page application (SPA). -- Supports [further optimization](docs/config#optimization) for additional hints like inlining or only statically pre-rendering JavaScript. +- Provides the option to pre-render your JavaScript (e.g. Web Components) to static HTML (using **puppeteer** or the server rendering solution of your choice) to provide a web standards based templating solution. +- Can [support](/docs/layouts/) a static site (SSG) or server rendered site (SSR), or a hybrid of the two! Single Page Application's (SPA) also welcome! +- Supports [further optimization](/docs/config#optimization) for additional hints like inlining or only statically pre-rendering JavaScript with no runtime cost. Lastly, Greenwood aims to be a low point of friction as part of a standard development workflow. In this way, there will be a balance between what tools and dependencies are considered core to Greenwood. We aim to avoid the common "meta" framework paradigm and instead want to hone in on a lean and efficient core with good extension points for longer term maintainability and technical design. ### Plugins -The Greenwood CLI will aim to support development for all modern web standards and file types out of the box, in addition to markdown. Otherwise, additional languages and tools can be added to extend the core development experience using Greenwood's [plugin](/plugins/) system. In fact, Greenwood even maintains a few of its own to help you get started! In this way the Greenwood team aims to keep a strong focus on a core experience that everyone will benefit from no matter what their building, while allowing a DIY / BYOP (bring your own plugin) workflow that anyone can use. +The Greenwood CLI will aim to support development for all modern web standards and file types out of the box, in addition to markdown. Otherwise, Greenwood can be extended through plugins. In fact, Greenwood even maintains a [few of its own plugins](/plugins/) to help you get started! In this way the Greenwood team aims to keep a strong focus on a core experience that everyone will benefit from no matter what their building, while allowing a DIY / BYOP (bring your own plugin) workflow that anyone can use. ### Browser Support -For when transpilation is desired (Babel, PostCSS), Greenwood recommends using an **"evergreen build"** approach that ensures that the code delivered to users is as modern as modern as possible, with the least amount of processing and tranformations applied. Greenwood has two [plugins](/plugins/) that already support taking advantage of the two amazing tools that makes this all possible; [**Browserslist**](https://github.com/browserslist/browserslist) and [caniuse.com](https://caniuse.com/). + +Greenwood aims to support all modern evergreen browsers out of the box and so advocates for a bundleless, untranspiled workflow by default. For when transpilation is needed (Babel, PostCSS), Greenwood recommends using an **"evergreen build"** approach that ensures that the code delivered to users is as modern as modern as possible, with the least amount of processing and transformations applied. Greenwood has two [plugins](/plugins/) that already supports this recommending by taking advantage of two a great tools; [**Browserslist**](https://github.com/browserslist/browserslist) and [caniuse.com](https://caniuse.com/). - [**Babel**](https://babeljs.io/) is a compiler for JavaScript that transforms modern JavaScript down to a specific "target" of JavaScript. For example, source code can be written using 2018+ syntax, but transformed such that browsers that don't support that syntax can still run that JavaScript. -- [**PostCSS**](https://postcss.org/), much like **Babel** is a compiler, but for CSS! Just as with **Babel**, we can use modern CSS features without a transpilation process from a higher level version of CSS (LESS, SASS). CSS has finally arrived in modern web applications! ✨ +- [**PostCSS**](https://postcss.org/), much like **Babel** is a compiler, but for CSS! Just as with **Babel**, we can use modern CSS features without a transpilation process from a higher level version of CSS (LESS, SASS). -Using the above tools and leveraging their respective `env` presets available, essentially, **Browserlist** will query CanIUse data to determine, based on the browser query provided, what features are / aren't needed for transpilation. This in turn allows Babel and PostCSS to intelligenty transpile _only_ what's needed for the features that are missing from the browser you are targeting, thus ensuring an "evergreen" experience for users _and_ developers. Nice. 😎 +Using the above tools and leveraging their respective `env` presets available, essentially, **Browserlist** will query CanIUse data to determine, based on the browser query provided, what features are / aren't needed for transpilation. This in turn allows Babel and PostCSS to intelligently transpile _only_ what's needed for the features that are missing from the browser you are targeting, thus ensuring an "evergreen" experience for users _and_ developers. Nice. 😎 So for example, a _.browserslistrc_ that looks like this: ```shell @@ -65,4 +66,4 @@ firefox 61 ios_saf 11.3-11.4 ios_saf 11.0-11.2 safari 11.1 -``` +``` \ No newline at end of file diff --git a/www/pages/about/index.md b/www/pages/about/index.md index d85c41b8c..d02500819 100644 --- a/www/pages/about/index.md +++ b/www/pages/about/index.md @@ -7,7 +7,6 @@ index: 1 ## About Greenwood -Greenwood's goal is to make web development easier, more accessible, and fun for all. By diligently adhering to web standards and building an optimized workflow around those open technologies, Greenwood promises to provide an easy learning experience that can grow to scale up to any skillset or project. Whether you are building a static site or a single page application, a hobby project or passion project, Greenwood can help you accomplish _your goals_. +Greenwood's goal is to make web development easier, more accessible, and to put the fun into fundamentals. By diligently adhering to web standards and building an optimized workflow around those open technologies, Greenwood promises to provide an easy learning experience that can grow to scale up to any skillset or project. Whether you are building a blog or a single page application, a hobby project or passion project, Greenwood can help you accomplish _your goals_. - -The Greenwood team are working hards towards a [1.0 release](https://github.com/ProjectEvergreen/greenwood/milestone/3) and are eager to get there quickly to help expand Greenwood's features and capabilities. We want to make sure Greenwood is the best experience it can be, for users and developers. If you have [any issues](https://github.com/ProjectEvergreen/greenwood/issues) or are curious to see what we're [working on next](https://github.com/ProjectEvergreen/greenwood/projects), please feel free to checkout our [GitHub repo](https://github.com/ProjectEvergreen/greenwood) and poke around. Comments / issues / feedback welcome, we are open for contributions! 👋 +The Greenwood team are working hards towards a [1.0 release](https://github.com/ProjectEvergreen/greenwood/milestone/3) and are eager to get there quickly to help expand Greenwood's features and capabilities. We want to make sure Greenwood is the best experience it can be, for users and developers. If you have [any issues](https://github.com/ProjectEvergreen/greenwood/issues) or are curious to see what we're [working on next](https://github.com/ProjectEvergreen/greenwood/projects), please feel free to checkout our [GitHub repo](https://github.com/ProjectEvergreen/greenwood) and poke around. Comments / issues / feedback welcome, we are open for contributions! 👋 \ No newline at end of file diff --git a/www/pages/blog/index.md b/www/pages/blog/index.md index 323af2fb8..4b3622e36 100644 --- a/www/pages/blog/index.md +++ b/www/pages/blog/index.md @@ -19,6 +19,7 @@ template: blog # News and Announcements +- [Release: v0.23.0](/blog/release/v0-23-0/) 📝 - [Release: v0.21.0](/blog/release/v0-21-0/) 📝 - [Release: v0.20.0](/blog/release/v0-20-0/) 📝 - [Release: v0.19.0](/blog/release/v0-19-0/) 📝 diff --git a/www/pages/blog/release/v0-15-0.md b/www/pages/blog/release/v0-15-0.md index 9445249de..dd002c479 100644 --- a/www/pages/blog/release/v0-15-0.md +++ b/www/pages/blog/release/v0-15-0.md @@ -17,7 +17,7 @@ Being a developer is a lot of work. Being a designer is also lot of work. Bein With Greenwood [_**Theme Packs**_](https://www.greenwoodjs.io/guides/theme-packs/), now developers and designers can create and share reusable HTML / CSS / JS as npm packages that other Greenwood users can pull into their Greenwood projects as a plugin. Now anyone can get up and running with a fully designed and themed site and all they have to do is just add the content! 🥳 ## In Practice -For those unfamiliar with [**CSS Zen Garden**](http://www.csszengarden.com/), it is a site aimed at showcasing the power of CSS through static HTML. +For those unfamiliar with [**CSS Zen Garden**](http://www.csszengarden.com/), it is a site aimed at showcasing the power of CSS through static HTML. > _The HTML remains the same, the only thing that has changed is the external CSS file. Yes, really._ diff --git a/www/pages/blog/release/v0-18-0.md b/www/pages/blog/release/v0-18-0.md index 45a9dc772..f1cfaf175 100644 --- a/www/pages/blog/release/v0-18-0.md +++ b/www/pages/blog/release/v0-18-0.md @@ -23,9 +23,9 @@ Neat! ## Not Found Page -Admittedly we are probably a little late to the party on this one, but thanks to the enthisuastic voices pushing for this one to be completed, now it's here. With first class support for a traditional Not Found (404) Page, Greenwood will now automatically generate a _404.html_ page for you as part of the build. Or if you provide one in the root of your _pages/_ directory, Greenwood will use that instead. +Admittedly we are probably a little late to the party on this one, but thanks to the enthusiastic voices pushing for this one to be completed, now it's here. With first class support for a traditional Not Found (404) Page, Greenwood will now automatically generate a _404.html_ page for you as part of the build. Or if you provide one in the root of your _pages/_ directory, Greenwood will use that instead. -For example, here's the Greenwood website [404 page](https://www.greenwoodjs.io/404.html). You'll notice that we are not on an active page in the site, and so most hosts, like ours (Netlify), will autatically serve the _404.html_! 🔍 +For example, here's the Greenwood website [404 page](https://www.greenwoodjs.io/404.html). You'll notice that we are not on an active page in the site, and so most hosts, like ours (Netlify), will automatically serve the _404.html_! 🔍 ![Not Found Page](/assets/blog-images/not-found.png) diff --git a/www/pages/blog/release/v0-19-0.md b/www/pages/blog/release/v0-19-0.md index ec6151097..cfc0bdc45 100644 --- a/www/pages/blog/release/v0-19-0.md +++ b/www/pages/blog/release/v0-19-0.md @@ -36,7 +36,7 @@ We have a lot of ideas and plans to add more capabilities to this command like s ## HTML Include Plugin -Greenwood loves the web. And we love JavaScript. What could be better than JavaScript? More JavaScrtipt surely!? Actually, we believe it's [more HTML](https://projectevergreen.github.io/blog/always-bet-on-html/) (and don't call me Shirley). With our new plugin, we hope to blend the best of both worlds! 🤝 +Greenwood loves the web. And we love JavaScript. What could be better than JavaScript? More JavaScript surely!? Actually, we believe it's [more HTML](https://projectevergreen.github.io/blog/always-bet-on-html/) (and don't call me Shirley). With our new plugin, we hope to blend the best of both worlds! 🤝 Our new plugin, [**plugin-include-html**](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-include-html) aims to follow in the spirit of the [abandoned HTML Imports spec](https://www.html5rocks.com/en/tutorials/webcomponents/imports/) that was originally part of the initial Web Components "feature suite", and gives developers two new ways to ship more _static_ HTML with NO client side JavaScript overhead incurred. @@ -47,7 +47,7 @@ So given a snippet of HTML ```html <header class="my-include"> <h1>Welcome to my website!<h1> -</header> +</header> ``` And a page template, you could then add this `<link>` tag @@ -59,7 +59,7 @@ And a page template, you could then add this `<link>` tag <link rel="html" href="/includes/header.html"></link> <h2>Hello 👋</h2> - + </body> <html> @@ -72,10 +72,10 @@ And Greenwood will statically generate this <body> <header class="my-include"> <h1>Welcome to my website!<h1> - </header> + </header> <h2>Hello 👋</h2> - + </body> <html> @@ -83,7 +83,7 @@ And Greenwood will statically generate this ### Custom Element (JavaScript) -For more advanced use cases where customization of the output may need to be done in a programmatic fashion and in supporting upcoming [SSR](https://github.com/ProjectEvergreen/greenwood/issues/708 based workflows, the custom element flavor supports declaring functions for providing markup and data that Greenwood will then build the HTML for on the fly. +For more advanced use cases where customization of the output may need to be done in a programmatic fashion and in supporting upcoming [SSR](https://github.com/ProjectEvergreen/greenwood/issues/708 based workflows, the custom element flavor supports declaring functions for providing markup and data that Greenwood will then build the HTML for on the fly. So using the [Greenwood footer as an example](https://github.com/ProjectEvergreen/greenwood/blob/master/www/includes/footer.js), have a JS file that exports two functions; `getTemplate` and `getData` ```js @@ -107,7 +107,7 @@ const getData = async () => { module.exports = { getTemplate, getData -}; +}; ``` In a page template, you can now do this with a custom element tag @@ -117,7 +117,7 @@ In a page template, you can now do this with a custom element tag <body> <h2>Hello 👋</h2> - <app-footer src="../includes/footer.js"></app-footer> + <app-footer src="../includes/footer.js"></app-footer> </body> <html> @@ -134,7 +134,7 @@ And Greenwood would statically generate this <footer class="footer"> <a href="/">Greenwood v0.19.0</a> </footer> - </app-footer> + </app-footer> </body> <html> diff --git a/www/pages/blog/release/v0-20-0.md b/www/pages/blog/release/v0-20-0.md index ab58fda53..7dd2cd3fd 100644 --- a/www/pages/blog/release/v0-20-0.md +++ b/www/pages/blog/release/v0-20-0.md @@ -14,13 +14,13 @@ So although there are no new features in [this release](https://github.com/Proje ## Why We Did It -It was a lot of work, and although ESM is not quite in a perfect place yet within the ecosystem (CJS <> ESM interop and the "dual module hazard"), there were two key motivations for us that made us want to make the jump now, especially before htting a 1.0 release. +It was a lot of work, and although ESM is not quite in a perfect place yet within the ecosystem (CJS <> ESM interop and the "dual module hazard"), there were two key motivations for us that made us want to make the jump now, especially before hitting a 1.0 release. ### Browser Parity ♻️ As Greenwood expands past just static sites with our upcoming plans to add support for [**Server Side Rendering**](https://github.com/ProjectEvergreen/greenwood/issues/708) and [**External Data Sources**](https://github.com/ProjectEvergreen/greenwood/issues/21), user's would be able to start writing server side code within their project's workspace. This meant there would be NodeJS and browser code right next to each other, and as part of Greenwood's [mission to make writing sites for the web easier](/about/), taking advantage of a consolidated module system makes perfect sense for developer experience. We want the NodeJS code you have to write to be as close to the code that you write for the browser, and so for Greenwood, this means supporting ESM in NodeJS. ### Server Rendering 🚀 -Additionally, libraries like [**Lit**](https://lit.dev/) that provide [support for SSR](https://github.com/lit/lit/tree/main/packages/labs/ssr) are themselves written in ESM and unfortutely because interop between CJS and ESM doesn't go both ways, we would not be able to support these projects if we stayed on CJS. For this reason, the first party code by users will need to be written in ESM. we also expect more packages to become ESM first / only, and so this helps us get ahead of an eventual migration anyway. +Additionally, libraries like [**Lit**](https://lit.dev/) that provide [support for SSR](https://github.com/lit/lit/tree/main/packages/labs/ssr) are themselves written in ESM and unfortunately because interop between CJS and ESM doesn't go both ways, we would not be able to support these projects if we stayed on CJS. For this reason, the first party code by users will need to be written in ESM. we also expect more packages to become ESM first / only, and so this helps us get ahead of an eventual migration anyway. ## Upgrade Path diff --git a/www/pages/blog/release/v0-23-0.md b/www/pages/blog/release/v0-23-0.md new file mode 100644 index 000000000..cd7151bc0 --- /dev/null +++ b/www/pages/blog/release/v0-23-0.md @@ -0,0 +1,121 @@ +--- +label: 'blog' +title: v0.23.0 Release +template: blog +--- + +# Greenwood v0.23.0 + +**Published: Feb 11, 2022** + +## What's New + +With this new release, the Greenwood team is excited to (soft) launch the ability to add Server Side Rendering (SSR) to your Greenwood project as well as support for using a custom renderer like [**Lit** SSR](https://www.npmjs.com/package/@lit-labs/ssr). Additionally, to enhance the ability of purely static sites to benefit from some build time templating, a new feature called "interpolate frontmatter" was introduced to easily reuse frontmatter similar to how you would use JavaScript interpolation, but in your HTML and markdown. Let's highlight them both below! 👇 + +## Server Side Rendering (SSR) + +As mentioned above, we are soft launching the ability to incorporate server rendering into your Greenwood projects. By simply adding a JavaScript file to your project, you will be able to have server rendered content available when running `greenwood serve`. You can also combine static and server rendered content all in the same project for a hybrid application! Let's take a look at a quick example. + +### How It Works +You can add a file to your project in the _pages/_ directory and implement either of the three supported APIs, and you will have a server rendered route available! +```shell +. +└── src + └── pages + ├── artists.js + ├── about.md +   └── index.html +``` + +```js +// artists.js +import fetch from 'node-fetch'; // this needs to be installed from npm + +async function getBody(compilation) { + const artists = await fetch('http://www.example.com/api/artists').then(resp => resp.json()); + + return ` + <body> + <h1>Hello from the server rendered artists page! 👋</h1> + <table> + <tr> + <th>Name</th> + <th>Image</th> + </tr> + ${ + artists.map((artist) => { + const { name, imageUrl } = artist; + return ` + <tr> + <td>${name}</td> + <td><img src="${imageUrl}"/></td> + </tr> + `; + }); + } + </table> + </body> + ` +} + +export { getBody } +``` + +You can then access `/artists/` and see the content! 💥 + +![Server Side Rendering example](/assets/blog-images/ssr.webp) + +> _In the above screenshot, we can also see a demonstration of our custom rendering using LitSSR and the `<simple-greeting>` component._ + +## Interpolate Frontmatter +At the risk of (re) implementing a templating system (handlebars, nunjucks, etc) but still recognizing that having a JavaScript only solution though our [_graph.json_](/docs/data/) for static sites can be a bit cumbersome, the Greenwood team is introducing the `interpolateFrontmatter` feature. With this new feature, when setting the corresponding flag in your _greenwood.config.js_, frontmatter in your markdown will be available in your HTML or markdown similar to how variable interpolation works in JavaScript. Great for `<meta>` tags! + +### How It Works +So given the following frontmatter +```md +--- +template: 'post' +title: 'Git Explorer' +emoji: '💡' +date: '04.07.2020' +description: 'Local git repository viewer' +image: '/assets/blog-post-images/git.png' +--- +``` + +And enabling the feature in _greenwood.config.js_ +```js +export default { + interpolateFrontmatter: true +} +``` + +You access the frontmatter data in the markdown or HTML on a _per page instance_ following the convention of JavaScript [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals), and Greenwood will interpolate those values at build time. + +```md +# My Blog Post + +<img src="${globalThis.page.image}" alt="Banner image for ${globalThis.page.description}"> + +Lorum Ipsum. +``` + +```html +<html> + <head> + <title>My Blog - ${globalThis.page.title} + + + + + + + + + + +``` + +## Learn More + +To learn more about SSR and the full API please check out our docs on [SSR](/docs/server-rendering/) and [interpolateFrontmatter](/docs/config#interpolateFrontmatter). For custom SSR, we have [plugin docs](/plugins/renderer/) and a [Lit Renderer plugin](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-renderer-lit) you can start using. As referenced at the start of the blog post, the SSR feature is brand new and we have many plans to incorporate new features and enhancements related to [hydration, statically exporting content from server routes, and more](https://github.com/ProjectEvergreen/greenwood/issues?q=is%3Aissue+is%3Aopen+label%3Assr)! Feedback is appreciated and we cant wait to see what you end building! 🙏 \ No newline at end of file diff --git a/www/pages/docs/component-model.md b/www/pages/docs/component-model.md index c19fa7734..f2373a678 100644 --- a/www/pages/docs/component-model.md +++ b/www/pages/docs/component-model.md @@ -6,11 +6,11 @@ index: 1 --- ## Component Model -Greenwood aims to support and optimize around the standard capabilities of the web platform and its features. In particular, the concept of using Web Components as a way to add interactivity and dynamic content into your application and... that can all be prerendered for you, just like you could do with any server side templating language. +Greenwood aims to support and optimize around the standard capabilities of the web platform and its features. In particular, the concept of using Web Components as a way to add and isolate interactivity and dynamic content into your application and that it can all be prerendered for you, just like you could do with any server side templating language. The options for how to design your app effectively comes down to what you're trying to build, so if that's with the native `HTMLElement` or something based on it like **LitElement** (installed separately), **Greenwood** will take care of the rest. -Below are a couple examples to get you going. +Below are a couple examples to get you going. > _Check out our [README](https://github.com/ProjectEvergreen/greenwood#built-with-greenwood) for more examples of sites built with **Greenwood** to see what's possible._ @@ -59,7 +59,7 @@ You can then use it within a page template. - + @@ -71,7 +71,7 @@ You can then use it within a page template. ### Alternatives -An alternative like [**LitElement**](https://lit.dev/) would work the same way. +An alternative like [**LitElement**](https://lit.dev/) would work the same way. > _Make sure you have installed LitElement with **npm** first!_ @@ -102,7 +102,7 @@ customElements.define('x-greeting', GreetingComponent); - + diff --git a/www/pages/docs/configuration.md b/www/pages/docs/configuration.md index 73c85cb08..21bf41ab4 100644 --- a/www/pages/docs/configuration.md +++ b/www/pages/docs/configuration.md @@ -18,12 +18,14 @@ export default { port: 1984, host: 'localhost' }, + port: 8080, + interpolateFrontmatter: false, markdown: { plugins: [], settings: {} }, meta: [], - mode: 'ssg', + staticRouter: false, optimization: 'default', plugins: [], title: 'My App', @@ -36,16 +38,16 @@ export default { ### Dev Server Configuration for Greenwood's development server is available using the `devServer` option. - `extensions`: Provide an array of to watch for changes and reload the live server with. By default, Greenwood will already watch all "standard" web assets (HTML, CSS, JS, etc) it supports by default, as well as any extensions set by [resource plugins](/plugins/resource) you are using in your _greenwood.config.json_. -- `hud`: The HUD option ([_head-up display_](https://en.wikipedia.org/wiki/Head-up_display)) is some additional HTML added to your site's page when Greenwood wants to help provide information to you in the brwoser. For example, if your HTML is detected as malformed, which could break the parser. Set this to `false` if you would like to turn it off. +- `hud`: The HUD option ([_head-up display_](https://en.wikipedia.org/wiki/Head-up_display)) is some additional HTML added to your site's page when Greenwood wants to help provide information to you in the browser. For example, if your HTML is detected as malformed, which could break the parser. Set this to `false` if you would like to turn it off. - `port`: Pick a different port when starting the dev server -- `proxy`: A set of paths to match and re-route to other hosts. Highest specificty should go at the end. +- `proxy`: A set of paths to match and re-route to other hosts. Highest specificity should go at the end. #### Example ```js export default { devServer: { extensions: ['.txt', '.rtf'], - port: 8181, + port: 3000, proxy: { '/api': 'https://stage.myapp.com', '/api/foo': 'https://foo.otherdomain.net' @@ -54,6 +56,52 @@ export default { } ``` +### Interpolate Frontmatter + +To support simple static templating in HTML and markdown pages and templates, the `interpolateFrontmatter` option can be set to `true` to allow the following kinds of simple static substitions using a syntax convention based on JavaScript [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals). + +#### Example +Given some frontmatter in a markdown file: +```md +--- +template: post +title: Git Explorer +published: 04.07.2020 +description: Local git repository viewer +author: Owen Buckley +image: /assets/blog-post-images/git.png +--- +``` + +It can be accessed and substituted statically in either markdown or HTML. + +##### Markdown +```md +# My Blog Post + +Published: ${globalThis.page.published} + +Lorum Ipsum. +``` + +##### HTML +```html + + + My Blog - ${globalThis.page.title} + + + + + + + + + ... + + +``` + ### Markdown You can install and provide custom **unifiedjs** [presets](https://github.com/unifiedjs/unified#preset) and [plugins](https://github.com/unifiedjs/unified#plugin) to further customize and process your markdown past what [Greenwood does by default](https://github.com/ProjectEvergreen/greenwood/blob/release/0.10.0/packages/cli/src/transforms/transform.md.js#L68). After running an `npm install` you can provide their package names to Greenwood. @@ -110,23 +158,6 @@ Which would be equivalent to: ``` -### Mode - -Greenwood provides a couple different "modes" by which you can indicate the type of project your are making: - -| Option | Description | Use Cases | -| ------ | ----------- | --------- | -|`ssg` | (_Default_) Generates a pre-rendered statically generated website from [pages and templates](/docs/layouts/)at build time. | Blog, portfolio, anything really! | -|`mpa` | Assumes an `ssg` based site, but additionally adds a client side router to create a _Multi Page Application_. | Any `ssg` based site where content lines up well with templates to help with transition between similar pages, like blogs and documentation sites. | -|`spa` | For building and bundling a _Single Page Application (SPA)_ with client side routing and a [single _index.html_ file](/docs/layouts/#single-page-applications). | Any type of client side only rendered application. | - -#### Example -```js -export default { - mode: 'mpa' -} -``` - ### Optimization Greenwood provides a number of different ways to send hints to Greenwood as to how JavaScript and CSS tags in your HTML should get loaded by the browser. Greenwood supplements, and builds up on top of existing [resource "hints" like `preload` and `prefetch`](https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content). These optimization settings are intended to compliment any `mode` setting you may have selected. @@ -136,7 +167,7 @@ Greenwood provides a number of different ways to send hints to Greenwood as to h |`default` | Will add a `` tag for every ` - + - + - + ``` @@ -191,11 +191,13 @@ Greenwood will automatically generate a [default _404.html_](https://github.com/   └── 404.html ``` -It will be emitted to the output directory as a top level _404.html_, which is the [commmon convention](https://docs.netlify.com/routing/redirects/redirect-options/#custom-404-page-handling) for most hosts and web servers. +It will be emitted to the output directory as a top level _404.html_, which is the [common convention](https://docs.netlify.com/routing/redirects/redirect-options/#custom-404-page-handling) for most hosts and web servers. ### Single Page Applications -If you would like to build a SPA and only deal with client side rendering, Greenwood can [support that](/docs/configuration#mode)! As the name implies, your layout will be slightly different in this case. Below is an example layout of a SPA, and you can see a working example in our [test suite](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/test/cases/build.config.mode-spa) where we validate using [**lit-redux-router**](https://github.com/fernandopasik/lit-redux-router) with route based code splitting. +If you would like to build a SPA and only deal with client side rendering, Greenwood can support that too As the name implies, you will just need to have an _index.html_ file in your workspace (no _pages/_ directory) and that's it! + +Below is an example layout of a SPA, and you can see a working example in our [test suite](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/test/cases/build.config.mode-spa) where we validate using [**lit-redux-router**](https://github.com/fernandopasik/lit-redux-router) with route based code splitting. ```shell diff --git a/www/pages/docs/menus.md b/www/pages/docs/menus.md index 73b1cef27..6661c68e2 100644 --- a/www/pages/docs/menus.md +++ b/www/pages/docs/menus.md @@ -2,7 +2,7 @@ label: 'menus' menu: side title: 'Menus' -index: 8 +index: 9 linkheadings: 3 --- diff --git a/www/pages/docs/server-rendering.md b/www/pages/docs/server-rendering.md new file mode 100644 index 000000000..414aeb12f --- /dev/null +++ b/www/pages/docs/server-rendering.md @@ -0,0 +1,197 @@ +--- +label: 'server-rendering' +menu: side +title: 'Server Rendering' +index: 8 +linkheadings: 3 +--- + +## Server Rendering (Beta) + +In addition to supporting [static and Single Page application project types](/docs/layouts/), you can also use Greenwood to author routes completely in JavaScript and host these on a server. + +> 👉 _To run a Greenwood project with SSR routes for production, just use the [`serve` command](/docs/#cli)._ + +### Routes + +File based routing also applies to server routes. Just create JavaScript file in the _pages/_ directory and that's it! + +```shell +src/ + pages/ + users.js +greenwood.config.js +``` + +The above would serve content in a browser at `/users/`. + +### API + +In your _[page].js_ file, Greenwood supports three functions you can `export` for providing server rendered configuration and content: +- `getFrontmatter`: Static [frontmatter](/docs/front-matter/), useful in conjunction with [menus](/docs/menus/) or otherwise static configuration / meta data. +- `getBody`: Effectively anything that you could put into a [``](/docs/layouts/#page-templates). +- `getTemplate`: Effectively the same as a [page template](/docs/layouts/#page-templates). + +```js +async function getFrontmatter(compilation, route, label, id) { + return { /* ... */ }; +} + +async function getBody(compilation, route) { + return `/* some HTML here */`; +} + +async function getTemplate(compilation, route) { + return `/* some HTML here */`; +} + +export { + getFrontmatter, + getBody, + getTemplate +} +``` + +#### Frontmatter + +Any Greenwood supported frontmatter can be returned here. _This is only run once when the server is started_ to populate the graph, which is helpful if you want your dynamic route to show up in a menu like in your header for navigation. + +You can even define a `template` and reuse all your existing [templates](/docs/layouts/), even for server routes! + +```js +// example +async function getFrontmatter(compilation, route) { + return { + template: 'user', + menu: 'header', + index: 1, + title: `${compilation.config.title} - ${route}`, + imports: [ + '/components/user.js' + ], + data: { + /* ... */ + } + }; +} +``` + +> _For defining custom dynamic based metadata, like for `` tags, use `getTemplate` and define those tags right in your HTML._ + +#### Body + +For just returning content, you can use `getBody`. For example, return a list of users from an API as the HTML you need. + +```js +import fetch from 'node-fetch'; // this needs to be installed from npm + +async function getBody(compilation) { + const users = await fetch('http://www.example.com/api/users').then(resp => resp.json()); + const timestamp = new Date().getTime(); + const usersListItems = users + .map((user) => { + const { name, imageUrl } = user; + + return ` + + ${name} + + + `; + }); + + return ` + +

Hello from the server rendered users page! 👋

+ + + + + + ${usersListItems.join('')} +
NameImage
+
Fetched at: ${timestamp}
+ + `; +} +``` + +#### Templates + +For creating a template dynamically, you can use `getTemplate` and return the HTML you need. + +```js +async function getTemplate(compilation, route) { + return ` + + + + + + + +

This heading was rendered server side!

+ + + + `; +} +``` + +### Hybrid Projects + +One of the great things about Greenwood is that you can seamlessly move from completely static to server rendered, without giving up either one! 💯 + +Given the following workspace of just pages +```shell +src/ + pages/ + index.md + about.md +``` + +Greenwood would output the following static build output +```shell +public/ + about + index.html + index.html +``` + +Now, add a dynamic route and run `serve`... +```shell +src/ + pages/ + index.md + about.md + user.js +``` + +Greenwood will now build and serve all the static content from the _pages/_ directory as before _BUT_ will also start a server that will now fulfill requests to the newly added server rendered pages too. Neat! + +### Render vs Prerender + +Greenwood provides the ability to [prerender](/docs/prerender/) your project and Web Components using Puppeteer. So what is the difference between that and rendering? In the context of Greenwood, _rendering_ is the process of generating the _initial_ HTML as you would when running on a server. _Prerendering_ is the ability to execute exclusively browser code in a browser and capture that result as static HTML. + +So what does that mean, exactly? Basically, you can think of them as being complimentary, where in you might have server side routes that pull content server side (`getBody`), but can be composed of static HTML templates (in your _src/templates_ directory) that can have client side code (Web Components) with `