From 2016aa4982a41b8934ad5e7c25ffd09ee584c987 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Sun, 10 Jan 2021 14:53:41 -0500 Subject: [PATCH 01/24] initial transforms refactor using Resources approach --- packages/cli/src/lib/resource-interface.js | 40 +++ packages/cli/src/lifecycles/serve.js | 74 +++--- packages/cli/src/plugins/plugin-serve-html.js | 236 ++++++++++++++++++ 3 files changed, 317 insertions(+), 33 deletions(-) create mode 100644 packages/cli/src/lib/resource-interface.js create mode 100644 packages/cli/src/plugins/plugin-serve-html.js diff --git a/packages/cli/src/lib/resource-interface.js b/packages/cli/src/lib/resource-interface.js new file mode 100644 index 000000000..2665c1996 --- /dev/null +++ b/packages/cli/src/lib/resource-interface.js @@ -0,0 +1,40 @@ +class ResourceInterface { + constructor(compilation, options = {}) { + this.type = 'resource'; + this.extensions = []; + this.contentType = ''; + this.compilation = compilation; + this.options = options; + } + + // introduce a new resource type to the browser, on the fly, ex: `\n + \n + `); + } + + if (script.rawAttrs === '') { + appTemplateContents = appTemplateContents.replace(/<\/script>/, ` + \n + \n + `); + } + }); + + headLinks.forEach(link => { + appTemplateContents = appTemplateContents.replace(/<\/link>/, ` + \n + \n + `); + }); + } + + return appTemplateContents || contents; +}; + +const getUserScripts = (contents, userWorkspace) => { + // TODO use an HTML parser? https://www.npmjs.com/package/node-html-parser + if (process.env.__GWD_COMMAND__ === 'develop') { // eslint-disable-line no-underscore-dangle + // TODO setup and teardown should be done together + // console.debug('running in develop mode, attach live reload script'); + contents = contents.replace('', ` + + + `); + } + + contents = contents.replace(/type="module"/g, 'type="module-shim"'); + + const importMap = {}; + const userPackageJson = fs.existsSync(`${userWorkspace}/package.json`) + ? require(path.join(userWorkspace, 'package.json')) // its a monorepo? + : fs.existsSync(`${process.cwd()}/package.json`) + ? require(path.join(process.cwd(), 'package.json')) + : {}; + + // console.debug('userPackageJson', userPackageJson); + // console.debug('dependencies', userPackageJson.dependencies); + + Object.keys(userPackageJson.dependencies || {}).forEach(dependency => { + const packageRootPath = path.join(process.cwd(), './node_modules', dependency); + const packageJsonPath = path.join(packageRootPath, 'package.json'); + const packageJson = require(packageJsonPath); + const packageEntryPointPath = path.join(process.cwd(), './node_modules', dependency, packageJson.main); + const packageFileContents = fs.readFileSync(packageEntryPointPath, 'utf-8'); + + walk.simple(acorn.parse(packageFileContents, { sourceType: 'module' }), { + ImportDeclaration(node) { + // console.log('Found a ImportDeclaration'); + const sourceValue = node.source.value; + + if (sourceValue.indexOf('.') !== 0 && sourceValue.indexOf('http') !== 0) { + // console.log(`found a bare import for ${sourceValue}!!!!!`); + importMap[sourceValue] = `/node_modules/${sourceValue}`; + } + }, + ExportNamedDeclaration(node) { + // console.log('Found a ExportNamedDeclaration'); + const sourceValue = node && node.source ? node.source.value : ''; + + if (sourceValue.indexOf('.') !== 0 && sourceValue.indexOf('http') !== 0) { + // console.log(`found a bare export for ${sourceValue}!!!!!`); + importMap[sourceValue] = `/node_modules/${sourceValue}`; + } + } + }); + + importMap[dependency] = `/node_modules/${dependency}/${packageJson.main}`; + }); + + // console.log('importMap all complete', importMap); + + contents = contents.replace('', ` + + + + `); + + if (process.env.__GWD_COMMAND__ === 'build') { // eslint-disable-line no-underscore-dangle + // TODO setup and teardown should be done together + // console.debug('running in build mode, polyfill WebComponents for puppeteer'); + contents = contents.replace('', ` + + + `); + } + return contents; +}; + +const getMetaContent = (url, config, contents) => { + + const title = config.title; + const metaContent = config.meta.map(item => { + let metaHtml = ''; + + // TODO better way to implememnt this? should we implement this? + for (const [key, value] of Object.entries(item)) { + const isOgUrl = item.property === 'og:url' && key === 'content'; + const hasTrailingSlash = isOgUrl && value[value.length - 1] === '/'; + const contextualValue = isOgUrl + ? hasTrailingSlash + ? `${value}${url.replace('/', '')}` + : `${value}${url === '/' ? '' : url}` + : value; + + metaHtml += ` ${key}="${contextualValue}"`; + } + + return item.rel + ? `` + : ``; + }).join('\n'); + + // console.log(contents); + + // TODO make smarter so that if it already exists, then leave it alone + contents = contents.replace(/(.*)<\/title>/, ''); + contents = contents.replace('<head>', `<head><title>${title}`); + contents = contents.replace('', metaContent); + + return contents; +}; + +class HtmlResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + + this.extensions = ['.html', '.md']; + this.contentType = 'text/html'; + } + + shouldServe(request) { + const { url } = request; + const { userWorkspace } = this.compilation.context; + + const barePath = url.endsWith('/') + ? `${userWorkspace}/pages${url}index` + : `${userWorkspace}/pages${url.replace('.html', '')}`; + + // TODO share this logic with graph.js lookup + return (this.extensions.indexOf(path.extname(url)) >= 0 || path.extname(url) === '') && + (fs.existsSync(`${barePath}.html`) || barePath.substring(barePath.length - 5, barePath.length) === 'index'); + } + + serve(request) { + const { url } = request; + const { userWorkspace } = this.compilation.context; + + const barePath = url[url.length - 1] === '/' + ? `${userWorkspace}/pages${url}index` + : `${userWorkspace}/pages${url.replace('.html', '')}`; + + let body = getAppTemplate(barePath, userWorkspace); + body = getAppTemplateScripts(body, userWorkspace); + body = getUserScripts(body, userWorkspace); + body = getMetaContent(url, this.compilation.config, body); + + return { + body, + contentType: this.contentType, + extension: this.extensions + }; + } + + // TOOD - not needed, just do everything in serve? + filter() { + + } + + // TODO - replace in serialize.js + transform () { + + } +} + +// TODO Plugin interface? +module.exports = { + name: 'plugin-serve-html', + provider: (compilation, options) => new HtmlResource(compilation, options) +}; \ No newline at end of file From 55e6294444427f4c5d55c579e43cd915d732d258 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Tue, 12 Jan 2021 21:03:49 -0500 Subject: [PATCH 02/24] POC of a FooResource --- greenwood.config.js | 48 +++++++++++++--- packages/cli/src/lib/resource-interface.js | 39 +++++++------ packages/cli/src/lifecycles/config.js | 29 +++++----- packages/cli/src/lifecycles/serve.js | 55 +++++++++++++++---- .../plugin-standard-html.js} | 29 +++++----- www/components/grant.foo | 1 + www/components/owen.foo | 3 + www/pages/index.html | 1 + 8 files changed, 136 insertions(+), 69 deletions(-) rename packages/cli/src/plugins/{plugin-serve-html.js => resource/plugin-standard-html.js} (94%) create mode 100644 www/components/grant.foo create mode 100644 www/components/owen.foo diff --git a/greenwood.config.js b/greenwood.config.js index 0eab90367..9f67d6ff6 100644 --- a/greenwood.config.js +++ b/greenwood.config.js @@ -1,10 +1,40 @@ +const fs = require('fs'); const path = require('path'); +const { ResourceInterface } = require('./packages/cli/src/lib/resource-interface'); // const pluginGoogleAnalytics = require('./packages/plugin-google-analytics/src/index'); // const pluginPolyfills = require('./packages/plugin-polyfills/src/index'); const META_DESCRIPTION = 'A modern and performant static site generator supporting Web Component based development'; const FAVICON_HREF = '/assets/favicon.ico'; +class FooResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + + this.extensions = ['.foo']; + this.contentType = 'text/javascript'; + } + + shouldResolve(request) { + const { url } = request; + + if (url.endsWith('.foo')) { + return true; + } + } + + resolve(request) { + const { url } = request; + const fooPath = path.join(this.compilation.context.userWorkspace, url); + const body = fs.readFileSync(fooPath, 'utf-8'); + + return { + body, + contentType: this.contentType + }; + } +} + module.exports = { workspace: path.join(__dirname, 'www'), title: 'Greenwood', @@ -20,13 +50,17 @@ module.exports = { { rel: 'icon', href: FAVICON_HREF }, { name: 'google-site-verification', content: '4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0' } ], - // TODO - // plugins: [ - // ...pluginGoogleAnalytics({ - // analyticsId: 'UA-147204327-1' - // }), - // ...pluginPolyfills() - // ], + plugins: [{ + type: 'resource', + name: 'plugin-foo', + provider: (compilation, options) => new FooResource(compilation, options) + } + // // TODO + // ...pluginGoogleAnalytics({ + // analyticsId: 'UA-147204327-1' + // }), + // ...pluginPolyfills() + ], markdown: { plugins: [ '@mapbox/rehype-prism', diff --git a/packages/cli/src/lib/resource-interface.js b/packages/cli/src/lib/resource-interface.js index 2665c1996..82a200979 100644 --- a/packages/cli/src/lib/resource-interface.js +++ b/packages/cli/src/lib/resource-interface.js @@ -1,6 +1,5 @@ class ResourceInterface { constructor(compilation, options = {}) { - this.type = 'resource'; this.extensions = []; this.contentType = ''; this.compilation = compilation; @@ -8,31 +7,31 @@ class ResourceInterface { } // introduce a new resource type to the browser, on the fly, ex: ` + From c0b51dc7d5a80668362e67c649d39e1de197c35c Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Tue, 12 Jan 2021 21:47:02 -0500 Subject: [PATCH 03/24] make resource resolve async and resource support for css js and json --- greenwood.config.js | 24 ++++--- packages/cli/src/lib/resource-interface.js | 4 +- packages/cli/src/lifecycles/serve.js | 32 ++++----- .../plugins/resource/plugin-standard-css.js | 70 +++++++++++++++++++ .../plugins/resource/plugin-standard-html.js | 40 ++++++----- .../resource/plugin-standard-javascript.js | 42 +++++++++++ .../plugins/resource/plugin-standard-json.js | 55 +++++++++++++++ 7 files changed, 224 insertions(+), 43 deletions(-) create mode 100644 packages/cli/src/plugins/resource/plugin-standard-css.js create mode 100644 packages/cli/src/plugins/resource/plugin-standard-javascript.js create mode 100644 packages/cli/src/plugins/resource/plugin-standard-json.js diff --git a/greenwood.config.js b/greenwood.config.js index 9f67d6ff6..bdd826473 100644 --- a/greenwood.config.js +++ b/greenwood.config.js @@ -23,15 +23,21 @@ class FooResource extends ResourceInterface { } } - resolve(request) { - const { url } = request; - const fooPath = path.join(this.compilation.context.userWorkspace, url); - const body = fs.readFileSync(fooPath, 'utf-8'); - - return { - body, - contentType: this.contentType - }; + async resolve(request) { + return new Promise((resolve, reject) => { + try { + const { url } = request; + const fooPath = path.join(this.compilation.context.userWorkspace, url); + const body = fs.readFileSync(fooPath, 'utf-8'); + + resolve({ + body, + contentType: this.contentType + }); + } catch (e) { + reject(e); + } + }); } } diff --git a/packages/cli/src/lib/resource-interface.js b/packages/cli/src/lib/resource-interface.js index 82a200979..81923d18f 100644 --- a/packages/cli/src/lib/resource-interface.js +++ b/packages/cli/src/lib/resource-interface.js @@ -1,3 +1,5 @@ +const path = require('path'); + class ResourceInterface { constructor(compilation, options = {}) { this.extensions = []; @@ -8,7 +10,7 @@ class ResourceInterface { // introduce a new resource type to the browser, on the fly, ex: `\n - \n - `); - } - - if (script.rawAttrs === '') { - appTemplateContents = appTemplateContents.replace(/<\/script>/, ` - \n - \n - `); - } - }); - - headLinks.forEach(link => { - appTemplateContents = appTemplateContents.replace(/<\/link>/, ` - \n - \n - `); - }); - } - return appTemplateContents || contents; -}; - -const getUserScripts = (contents, userWorkspace) => { - // TODO use an HTML parser? https://www.npmjs.com/package/node-html-parser - if (process.env.__GWD_COMMAND__ === 'develop') { // eslint-disable-line no-underscore-dangle - // TODO setup and teardown should be done together - // console.debug('running in develop mode, attach live reload script'); - contents = contents.replace('', ` - - - `); - } - - contents = contents.replace(/type="module"/g, 'type="module-shim"'); - - const importMap = {}; - const userPackageJson = fs.existsSync(`${userWorkspace}/package.json`) - ? require(path.join(userWorkspace, 'package.json')) // its a monorepo? - : fs.existsSync(`${process.cwd()}/package.json`) - ? require(path.join(process.cwd(), 'package.json')) - : {}; - - // console.debug('userPackageJson', userPackageJson); - // console.debug('dependencies', userPackageJson.dependencies); - - Object.keys(userPackageJson.dependencies || {}).forEach(dependency => { - const packageRootPath = path.join(process.cwd(), './node_modules', dependency); - const packageJsonPath = path.join(packageRootPath, 'package.json'); - const packageJson = require(packageJsonPath); - const packageEntryPointPath = path.join(process.cwd(), './node_modules', dependency, packageJson.main); - const packageFileContents = fs.readFileSync(packageEntryPointPath, 'utf-8'); - - walk.simple(acorn.parse(packageFileContents, { sourceType: 'module' }), { - ImportDeclaration(node) { - // console.log('Found a ImportDeclaration'); - const sourceValue = node.source.value; - - if (sourceValue.indexOf('.') !== 0 && sourceValue.indexOf('http') !== 0) { - // console.log(`found a bare import for ${sourceValue}!!!!!`); - importMap[sourceValue] = `/node_modules/${sourceValue}`; - } - }, - ExportNamedDeclaration(node) { - // console.log('Found a ExportNamedDeclaration'); - const sourceValue = node && node.source ? node.source.value : ''; - - if (sourceValue.indexOf('.') !== 0 && sourceValue.indexOf('http') !== 0) { - // console.log(`found a bare export for ${sourceValue}!!!!!`); - importMap[sourceValue] = `/node_modules/${sourceValue}`; - } - } - }); - - importMap[dependency] = `/node_modules/${dependency}/${packageJson.main}`; - }); - - // console.log('importMap all complete', importMap); - - contents = contents.replace('', ` - - - - `); - - if (process.env.__GWD_COMMAND__ === 'build') { // eslint-disable-line no-underscore-dangle - // TODO setup and teardown should be done together - // console.debug('running in build mode, polyfill WebComponents for puppeteer'); - contents = contents.replace('', ` - - - `); - } - return contents; -}; - -const getMetaContent = (url, config, contents) => { - - const title = config.title; - const metaContent = config.meta.map(item => { - let metaHtml = ''; - - // TODO better way to implememnt this? should we implement this? - for (const [key, value] of Object.entries(item)) { - const isOgUrl = item.property === 'og:url' && key === 'content'; - const hasTrailingSlash = isOgUrl && value[value.length - 1] === '/'; - const contextualValue = isOgUrl - ? hasTrailingSlash - ? `${value}${url.replace('/', '')}` - : `${value}${url === '/' ? '' : url}` - : value; - - metaHtml += ` ${key}="${contextualValue}"`; - } - - return item.rel - ? `` - : ``; - }).join('\n'); - - // console.log(contents); - - // TODO make smarter so that if it already exists, then leave it alone - contents = contents.replace(/(.*)<\/title>/, ''); - contents = contents.replace('<head>', `<head><title>${title}`); - contents = contents.replace('', metaContent); - - return contents; -}; - -module.exports = { - getAppTemplate, - getAppTemplateScripts, - getMetaContent, - getUserScripts -}; \ No newline at end of file From c37dd9b4c9481fd775c1c623c466fcd925206546 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Sat, 16 Jan 2021 17:57:38 -0500 Subject: [PATCH 10/24] filename change for consistency --- packages/cli/src/lifecycles/serve.js | 4 ++-- ...de-modules-resolver.js => plugin-resolver-node-modules.js} | 0 ...orkspace-resolver.js => plugin-resolver-user-workspace.js} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/cli/src/plugins/resource/{plugin-node-modules-resolver.js => plugin-resolver-node-modules.js} (100%) rename packages/cli/src/plugins/resource/{plugin-user-workspace-resolver.js => plugin-resolver-user-workspace.js} (100%) diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js index 83a871921..b10766e22 100644 --- a/packages/cli/src/lifecycles/serve.js +++ b/packages/cli/src/lifecycles/serve.js @@ -2,8 +2,8 @@ const fs = require('fs'); const path = require('path'); const Koa = require('koa'); -const pluginResolverNodeModules = require('../plugins/resource/plugin-node-modules-resolver'); -const pluginResolverUserWorkspace = require('../plugins/resource/plugin-user-workspace-resolver'); +const pluginResolverNodeModules = require('../plugins/resource/plugin-resolver-node-modules'); +const pluginResolverUserWorkspace = require('../plugins/resource/plugin-resolver-user-workspace'); const pluginResourceStandardCss = require('../plugins/resource/plugin-standard-css'); const pluginResourceStandardFont = require('../plugins/resource/plugin-standard-font'); const pluginResourceStandardHtml = require('../plugins/resource/plugin-standard-html'); diff --git a/packages/cli/src/plugins/resource/plugin-node-modules-resolver.js b/packages/cli/src/plugins/resource/plugin-resolver-node-modules.js similarity index 100% rename from packages/cli/src/plugins/resource/plugin-node-modules-resolver.js rename to packages/cli/src/plugins/resource/plugin-resolver-node-modules.js diff --git a/packages/cli/src/plugins/resource/plugin-user-workspace-resolver.js b/packages/cli/src/plugins/resource/plugin-resolver-user-workspace.js similarity index 100% rename from packages/cli/src/plugins/resource/plugin-user-workspace-resolver.js rename to packages/cli/src/plugins/resource/plugin-resolver-user-workspace.js From d09bf375ea5ef46955e8c261a9a008ec7f8a8fe8 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Sat, 16 Jan 2021 18:38:53 -0500 Subject: [PATCH 11/24] custom file extension integration with rollup --- greenwood.config.js | 4 +++- packages/cli/src/config/rollup.config.js | 27 ++++++++++++++++++++---- www/components/grant.foo | 9 +++++++- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/greenwood.config.js b/greenwood.config.js index ff58513aa..2a225a646 100644 --- a/greenwood.config.js +++ b/greenwood.config.js @@ -18,8 +18,10 @@ class FooResource extends ResourceInterface { async serve(url) { return new Promise(async (resolve, reject) => { try { - const body = await fs.promises.readFile(url, 'utf-8'); + let body = await fs.promises.readFile(url, 'utf-8'); + body = body.replace(/interface (.*){(.*)}/s, ''); + resolve({ body, contentType: this.contentType diff --git a/packages/cli/src/config/rollup.config.js b/packages/cli/src/config/rollup.config.js index a8ea9b94a..ca0eaab51 100644 --- a/packages/cli/src/config/rollup.config.js +++ b/packages/cli/src/config/rollup.config.js @@ -37,9 +37,28 @@ function greenwoodHtmlPlugin(compilation) { return { name: 'greenwood-html-plugin', - load(id) { - if (path.extname(id) === '.html') { - return ''; + async load(id) { + const extension = path.extname(id); + + if (extension === '.html') { + return Promise.resolve(''); + } + + // handle custom user file extensions + const customResources = compilation.config.plugins.filter((plugin) => { + return plugin.type === 'resource'; + }).map((plugin) => { + return plugin.provider(compilation); + }).filter((resource) => { + if (resource.shouldServe(id)) { + return resource; + } + }); + + if (customResources.length) { + const response = await customResources[0].serve(id); + + return response.body; } }, // TODO do this during load instead? @@ -59,7 +78,7 @@ function greenwoodHtmlPlugin(compilation) { const srcPath = src.replace('../', './'); const source = fs.readFileSync(path.join(userWorkspace, srcPath), 'utf-8'); - + that.emitFile({ type: 'chunk', id: srcPath, diff --git a/www/components/grant.foo b/www/components/grant.foo index 28ce987a8..09aeb9905 100644 --- a/www/components/grant.foo +++ b/www/components/grant.foo @@ -1 +1,8 @@ -console.log('hello from grant.foo 😎'); \ No newline at end of file +interface User { + id: number + firstName: string + lastName: string + role: string +} + +console.log('hello from grant.foo! 😎'); \ No newline at end of file From 9a941d742774cc046041e665b8fd14725adcbff0 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 18 Jan 2021 12:18:06 -0500 Subject: [PATCH 12/24] fix existing test cases to restore expected outcomes post refactor --- .../plugins/resource/plugin-standard-html.js | 47 ++++++++----------- .../build.default.markdown/src/pages/index.md | 2 +- ....default.workspace-getting-started.spec.js | 4 ++ .../src/templates/blog.html | 1 + 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js index 66ee498d6..248e5e512 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-html.js +++ b/packages/cli/src/plugins/resource/plugin-standard-html.js @@ -32,9 +32,12 @@ const getPageTemplate = (barePath, workspace, template) => { } else if (fs.existsSync(`${templatesDir}/page.html`)) { // else look for default page template contents = fs.readFileSync(`${templatesDir}/page.html`, 'utf-8'); - } else { - // finally, fallback to just using the stock app template + } else if (fs.existsSync(`${templatesDir}/app.html`)) { + // fallback to just using their app template contents = fs.readFileSync(`${templatesDir}/app.html`, 'utf-8'); + } else { + // fallback to using Greenwood's stock app template + contents = fs.readFileSync(path.join(__dirname, '../../templates/app.html'), 'utf-8'); } return contents; @@ -165,8 +168,7 @@ const getUserScripts = (contents, userWorkspace) => { }; const getMetaContent = (url, config, contents) => { - - const title = config.title; + const title = config.title || ''; const metaContent = config.meta.map(item => { let metaHtml = ''; @@ -188,8 +190,6 @@ const getMetaContent = (url, config, contents) => { : ``; }).join('\n'); - // console.log(contents); - // TODO make smarter so that if it already exists, then leave it alone contents = contents.replace(/(.*)<\/title>/, ''); contents = contents.replace('<head>', `<head><title>${title}`); @@ -213,42 +213,29 @@ class StandardHtmlResource extends ResourceInterface { shouldServe(url) { const { userWorkspace } = this.compilation.context; const relativeUrl = this.getRelativeUserworkspaceUrl(url); - // console.debug('>>>>>>>>>>>> HTML shouldServe url', url); - // console.debug('>>>>>>>>>>>> HTML shouldServe relativeUrl', relativeUrl); - const barePath = relativeUrl.endsWith('/') ? `${userWorkspace}/pages${relativeUrl}index` : `${userWorkspace}/pages${relativeUrl.replace('.html', '')}`; - // console.debug('should serve HTML bare path', barePath); - // TODO share this logic with graph.js lookup return (this.extensions.indexOf(path.extname(relativeUrl)) >= 0 || path.extname(relativeUrl) === '') && (fs.existsSync(`${barePath}.html`) || barePath.substring(barePath.length - 5, barePath.length) === 'index') || fs.existsSync(`${barePath}.md`) || fs.existsSync(`${barePath.substring(0, barePath.lastIndexOf('/index'))}.md`); } async serve(url) { - // console.debug('>>>>>>>>>>>> serve ', url); return new Promise(async (resolve, reject) => { try { - const { config } = this.compilation; + const config = Object.assign({}, this.compilation.config); const { userWorkspace } = this.compilation.context; const normalizedUrl = this.getRelativeUserworkspaceUrl(url); let body = ''; - let title = config.title || ''; let template = null; let processedMarkdown = null; const barePath = normalizedUrl.endsWith('/') ? `${userWorkspace}/pages${normalizedUrl}index` : `${userWorkspace}/pages${normalizedUrl.replace('.html', '')}`; - - // console.debug('>>>>>>> barePath', barePath); - // console.debug('markdown exists 1?', fs.existsSync(`${barePath}.md`)); - // console.debug('markdown exists 2?', fs.existsSync(`${userWorkspace}/pages${url.replace('/index.html', '.md')}`)); - // console.debug('markdown exists 3?', fs.existsSync(`${barePath.replace('/index', '.md')}`)); - - const isMarkdownContent = fs.existsSync(`${barePath}.md`) - // || fs.existsSync(`${userWorkspace}/pages${url.replace('/index.html', '.md')}`) + const isMarkdownContent = fs.existsSync(`${barePath}.md`) + || fs.existsSync(`${barePath.substring(0, barePath.lastIndexOf('/index'))}.md`) || fs.existsSync(`${barePath.replace('/index', '.md')}`); if (isMarkdownContent) { @@ -283,13 +270,17 @@ class StandardHtmlResource extends ResourceInterface { .use(rehypeStringify) // convert AST to HTML string .process(markdownContents); - // use page title - if (fm.attributes.title) { - config.title = `${title} - ${fm.attributes.title}`; - } + // configure via frontmatter + if (fm.attributes) { + const { attributes } = fm; - if (fm.attributes.template) { - template = template; + if (attributes.title) { + config.title = `${config.title} - ${attributes.title}`; + } + + if (attributes.template) { + template = attributes.template; + } } } diff --git a/packages/cli/test/cases/build.default.markdown/src/pages/index.md b/packages/cli/test/cases/build.default.markdown/src/pages/index.md index bd96b580d..66ce731f5 100644 --- a/packages/cli/test/cases/build.default.markdown/src/pages/index.md +++ b/packages/cli/test/cases/build.default.markdown/src/pages/index.md @@ -6,4 +6,4 @@ This is some markdown being rendered by Greenwood. console.log('hello world'); ``` -just passing by \ No newline at end of file +just passing by \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-getting-started/build.default.workspace-getting-started.spec.js b/packages/cli/test/cases/build.default.workspace-getting-started/build.default.workspace-getting-started.spec.js index 446470b50..55a8a95d5 100644 --- a/packages/cli/test/cases/build.default.workspace-getting-started/build.default.workspace-getting-started.spec.js +++ b/packages/cli/test/cases/build.default.workspace-getting-started/build.default.workspace-getting-started.spec.js @@ -187,9 +187,11 @@ describe('Build Greenwood With: ', function() { }); it('should have an the expected content in the ', function() { + const h1 = dom.window.document.querySelector('body h1'); const h2 = dom.window.document.querySelector('body h2'); const p = dom.window.document.querySelectorAll('body p'); + expect(h1.textContent).to.be.equal('A Blog Post Page'); expect(h2.textContent).to.be.equal('My First Blog Post'); expect(p[0].textContent).to.be.equal('Lorem Ipsum'); @@ -252,9 +254,11 @@ describe('Build Greenwood With: ', function() { }); it('should have an the expected content in the ', function() { + const h1 = dom.window.document.querySelector('body h1'); const h2 = dom.window.document.querySelector('body h2'); const p = dom.window.document.querySelectorAll('body p'); + expect(h1.textContent).to.be.equal('A Blog Post Page'); expect(h2.textContent).to.be.equal('My Second Blog Post'); expect(p[0].textContent).to.be.equal('Lorem Ipsum'); diff --git a/packages/cli/test/cases/build.default.workspace-getting-started/src/templates/blog.html b/packages/cli/test/cases/build.default.workspace-getting-started/src/templates/blog.html index abd851272..6d590b088 100644 --- a/packages/cli/test/cases/build.default.workspace-getting-started/src/templates/blog.html +++ b/packages/cli/test/cases/build.default.workspace-getting-started/src/templates/blog.html @@ -14,6 +14,7 @@
+

A Blog Post Page

From c4228c007d7de4574addcd70ab9ff9e6c01e7e17 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 18 Jan 2021 13:06:41 -0500 Subject: [PATCH 13/24] fix .ico resolution by using url --- packages/cli/src/plugins/resource/plugin-standard-image.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/plugins/resource/plugin-standard-image.js b/packages/cli/src/plugins/resource/plugin-standard-image.js index 5a5265f5e..08c80e591 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-image.js +++ b/packages/cli/src/plugins/resource/plugin-standard-image.js @@ -34,7 +34,7 @@ class StandardFontResource extends ResourceInterface { } } else if (['.ico'].includes(ext)) { contentType = 'image/x-icon'; - body = await fs.promises.readFile(assetPath); + body = await fs.promises.readFile(url); } resolve({ From 948882218aa536a2d5da33d96c15aace2202544a Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 18 Jan 2021 13:08:44 -0500 Subject: [PATCH 14/24] made spec for a foo resource plugin --- .../build.config.plugins-resource.spec.js | 70 +++++++++++++++++++ .../greenwood.config.js | 36 ++++++++++ .../src/foo-files/my-custom-file.foo | 9 +++ .../src/foo-files/my-other-custom-file.foo | 8 +++ .../src/pages/index.html | 13 ++++ www/components/owen.foo | 6 ++ 6 files changed, 142 insertions(+) create mode 100644 packages/cli/test/cases/build.plugins.resource/build.config.plugins-resource.spec.js create mode 100644 packages/cli/test/cases/build.plugins.resource/greenwood.config.js create mode 100644 packages/cli/test/cases/build.plugins.resource/src/foo-files/my-custom-file.foo create mode 100644 packages/cli/test/cases/build.plugins.resource/src/foo-files/my-other-custom-file.foo create mode 100644 packages/cli/test/cases/build.plugins.resource/src/pages/index.html diff --git a/packages/cli/test/cases/build.plugins.resource/build.config.plugins-resource.spec.js b/packages/cli/test/cases/build.plugins.resource/build.config.plugins-resource.spec.js new file mode 100644 index 000000000..c23263953 --- /dev/null +++ b/packages/cli/test/cases/build.plugins.resource/build.config.plugins-resource.spec.js @@ -0,0 +1,70 @@ +/* + * Use Case + * Run Greenwood with a custom resource plugin and default workspace. + * + * Uaer Result + * Should generate a bare bones Greenwood build with expected custom file (.foo) behavior. + * + * User Command + * greenwood build + * + * User Config + * class FooResource extends ResourceInterface { + * // see complete implementation in the greenwood.config.js file used for this spec + * } + * + * { + * plugins: [{ + * type: 'resource', + * name: 'plugin-foo', + * provider: (compilation, options) => new FooResource(compilation, options) + * }] + * } + * + * Custom Workspace + * src/ + * pages/ + * index.html + * foo-files/ + * my-custom-file.foo + * my-other-custom-file.foo + */ +const expect = require('chai').expect; +const { JSDOM } = require('jsdom'); +const path = require('path'); +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'Custom FooResource Plugin and Default Workspace'; + let setup; + + before(async function() { + setup = new TestBed(); + this.context = await setup.setupTestBed(__dirname); + }); + + describe(LABEL, function() { + before(async function() { + await setup.runGreenwoodCommand('build'); + }); + + describe('Transpiling and DOM Manipulation', function() { + let dom; + + beforeEach(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + it('should have expected text executed from my-custom-file.foo in the DOM', function() { + const placeholder = dom.window.document.querySelector('body h6'); + + expect(placeholder.textContent).to.be.equal('hello from my-custom-file.foo'); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.plugins.resource/greenwood.config.js b/packages/cli/test/cases/build.plugins.resource/greenwood.config.js new file mode 100644 index 000000000..2cd06f201 --- /dev/null +++ b/packages/cli/test/cases/build.plugins.resource/greenwood.config.js @@ -0,0 +1,36 @@ +const fs = require('fs'); +const { ResourceInterface } = require('../../../src/lib/resource-interface'); + +class FooResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + + this.extensions = ['.foo']; + this.contentType = 'text/javascript'; + } + + async serve(url) { + return new Promise(async (resolve, reject) => { + try { + let body = await fs.promises.readFile(url, 'utf-8'); + + body = body.replace(/interface (.*){(.*)}/s, ''); + + resolve({ + body, + contentType: this.contentType + }); + } catch (e) { + reject(e); + } + }); + } +} + +module.exports = { + plugins: [{ + type: 'resource', + name: 'plugin-foo', + provider: (compilation, options) => new FooResource(compilation, options) + }] +}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.plugins.resource/src/foo-files/my-custom-file.foo b/packages/cli/test/cases/build.plugins.resource/src/foo-files/my-custom-file.foo new file mode 100644 index 000000000..974a7d013 --- /dev/null +++ b/packages/cli/test/cases/build.plugins.resource/src/foo-files/my-custom-file.foo @@ -0,0 +1,9 @@ +import './my-other-custom-file.foo'; + +const node = document.createElement('h6'); +const textnode = document.createTextNode('hello from my-custom-file.foo'); + +node.appendChild(textnode); +document.getElementsByTagName('body')[0].appendChild(node); + +console.log('hello from my-custom-file.foo 👋'); \ No newline at end of file diff --git a/packages/cli/test/cases/build.plugins.resource/src/foo-files/my-other-custom-file.foo b/packages/cli/test/cases/build.plugins.resource/src/foo-files/my-other-custom-file.foo new file mode 100644 index 000000000..36d618ede --- /dev/null +++ b/packages/cli/test/cases/build.plugins.resource/src/foo-files/my-other-custom-file.foo @@ -0,0 +1,8 @@ +interface User { + id: number + firstName: string + lastName: string + role: string +} + +console.log('hello from some-other-custom-file.foo! 😎'); \ No newline at end of file diff --git a/packages/cli/test/cases/build.plugins.resource/src/pages/index.html b/packages/cli/test/cases/build.plugins.resource/src/pages/index.html new file mode 100644 index 000000000..6532e0c88 --- /dev/null +++ b/packages/cli/test/cases/build.plugins.resource/src/pages/index.html @@ -0,0 +1,13 @@ + + + + + + + + + +

My Foo Website

+ + + \ No newline at end of file diff --git a/www/components/owen.foo b/www/components/owen.foo index 08bfc0327..9516295a8 100644 --- a/www/components/owen.foo +++ b/www/components/owen.foo @@ -1,3 +1,9 @@ import './grant.foo'; +const node = document.createElement('h6'); +const textnode = document.createTextNode('hello from my-custom-file.foo 👋'); + +node.appendChild(textnode); +document.getElementsByTagName('body')[0].appendChild(node); + console.log('hello from owen.foo! 👋'); \ No newline at end of file From c4d7fd66a42516968bcc6486d46c53deccd02f5a Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 18 Jan 2021 13:30:04 -0500 Subject: [PATCH 15/24] delete old plugin specs, rename existing specs, added plugin name spec --- packages/cli/src/lifecycles/config.js | 9 +- .../build.plugins-index.spec.js | 85 ------------------- .../build.plugins-index/greenwood.config.js | 31 ------- .../build-plugins-webpack.spec.js | 78 ----------------- .../build.plugins-webpack/greenwood.config.js | 13 --- .../build.plugins.error-name.spec.js | 49 +++++++++++ .../greenwood.config.js | 8 ++ .../build.plugins.error-provider.spec.js} | 7 +- .../greenwood.config.js | 2 +- .../build.plugins.error-type.spec.js} | 10 +-- .../greenwood.config.js | 0 11 files changed, 74 insertions(+), 218 deletions(-) delete mode 100644 packages/cli/test/cases/build.plugins-index/build.plugins-index.spec.js delete mode 100644 packages/cli/test/cases/build.plugins-index/greenwood.config.js delete mode 100644 packages/cli/test/cases/build.plugins-webpack/build-plugins-webpack.spec.js delete mode 100644 packages/cli/test/cases/build.plugins-webpack/greenwood.config.js create mode 100644 packages/cli/test/cases/build.plugins.error-name/build.plugins.error-name.spec.js create mode 100644 packages/cli/test/cases/build.plugins.error-name/greenwood.config.js rename packages/cli/test/cases/{build.plugins-error-provider/build.plugins-error-provider.spec.js => build.plugins.error-provider/build.plugins.error-provider.spec.js} (79%) rename packages/cli/test/cases/{build.plugins-error-provider => build.plugins.error-provider}/greenwood.config.js (74%) rename packages/cli/test/cases/{build.plugins-error-type/build.plugins-error-type.spec.js => build.plugins.error-type/build.plugins.error-type.spec.js} (71%) rename packages/cli/test/cases/{build.plugins-error-type => build.plugins.error-type}/greenwood.config.js (100%) diff --git a/packages/cli/src/lifecycles/config.js b/packages/cli/src/lifecycles/config.js index 2305c0288..2d830ce07 100644 --- a/packages/cli/src/lifecycles/config.js +++ b/packages/cli/src/lifecycles/config.js @@ -79,7 +79,13 @@ module.exports = readAndMergeConfig = async() => { if (!plugin.provider || typeof plugin.provider !== 'function') { const providerTypeof = typeof plugin.provider; - reject(`Error: greenwood.config.js plugins provider must of type function. got ${providerTypeof} instead.`); + reject(`Error: greenwood.config.js plugins provider must be a function. got ${providerTypeof} instead.`); + } + + if (!plugin.name || typeof plugin.name !== 'string') { + const nameTypeof = typeof plugin.name; + + reject(`Error: greenwood.config.js plugins must have a name. got ${nameTypeof} instead.`); } }); @@ -94,7 +100,6 @@ module.exports = readAndMergeConfig = async() => { reject(`Error: greenwood.config.js devServer port must be an integer. Passed value was: ${devServer.port}`); } else { customConfig.devServer.port = devServer.port; - // console.log(`custom port provided => ${customConfig.devServer.port}`); } } } diff --git a/packages/cli/test/cases/build.plugins-index/build.plugins-index.spec.js b/packages/cli/test/cases/build.plugins-index/build.plugins-index.spec.js deleted file mode 100644 index 08e44fbd5..000000000 --- a/packages/cli/test/cases/build.plugins-index/build.plugins-index.spec.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Use Case - * Run Greenwood with some plugins and default workspace. - * - * Uaer Result - * Should generate a bare bones Greenwood build with certain plugins injected into index.html. - * - * User Command - * greenwood build - * - * User Config - * { - * plugins: [{ - * type: 'index', - * provider: function() { - * return { - * hookGreenwoodAnalytics: ` - * - * ` - * }; - * } - * }, { - * type: 'index', - * provider: function() { - * return { - * hookGreenwoodPolyfills: ` - * - * ` - * }; - * } - * }] - * - * } - * - * User Workspace - * Greenwood default (src/) - */ -const expect = require('chai').expect; -const { JSDOM } = require('jsdom'); -const path = require('path'); -const runSmokeTest = require('../../../../../test/smoke-test'); -const TestBed = require('../../../../../test/test-bed'); - -xdescribe('Build Greenwood With: ', function() { - const LABEL = 'Custom Index Plugin and Default Workspace'; - let setup; - - before(async function() { - setup = new TestBed(); - this.context = await setup.setupTestBed(__dirname); - }); - - describe(LABEL, function() { - before(async function() { - await setup.runGreenwoodCommand('build'); - }); - - runSmokeTest(['public', 'index', 'not-found', 'hello'], LABEL); - - describe('Custom Index Hooks', function() { - let dom; - - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); - }); - - it('should have placeholder for hookGreenwoodAnalytics', function() { - const placeholder = dom.window.document.querySelectorAll('body div.hook-analytics'); - - expect(placeholder.length).to.be.equal(1); - }); - - it('should have placeholder for hookGreenwoodPolyfills', function() { - const placeholder = dom.window.document.querySelectorAll('body div.hook-polyfills'); - - expect(placeholder.length).to.be.equal(1); - }); - }); - }); - - after(function() { - setup.teardownTestBed(); - }); - -}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.plugins-index/greenwood.config.js b/packages/cli/test/cases/build.plugins-index/greenwood.config.js deleted file mode 100644 index bbbdce65f..000000000 --- a/packages/cli/test/cases/build.plugins-index/greenwood.config.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = { - - plugins: [{ - type: 'index', - provider: function() { - return { - hookGreenwoodAnalytics: ` -
- -
- ` - }; - } - }, { - type: 'index', - provider: function() { - return { - hookGreenwoodPolyfills: ` - -
- -
- ` - }; - } - }] - -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.plugins-webpack/build-plugins-webpack.spec.js b/packages/cli/test/cases/build.plugins-webpack/build-plugins-webpack.spec.js deleted file mode 100644 index 758ea144b..000000000 --- a/packages/cli/test/cases/build.plugins-webpack/build-plugins-webpack.spec.js +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Use Case - * Run Greenwood with some plugins and default workspace. - * - * Uaer Result - * Should generate a bare bones Greenwood build with certain plugins injected into index.html. - * - * User Command - * greenwood build - * - * User Config - * const webpack = require('webpack'); - * - * { - * plugins: [{ - * type: 'weboack', - * provider: function() { - * return new webpack.BannerPlugin('Some custom text') - * } - * }] - * } - * - * User Workspace - * Greenwood default (src/) - */ -const expect = require('chai').expect; -const fs = require('fs'); -const { JSDOM } = require('jsdom'); -const path = require('path'); -const runSmokeTest = require('../../../../../test/smoke-test'); -const TestBed = require('../../../../../test/test-bed'); -const { version } = require('../../../package.json'); - -xdescribe('Build Greenwood With: ', function() { - const mockBanner = `My Banner - v${version}`; - const LABEL = 'Custom Webpack Plugin and Default Workspace'; - let setup; - - before(async function() { - setup = new TestBed(); - this.context = await setup.setupTestBed(__dirname); - }); - - describe(LABEL, function() { - before(async function() { - await setup.runGreenwoodCommand('build'); - }); - - runSmokeTest(['public', 'index', 'not-found', 'hello'], LABEL); - - describe('Banner Plugin', function() { - let bundleFile; - - beforeEach(async function() { - const dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); - const scriptTags = dom.window.document.querySelectorAll('body script'); - const bundleScripts = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src; - - return src.indexOf('index.') >= 0 && src.indexOf('.bundle.js') >= 0; - }); - - bundleFile = bundleScripts[0].src.replace('file:///', ''); - }); - - it('should have the banner text in index.js', function() { - const fileContents = fs.readFileSync(path.resolve(this.context.publicDir, bundleFile), 'utf8'); - - expect(fileContents).to.contain(mockBanner); - }); - }); - }); - - after(function() { - setup.teardownTestBed(); - }); - -}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.plugins-webpack/greenwood.config.js b/packages/cli/test/cases/build.plugins-webpack/greenwood.config.js deleted file mode 100644 index 62c9bf8cb..000000000 --- a/packages/cli/test/cases/build.plugins-webpack/greenwood.config.js +++ /dev/null @@ -1,13 +0,0 @@ -const { version } = require('../../../package.json'); -const webpack = require('webpack'); - -module.exports = { - - plugins: [{ - type: 'webpack', - provider: function() { - return new webpack.BannerPlugin(`My Banner - v${version}`); - } - }] - -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.plugins.error-name/build.plugins.error-name.spec.js b/packages/cli/test/cases/build.plugins.error-name/build.plugins.error-name.spec.js new file mode 100644 index 000000000..88ee9352d --- /dev/null +++ b/packages/cli/test/cases/build.plugins.error-name/build.plugins.error-name.spec.js @@ -0,0 +1,49 @@ +/* + * Use Case + * Run Greenwood build command with a bad value for the type of a plugin. + * + * User Result + * Should throw an error. + * + * User Command + * greenwood build + * + * User Config + * { + * plugins: [{ + * type: 'resource', + * plugin: function () { } + * }] + * } + * + * User Workspace + * Greenwood default (src/) + * + */ + +const expect = require('chai').expect; +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + let setup; + + before(async function() { + setup = new TestBed(); + await setup.setupTestBed(__dirname); + }); + + describe('Custom Configuration with a bad name value for a plugin', function() { + it('should throw an error that plugin.name is not a string', async function() { + try { + await setup.runGreenwoodCommand('build'); + } catch (err) { + expect(err).to.contain('Error: greenwood.config.js plugins must have a name. got undefined instead.'); + } + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.plugins.error-name/greenwood.config.js b/packages/cli/test/cases/build.plugins.error-name/greenwood.config.js new file mode 100644 index 000000000..004d95a53 --- /dev/null +++ b/packages/cli/test/cases/build.plugins.error-name/greenwood.config.js @@ -0,0 +1,8 @@ +module.exports = { + + plugins: [{ + type: 'resource', + provider: function() { } + }] + +}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.plugins-error-provider/build.plugins-error-provider.spec.js b/packages/cli/test/cases/build.plugins.error-provider/build.plugins.error-provider.spec.js similarity index 79% rename from packages/cli/test/cases/build.plugins-error-provider/build.plugins-error-provider.spec.js rename to packages/cli/test/cases/build.plugins.error-provider/build.plugins.error-provider.spec.js index d0288050f..2177b4060 100644 --- a/packages/cli/test/cases/build.plugins-error-provider/build.plugins-error-provider.spec.js +++ b/packages/cli/test/cases/build.plugins.error-provider/build.plugins.error-provider.spec.js @@ -12,6 +12,7 @@ * { * plugins: [{ * type: 'index', + * name: 'plugin-something', * plugin: {} * }] * } @@ -24,7 +25,7 @@ const expect = require('chai').expect; const TestBed = require('../../../../../test/test-bed'); -xdescribe('Build Greenwood With: ', function() { +describe('Build Greenwood With: ', function() { let setup; before(async function() { @@ -33,11 +34,11 @@ xdescribe('Build Greenwood With: ', function() { }); describe('Custom Configuration with a bad provider value for a plugin', function() { - it('should throw an error that plugin.type is not valid must be a string', async function() { + it('should throw an error that plugin.provider is not a function', async function() { try { await setup.runGreenwoodCommand('build'); } catch (err) { - expect(err).to.contain('Error: greenwood.config.js plugins provider must of type function. got object instead'); + expect(err).to.contain('Error: greenwood.config.js plugins provider must be a function. got object instead.'); } }); }); diff --git a/packages/cli/test/cases/build.plugins-error-provider/greenwood.config.js b/packages/cli/test/cases/build.plugins.error-provider/greenwood.config.js similarity index 74% rename from packages/cli/test/cases/build.plugins-error-provider/greenwood.config.js rename to packages/cli/test/cases/build.plugins.error-provider/greenwood.config.js index cfb4a4e2f..ca2d3172e 100644 --- a/packages/cli/test/cases/build.plugins-error-provider/greenwood.config.js +++ b/packages/cli/test/cases/build.plugins.error-provider/greenwood.config.js @@ -1,7 +1,7 @@ module.exports = { plugins: [{ - type: 'index', + type: 'resource', provider: {} }] 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 similarity index 71% rename from packages/cli/test/cases/build.plugins-error-type/build.plugins-error-type.spec.js rename to packages/cli/test/cases/build.plugins.error-type/build.plugins.error-type.spec.js index 04408f939..74cd25d4a 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 @@ -12,9 +12,9 @@ * { * plugins: [{ * type: 'indexxxx', + * name: 'plugin-something', * provider: function() { } * }] - * * } * * User Workspace @@ -25,7 +25,7 @@ const expect = require('chai').expect; const TestBed = require('../../../../../test/test-bed'); -xdescribe('Build Greenwood With: ', function() { +describe('Build Greenwood With: ', function() { let setup; before(async function() { @@ -33,12 +33,12 @@ xdescribe('Build Greenwood With: ', function() { await setup.setupTestBed(__dirname); }); - describe('Custom Configuration with a bad type value for a plugin', function() { - it('should throw an error that plugin.type is not valid must be a string', async 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() { try { await setup.runGreenwoodCommand('build'); } catch (err) { - expect(err).to.contain('Error: greenwood.config.js plugins must be one of type "index, webpack". got "indexxx" instead.'); + expect(err).to.contain('Error: greenwood.config.js plugins must be one of type "resource". got "indexxx" instead.'); } }); }); diff --git a/packages/cli/test/cases/build.plugins-error-type/greenwood.config.js b/packages/cli/test/cases/build.plugins.error-type/greenwood.config.js similarity index 100% rename from packages/cli/test/cases/build.plugins-error-type/greenwood.config.js rename to packages/cli/test/cases/build.plugins.error-type/greenwood.config.js From a8831aaac473b63f95b66f897207102984f766af Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 18 Jan 2021 16:18:14 -0500 Subject: [PATCH 16/24] refactor and restore google analytics plugin --- greenwood.config.js | 13 ++--- packages/cli/src/lib/resource-interface.js | 4 +- packages/cli/src/lifecycles/serialize.js | 23 +++++++- packages/plugin-google-analytics/src/index.js | 56 ++++++++++++------- .../test/cases/default/default.spec.js | 9 ++- .../test/cases/default/greenwood.config.js | 2 +- .../error-analytics-id.spec.js | 6 +- .../error-analytics-id/greenwood.config.js | 2 +- .../option-anonymous/greenwood.config.js | 2 +- .../option-anonymous/option-anonymous.spec.js | 8 +-- www/templates/app.html | 6 -- 11 files changed, 80 insertions(+), 51 deletions(-) diff --git a/greenwood.config.js b/greenwood.config.js index 2a225a646..375b37690 100644 --- a/greenwood.config.js +++ b/greenwood.config.js @@ -1,7 +1,7 @@ const fs = require('fs'); const path = require('path'); const { ResourceInterface } = require('./packages/cli/src/lib/resource-interface'); -// const pluginGoogleAnalytics = require('./packages/plugin-google-analytics/src/index'); +const pluginGoogleAnalytics = require('./packages/plugin-google-analytics/src/index'); // const pluginPolyfills = require('./packages/plugin-polyfills/src/index'); const META_DESCRIPTION = 'A modern and performant static site generator supporting Web Component based development'; @@ -52,12 +52,11 @@ module.exports = { type: 'resource', name: 'plugin-foo', provider: (compilation, options) => new FooResource(compilation, options) - } - // // TODO - // ...pluginGoogleAnalytics({ - // analyticsId: 'UA-147204327-1' - // }), - // ...pluginPolyfills() + }, + pluginGoogleAnalytics({ + analyticsId: 'UA-147204327-1' + }) + // TODO ...pluginPolyfills() ], markdown: { plugins: [ diff --git a/packages/cli/src/lib/resource-interface.js b/packages/cli/src/lib/resource-interface.js index 2fa454d74..04ef513c9 100644 --- a/packages/cli/src/lib/resource-interface.js +++ b/packages/cli/src/lib/resource-interface.js @@ -47,12 +47,12 @@ class ResourceInterface { // ex: remove es shim + async optimize(html) { + const { analyticsId, anonymous } = this.options; + const trackAnon = typeof anonymous === 'boolean' ? anonymous : true; + return new Promise((resolve, reject) => { + try { + const newHtml = html.replace('', ` + + - ` - }; - } - }]; + + `); + + resolve(newHtml); + } catch (e) { + reject(e); + } + }); + } +} + +module.exports = (options = {}) => { + return { + type: 'resource', + name: 'plugin-google-analytics', + provider: (compilation) => new GoogleAnalyticsResource(compilation, options) + }; }; \ No newline at end of file diff --git a/packages/plugin-google-analytics/test/cases/default/default.spec.js b/packages/plugin-google-analytics/test/cases/default/default.spec.js index 405462eef..31680308e 100644 --- a/packages/plugin-google-analytics/test/cases/default/default.spec.js +++ b/packages/plugin-google-analytics/test/cases/default/default.spec.js @@ -13,7 +13,7 @@ * * { * plugins: [{ - * ...googleAnalyticsPlugin({ + * googleAnalyticsPlugin({ * analyticsId: 'UA-123456-1' * }) * }] @@ -29,7 +29,7 @@ const path = require('path'); const runSmokeTest = require('../../../../../test/smoke-test'); const TestBed = require('../../../../../test/test-bed'); -xdescribe('Build Greenwood With: ', function() { +describe('Build Greenwood With: ', function() { const LABEL = 'Google Analytics Plugin with default options and Default Workspace'; const mockAnalyticsId = 'UA-123456-1'; @@ -45,7 +45,8 @@ xdescribe('Build Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'index', 'not-found', 'hello'], LABEL); + // TODO runSmokeTest(['public', 'index', 'not-found'], LABEL); + runSmokeTest(['public'], LABEL); describe('Initialization script', function() { let inlineScript = []; @@ -78,11 +79,9 @@ xdescribe('Build Greenwood With: ', function() { 'transport_type': 'beacon' }); } - window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); - gtag('config', '${mockAnalyticsId}', { 'anonymize_ip': true }); gtag('config', '${mockAnalyticsId}'); `; diff --git a/packages/plugin-google-analytics/test/cases/default/greenwood.config.js b/packages/plugin-google-analytics/test/cases/default/greenwood.config.js index 9b18283c0..47a131dad 100644 --- a/packages/plugin-google-analytics/test/cases/default/greenwood.config.js +++ b/packages/plugin-google-analytics/test/cases/default/greenwood.config.js @@ -2,7 +2,7 @@ const googleAnalyticsPlugin = require('../../../src/index'); module.exports = { plugins: [ - ...googleAnalyticsPlugin({ + googleAnalyticsPlugin({ analyticsId: 'UA-123456-1' }) ] diff --git a/packages/plugin-google-analytics/test/cases/error-analytics-id/error-analytics-id.spec.js b/packages/plugin-google-analytics/test/cases/error-analytics-id/error-analytics-id.spec.js index 46792e349..7a7558276 100644 --- a/packages/plugin-google-analytics/test/cases/error-analytics-id/error-analytics-id.spec.js +++ b/packages/plugin-google-analytics/test/cases/error-analytics-id/error-analytics-id.spec.js @@ -13,7 +13,7 @@ * * { * plugins: [{ - * ...googleAnalyticsPlugin() + * googleAnalyticsPlugin() * }] * * } @@ -24,7 +24,7 @@ const expect = require('chai').expect; const TestBed = require('../../../../../test/test-bed'); -xdescribe('Build Greenwood With: ', function() { +describe('Build Greenwood With: ', function() { let setup; before(async function() { @@ -37,7 +37,7 @@ xdescribe('Build Greenwood With: ', function() { try { await setup.runGreenwoodCommand('build'); } catch (err) { - expect(err).to.contain('analyticsId should be of type string. get "undefined" instead.'); + expect(err).to.contain('Error: analyticsId should be of type string. got "undefined" instead.'); } }); }); diff --git a/packages/plugin-google-analytics/test/cases/error-analytics-id/greenwood.config.js b/packages/plugin-google-analytics/test/cases/error-analytics-id/greenwood.config.js index 597d8216b..c2d3d48ee 100644 --- a/packages/plugin-google-analytics/test/cases/error-analytics-id/greenwood.config.js +++ b/packages/plugin-google-analytics/test/cases/error-analytics-id/greenwood.config.js @@ -2,6 +2,6 @@ const googleAnalyticsPlugin = require('../../../src/index'); module.exports = { plugins: [ - ...googleAnalyticsPlugin() + googleAnalyticsPlugin() ] }; \ No newline at end of file diff --git a/packages/plugin-google-analytics/test/cases/option-anonymous/greenwood.config.js b/packages/plugin-google-analytics/test/cases/option-anonymous/greenwood.config.js index 764538e7b..1c628c190 100644 --- a/packages/plugin-google-analytics/test/cases/option-anonymous/greenwood.config.js +++ b/packages/plugin-google-analytics/test/cases/option-anonymous/greenwood.config.js @@ -2,7 +2,7 @@ const googleAnalyticsPlugin = require('../../../src/index'); module.exports = { plugins: [ - ...googleAnalyticsPlugin({ + googleAnalyticsPlugin({ analyticsId: 'UA-123456-1', anonymous: false }) diff --git a/packages/plugin-google-analytics/test/cases/option-anonymous/option-anonymous.spec.js b/packages/plugin-google-analytics/test/cases/option-anonymous/option-anonymous.spec.js index 730f1f674..057c56288 100644 --- a/packages/plugin-google-analytics/test/cases/option-anonymous/option-anonymous.spec.js +++ b/packages/plugin-google-analytics/test/cases/option-anonymous/option-anonymous.spec.js @@ -13,7 +13,7 @@ * * { * plugins: [{ - * ...googleAnalyticsPlugin({ + * googleAnalyticsPlugin({ * analyticsId: 'UA-123456-1', * anonymouse: false * }) @@ -30,7 +30,7 @@ const path = require('path'); const runSmokeTest = require('../../../../../test/smoke-test'); const TestBed = require('../../../../../test/test-bed'); -xdescribe('Build Greenwood With: ', function() { +describe('Build Greenwood With: ', function() { const LABEL = 'Google Analytics Plugin with IP Anonymization tracking set to false and Default Workspace'; const mockAnalyticsId = 'UA-123456-1'; @@ -46,7 +46,8 @@ xdescribe('Build Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'index', 'not-found', 'hello'], LABEL); + // TODO runSmokeTest([not-found'], LABEL); + runSmokeTest(['public'], LABEL); describe('Initialization script', function() { let inlineScript; @@ -70,7 +71,6 @@ xdescribe('Build Greenwood With: ', function() { window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); - gtag('config', '${mockAnalyticsId}', { 'anonymize_ip': false }); gtag('config', '${mockAnalyticsId}'); `; diff --git a/www/templates/app.html b/www/templates/app.html index 9e5a92d74..30eec9e80 100644 --- a/www/templates/app.html +++ b/www/templates/app.html @@ -14,12 +14,6 @@ - - From 2fdef5343a053afb941b027cd443fe090c930b7d Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 18 Jan 2021 17:10:56 -0500 Subject: [PATCH 17/24] refactor and restore polyfills plugin --- greenwood.config.js | 8 +- packages/plugin-polyfills/src/index.js | 89 ++++++++++++------- .../test/cases/default/default.spec.js | 42 +++++++-- .../test/cases/default/greenwood.config.js | 2 +- 4 files changed, 97 insertions(+), 44 deletions(-) diff --git a/greenwood.config.js b/greenwood.config.js index 375b37690..86ac98052 100644 --- a/greenwood.config.js +++ b/greenwood.config.js @@ -1,8 +1,8 @@ const fs = require('fs'); const path = require('path'); -const { ResourceInterface } = require('./packages/cli/src/lib/resource-interface'); const pluginGoogleAnalytics = require('./packages/plugin-google-analytics/src/index'); -// const pluginPolyfills = require('./packages/plugin-polyfills/src/index'); +const pluginPolyfills = require('./packages/plugin-polyfills/src/index'); +const { ResourceInterface } = require('./packages/cli/src/lib/resource-interface'); const META_DESCRIPTION = 'A modern and performant static site generator supporting Web Component based development'; const FAVICON_HREF = '/assets/favicon.ico'; @@ -55,8 +55,8 @@ module.exports = { }, pluginGoogleAnalytics({ analyticsId: 'UA-147204327-1' - }) - // TODO ...pluginPolyfills() + }), + pluginPolyfills() ], markdown: { plugins: [ diff --git a/packages/plugin-polyfills/src/index.js b/packages/plugin-polyfills/src/index.js index e8deecf33..31186a735 100644 --- a/packages/plugin-polyfills/src/index.js +++ b/packages/plugin-polyfills/src/index.js @@ -1,34 +1,61 @@ -const CopyWebpackPlugin = require('copy-webpack-plugin'); // part of @greeenwood/cli +const fs = require('fs'); const path = require('path'); +const { ResourceInterface } = require('@greenwood/cli/src/lib/resource-interface'); -module.exports = () => { - const filename = 'webcomponents-loader.js'; - const nodeModuleRoot = 'node_modules/@webcomponents/webcomponentsjs'; - - return [{ - type: 'index', - provider: () => { - return { - hookGreenwoodPolyfills: ` - - - ` - }; - } - }, { - type: 'webpack', - provider: (compilation) => { - const cwd = process.cwd(); - const { publicDir } = compilation.context; - - return new CopyWebpackPlugin([{ - from: path.join(cwd, nodeModuleRoot, filename), - to: publicDir - }, { - context: path.join(cwd, nodeModuleRoot), - from: 'bundles/*.js', - to: publicDir - }]); - } - }]; +class PolyfillsResource extends ResourceInterface { + constructor(compilation, options = {}) { + super(compilation, options); + } + + shouldOptimize() { + return true; + } + + async optimize(html) { + const filename = 'webcomponents-loader.js'; + const nodeModuleRoot = 'node_modules/@webcomponents/webcomponentsjs'; + + return new Promise(async (resolve, reject) => { + try { + const cwd = process.cwd(); + const { outputDir } = this.compilation.context; + const polyfillFiles = [ + 'webcomponents-loader.js', + ...fs.readdirSync(path.join(process.cwd(), nodeModuleRoot, 'bundles')).map(file => { + return `bundles/${file}`; + }) + ]; + + if (!fs.existsSync(path.join(outputDir, 'bundles'))) { + fs.mkdirSync(path.join(outputDir, 'bundles')); + } + + await Promise.all(polyfillFiles.map(async (file) => { + const from = path.join(cwd, nodeModuleRoot, file); + const to = path.join(outputDir, file); + + return !fs.existsSync(to) + ? fs.promises.copyFile(from, to) + : Promise.resolve(); + })); + + const newHtml = html.replace('', ` + + + `); + + resolve(newHtml); + } catch (e) { + reject(e); + } + }); + } +} + +module.exports = (options = {}) => { + return { + type: 'resource', + name: 'plugin-polyfills', + provider: (compilation) => new PolyfillsResource(compilation, options) + }; }; \ No newline at end of file diff --git a/packages/plugin-polyfills/test/cases/default/default.spec.js b/packages/plugin-polyfills/test/cases/default/default.spec.js index a580a4577..286f0879d 100644 --- a/packages/plugin-polyfills/test/cases/default/default.spec.js +++ b/packages/plugin-polyfills/test/cases/default/default.spec.js @@ -22,12 +22,24 @@ * Greenwood default (src/) */ const expect = require('chai').expect; +const fs = require('fs'); const { JSDOM } = require('jsdom'); const path = require('path'); -const runSmokeTest = require('../../../../../test/smoke-test'); const TestBed = require('../../../../../test/test-bed'); -xdescribe('Build Greenwood With: ', function() { +const expectedPolyfillFiles = [ + 'webcomponents-loader.js', + 'webcomponents-ce.js', + 'webcomponents-ce.js.map', + 'webcomponents-sd-ce-pf.js', + 'webcomponents-sd-ce-pf.js.map', + 'webcomponents-sd-ce.js', + 'webcomponents-sd-ce.js.map', + 'webcomponents-sd.js', + 'webcomponents-sd.js.map' +]; + +describe('Build Greenwood With: ', function() { const LABEL = 'Polyfill Plugin with default options and Default Workspace'; let setup; @@ -35,10 +47,16 @@ xdescribe('Build Greenwood With: ', function() { before(async function() { setup = new TestBed(); - this.context = await setup.setupTestBed(__dirname, [{ - dir: 'node_modules/@webcomponents/webcomponentsjs', - name: 'webcomponents-loader.js' - }]); + this.context = await setup.setupTestBed(__dirname, expectedPolyfillFiles.map((file) => { + const dir = file === 'webcomponents-loader.js' + ? 'node_modules/@webcomponents/webcomponentsjs' + : 'node_modules/@webcomponents/webcomponentsjs/bundles'; + + return { + dir, + name: file + }; + })); }); describe(LABEL, function() { @@ -46,7 +64,7 @@ xdescribe('Build Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'index', 'not-found', 'hello'], LABEL); + // runSmokeTest(['not-found'], LABEL); describe('Script tag in the tag', function() { let dom; @@ -61,10 +79,18 @@ xdescribe('Build Greenwood With: ', function() { // hyphen is used to make sure no other bundles get loaded by accident (#9) return script.src.indexOf('/webcomponents-') >= 0; }); - + expect(polyfillScriptTags.length).to.be.equal(1); }); + it('should have the expected polyfill files in the output directory', function() { + expectedPolyfillFiles.forEach((file) => { + const dir = file === 'webcomponents-loader.js' ? '' : 'bundles/'; + + expect(fs.existsSync(path.join(this.context.publicDir, `${dir}${file}`))).to.be.equal(true); + }); + }); + }); }); diff --git a/packages/plugin-polyfills/test/cases/default/greenwood.config.js b/packages/plugin-polyfills/test/cases/default/greenwood.config.js index cd3d29444..df0a2b27d 100644 --- a/packages/plugin-polyfills/test/cases/default/greenwood.config.js +++ b/packages/plugin-polyfills/test/cases/default/greenwood.config.js @@ -2,6 +2,6 @@ const polyfillsPlugin = require('../../../src/index'); module.exports = { plugins: [ - ...polyfillsPlugin() + polyfillsPlugin() ] }; \ No newline at end of file From d0330d77a9c5cdecd7e176bf8d4776325245271a Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 18 Jan 2021 18:37:16 -0500 Subject: [PATCH 18/24] plugin documentation refactoring --- packages/cli/src/lib/resource-interface.js | 8 +- packages/cli/src/lifecycles/serialize.js | 1 - ...composite-plugins.md => custom-plugins.md} | 8 +- www/pages/plugins/index-hooks.md | 111 --------------- www/pages/plugins/index.md | 64 +++++---- www/pages/plugins/resources.md | 132 ++++++++++++++++++ www/pages/plugins/webpack.md | 45 ------ 7 files changed, 176 insertions(+), 193 deletions(-) rename www/pages/plugins/{composite-plugins.md => custom-plugins.md} (79%) delete mode 100644 www/pages/plugins/index-hooks.md create mode 100644 www/pages/plugins/resources.md delete mode 100644 www/pages/plugins/webpack.md diff --git a/packages/cli/src/lib/resource-interface.js b/packages/cli/src/lib/resource-interface.js index 04ef513c9..c5a4f4a9f 100644 --- a/packages/cli/src/lib/resource-interface.js +++ b/packages/cli/src/lib/resource-interface.js @@ -39,20 +39,20 @@ class ResourceInterface { } // eslint-disable-next-line no-unused-vars - async intercept(url, headers) { - return Promise.resolve(url); + async intercept(contents, headers) { + return Promise.resolve(contents); } // handle a (final) resource type post build, pre optimize, // ex: remove es shim - - ` - } - } - }] -} -``` -### Custom Index File -It should be noted that if these specific hook types are too limiting Greenwood supports providing your own _index.html_ in the root of your workspace directory. This can either be used to define your own hooks or just hardcode everything you need instead of using plugins. - -The minimum recommended markup for a custom _index.html_ would be this following: -```html - - - - - - - - - - - <%= htmlWebpackPlugin.options.hookSpaIndexFallback %> - - - - - - - - - - -``` - -To add your own hook, define it in a _greenwood.config.js_ -```js -module.exports = { - - ... - - plugins: [{ - type: 'index', - provider: (compilation) => { - // you can access things like config, context if you need from compilation - return { - myCustomHook: ` -
My custom HTML here
- ` - ] - } - }] - -} -``` - - -And updated _index.html_ -```html - - - - ... - - - - - - <%= htmlWebpackPlugin.options.myCustomHook %> - - - -``` - -> For reference, here is the [default _index.html_](https://github.com/ProjectEvergreen/greenwood/blob/master/packages/cli/src/templates/index.html) provided by Greenwood. You can mix and match with your own hooks and Greenwood's hooks to support whatever best suits your needs. \ No newline at end of file diff --git a/www/pages/plugins/index.md b/www/pages/plugins/index.md index 672bca36c..9d6d5e0d9 100644 --- a/www/pages/plugins/index.md +++ b/www/pages/plugins/index.md @@ -9,21 +9,23 @@ index: 4 At its core, Greenwood provides a CLI to drive all the development related workflows for a Greenwood project. The CLI aims to provide a simple interface for quickly and simply building sites from as little as markdown files. -However, for more complex sites and use cases, there will come a need to extend the default functionality of Greenwood for additional capabilities like: -- Site Analytics (Google, Snowplow) -- Progressive Web App experiences (PWA) +However, for more complex sites and use cases, there will come a need to extend the default functionality of Greenwood and the standard web primitives (e.g. HTML, CSS, JS) for additional capabilities like: +- Integrating Site Analytics (Google, Snowplow) +- Building out Progressive Web App (PWA) experiences - Consuming content from a CMS (like Wordpress, Drupal) +- Supporting additional file types, like TypeScript - Whatever you can think of! Greenwood aims to cater to these use cases through two approaches: -1. A plugin based architecture exposing low level "primitives" of the Greenwood build that anyone can extend. +1. A plugin based architecture exposing areas of the Greenwood build that anyone can tap into. 1. A set of pre-built plugins to help facilitate some of the most common uses cases and workflows, that don't require needing to know anything about the low level APIs. ### API -Each plugin type requires two properties. -- `type`: A string to specify to Greenwood the type of plugin. Can be one of the following values: `'index'`, `'webpack'` -- `provider`: A function that will be invoked by Greenwood during the build, determined by the `type`. Can accept a `compilation` param that provides read-only access to parts of Greenwood's state and configuration that can be used by a plugin. +Each plugin must return a function that has the following three properties:. +- `name`: A string to give your plugin a name and used for error handling and troubleshooting. +- `type`: A string to specify to Greenwood the type of plugin. Right now the current supported plugin type `'resource'` +- `provider`: A function that will be invoked by Greenwood that Can accept a `compilation` param that provides read-only access to parts of Greenwood's state and configuration that can be used by a plugin. Here is an example of creating a plugin in a _greenwood.config.js_. ```javascript @@ -31,17 +33,22 @@ module.exports = { ... - plugins: [{ - type: 'webpack', - provider: (compilation) => { - // do stuff here + plugins: [ + ({ opt1: 'something' }) => { + return { + name: 'my-plugin', + type: 'resource', + provider: (compilation) => { + // do stuff here + } + } } }] } ``` - -`compilation` provides read-only access to the follow objects: +- `options` - Passed in by users when they run the exported function: +- `compilation` - Provides read-only access to the follow objects: #### Config This is Greenwood's default configuration options merged with any user provided configuration options in _greenwood.config.js_. See the [configuration docs](/docs/configuration/) for more info. @@ -52,9 +59,10 @@ module.exports = { title: 'My Blog', plugins: [{ - type: 'index|webpack', + name: 'my-plugin', + type: 'resource', provider: (compilation) => { - console.log(compilation.config.title); // My Blog + console.log(`Title of the site is ${compilation.config.title}`); // My Blog } }] @@ -65,12 +73,14 @@ module.exports = { This provides access to all the input / output directories and file paths Greenwood uses to build the site and output all the generated files. Context is especially useful for copying files or writing to the build directory. Here are paths you can get from `context`, all of which are absolute URLs: -- `scratchDir`: Greenwood's temporay output file (_.greenwood/_) -- `publicDir`: Where Greenwood outputs the final static site -- `pagesDir`: Path to the _pages/_ directory in the workspace -- `templatesDir`: Path to the _templates/_ directory in the workspace + +- `outputDir`: Where Greenwood outputs the final static site +- `pagesDir`: Path to the _pages/_ directory in the user's workspace +- `projectDirectory`: Path to the root of the current project +- `scratchDir`: Path to Greenwood's temporay output file directory (`${process.cwd()}.greenwood/`)s +- `userTemplatesDir`: Path to the _templates/_ directory in the user's workspace - `userWorkspace`: Path to the workspace directory (_src/_ by default) -- `assetDir`: Path to the _assets/_ directory in the workspace + Example using `context` to write to `publicDir` from _greenwood.config.js_ ```javascript @@ -80,9 +90,10 @@ const path = require('path'); module.exports = { plugins: [{ - type: 'index|webpack', + name: 'my-plugin' + type: 'resource', provider: (compilation) => { - const outputDir = compilation.context.outputDir; + const { outputDir } = compilation.context; fs.writeFileSync(path.join(outputDir, 'robots.txt'), 'Hello World!'); } @@ -91,11 +102,8 @@ module.exports = { } ``` -### Types +### Plugin Types While each API has its own documentation section on the left sidebar of this page, here is a quick overview of the current set of Plugin APIs Greenwood supports. -#### Index Hooks -It is common when working with certain libraries (3rd party or otherwise) that scripts _must_ be loaded globally and / or unbundled. Good examples of these are analytics libraries and polyfills. With an index hook plugin, users can leverage predefined "injection" sites to add this code to their project's _index.html_. - -#### Webpack Plugins -Feel comfortable with **webpack**? Use this plugin type to pass in a **webpack** plugin directly! \ No newline at end of file +#### Resource Plugins +Resource plugins allow users to interact with the request and response lifecycles of files at a variety of different ways. These lifecycles provide the ability to do things like introduce new file types, to adding hosted 3rd party scripts to your site. \ No newline at end of file diff --git a/www/pages/plugins/resources.md b/www/pages/plugins/resources.md new file mode 100644 index 000000000..a86e1244b --- /dev/null +++ b/www/pages/plugins/resources.md @@ -0,0 +1,132 @@ +--- +label: 'Resources' +menu: side +title: 'Resources' +index: 2 +--- + +## Resources + +Resource plugins allow developers to interact with the request and response lifecycles of files at a variety of different points along the development and build workflow, when running the `develop` and `build` commands. These lifecycles provide the ability to do things like: +- Integrating Site Analytics (Google, Snowplow) in each generated _index.html_ page +- Introduce additional file types, like TypeScript + +### API (Resource Interface) +Although JavaScript is loosely typed, a [resource "interface"](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/lib/resource-interface.js) has been provided by Greenwood that you can use to start building our own resource plugins. Effectively you have to define two things: +- `extensions`: The file types your plugin will operate on +- `contentType`: A browser compatible contentType to ensure browsers correctly interpret you transformations + +```javascript +const fs = require('fs'); +const { ResourceInterface } = require('@greenwood/cli/src/lib/resource-interface'); + +class ExampleResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + this.extensions = ['.xyz']; + this.contentType = 'text/something'; + } + + // test if this plugin should be used to process a given for the browser, ex: ` + + `); + + resolve(newContents); + } catch (e) { + reject(e); + } + }); + } +} + +module.exports = { + type: 'resource', + name: 'plugin-node-modules', + provider: (compilation, options) => new NodeModulesResource(compilation, options) +}; \ No newline at end of file diff --git a/packages/cli/src/plugins/resource/plugin-resolver-node-modules.js b/packages/cli/src/plugins/resource/plugin-resolver-node-modules.js deleted file mode 100644 index 417aa0f97..000000000 --- a/packages/cli/src/plugins/resource/plugin-resolver-node-modules.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * - * Detects and fully resolve srequest to node_modules. - * - */ -const path = require('path'); -const { ResourceInterface } = require('../../lib/resource-interface'); - -class NodeModulesResource extends ResourceInterface { - constructor(compilation, options) { - super(compilation, options); - this.extensions = ['*']; - } - - shouldResolve(url) { - return url.indexOf('node_modules/') >= 0; - } - - async resolve(url) { - return new Promise((resolve, reject) => { - try { - const relativeUrl = url.replace(this.compilation.context.userWorkspace, ''); - const nodeModulesUrl = path.join(process.cwd(), relativeUrl); - - resolve(nodeModulesUrl); - } catch (e) { - console.error(e); - reject(e); - } - }); - } -} - -module.exports = { - type: 'resource', - name: 'plugin-node-modules-resolver', - provider: (compilation, options) => new NodeModulesResource(compilation, options) -}; \ No newline at end of file diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js index 5bce9a48c..3beb0c036 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-html.js +++ b/packages/cli/src/plugins/resource/plugin-standard-html.js @@ -4,7 +4,6 @@ * This is a Greenwood default plugin. * */ -const acorn = require('acorn'); const frontmatter = require('front-matter'); const fs = require('fs'); const htmlparser = require('node-html-parser'); @@ -16,7 +15,6 @@ const remarkParse = require('remark-parse'); const remarkRehype = require('remark-rehype'); const { ResourceInterface } = require('../../lib/resource-interface'); const unified = require('unified'); -const walk = require('acorn-walk'); // TODO better error handling / messaging for users if things are not where they are expected to be // general refactoring @@ -101,61 +99,6 @@ const getUserScripts = (contents, userWorkspace) => { `); } - contents = contents.replace(/type="module"/g, 'type="module-shim"'); - - const importMap = {}; - const userPackageJson = fs.existsSync(`${userWorkspace}/package.json`) - ? require(path.join(userWorkspace, 'package.json')) // its a monorepo? - : fs.existsSync(`${process.cwd()}/package.json`) - ? require(path.join(process.cwd(), 'package.json')) - : {}; - - // console.debug('userPackageJson', userPackageJson); - // console.debug('dependencies', userPackageJson.dependencies); - - Object.keys(userPackageJson.dependencies || {}).forEach(dependency => { - const packageRootPath = path.join(process.cwd(), './node_modules', dependency); - const packageJsonPath = path.join(packageRootPath, 'package.json'); - const packageJson = require(packageJsonPath); - const packageEntryPointPath = path.join(process.cwd(), './node_modules', dependency, packageJson.main); - const packageFileContents = fs.readFileSync(packageEntryPointPath, 'utf-8'); - - walk.simple(acorn.parse(packageFileContents, { sourceType: 'module' }), { - ImportDeclaration(node) { - // console.log('Found a ImportDeclaration'); - const sourceValue = node.source.value; - - if (sourceValue.indexOf('.') !== 0 && sourceValue.indexOf('http') !== 0) { - // console.log(`found a bare import for ${sourceValue}!!!!!`); - importMap[sourceValue] = `/node_modules/${sourceValue}`; - } - }, - ExportNamedDeclaration(node) { - // console.log('Found a ExportNamedDeclaration'); - const sourceValue = node && node.source ? node.source.value : ''; - - if (sourceValue.indexOf('.') !== 0 && sourceValue.indexOf('http') !== 0) { - // console.log(`found a bare export for ${sourceValue}!!!!!`); - importMap[sourceValue] = `/node_modules/${sourceValue}`; - } - } - }); - - importMap[dependency] = `/node_modules/${dependency}/${packageJson.main}`; - }); - - // console.log('importMap all complete', importMap); - - contents = contents.replace('', ` - - - - `); - if (process.env.__GWD_COMMAND__ === 'build') { // eslint-disable-line no-underscore-dangle // TODO setup and teardown should be done together // console.debug('running in build mode, polyfill WebComponents for puppeteer'); diff --git a/packages/cli/src/plugins/resource/plugin-resolver-user-workspace.js b/packages/cli/src/plugins/resource/plugin-user-workspace.js similarity index 76% rename from packages/cli/src/plugins/resource/plugin-resolver-user-workspace.js rename to packages/cli/src/plugins/resource/plugin-user-workspace.js index 75fb71a7f..75ce15f6a 100644 --- a/packages/cli/src/plugins/resource/plugin-resolver-user-workspace.js +++ b/packages/cli/src/plugins/resource/plugin-user-workspace.js @@ -7,7 +7,7 @@ const path = require('path'); const { ResourceInterface } = require('../../lib/resource-interface'); -class UserWorkspaceResolverResource extends ResourceInterface { +class UserWorkspaceResource extends ResourceInterface { constructor(compilation, options) { super(compilation, options); this.extensions = ['*']; @@ -28,6 +28,6 @@ class UserWorkspaceResolverResource extends ResourceInterface { module.exports = { type: 'resource', - name: 'plugin-user-workspace-resolver', - provider: (compilation, options) => new UserWorkspaceResolverResource(compilation, options) + name: 'plugin-user-workspace', + provider: (compilation, options) => new UserWorkspaceResource(compilation, options) }; \ No newline at end of file From ba8ba755fda16367228135ded552e9aab41b34ab Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Wed, 20 Jan 2021 11:28:59 -0500 Subject: [PATCH 23/24] fix linting --- packages/cli/src/plugins/resource/plugin-standard-html.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js index 3beb0c036..590cfb6fa 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-html.js +++ b/packages/cli/src/plugins/resource/plugin-standard-html.js @@ -88,7 +88,7 @@ const getAppTemplate = (contents, userWorkspace) => { return appTemplateContents || contents; }; -const getUserScripts = (contents, userWorkspace) => { +const getUserScripts = (contents) => { // TODO use an HTML parser? https://www.npmjs.com/package/node-html-parser if (process.env.__GWD_COMMAND__ === 'develop') { // eslint-disable-line no-underscore-dangle // TODO setup and teardown should be done together @@ -229,7 +229,7 @@ class StandardHtmlResource extends ResourceInterface { body = getPageTemplate(barePath, userWorkspace, template); body = getAppTemplate(body, userWorkspace); - body = getUserScripts(body, userWorkspace); + body = getUserScripts(body); body = getMetaContent(normalizedUrl, config, body); if (processedMarkdown) { From 39ba6f0e44073cd85a0ebf0362fb1feecc10bc7b Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Wed, 20 Jan 2021 20:15:48 -0500 Subject: [PATCH 24/24] remove example foo code --- greenwood.config.js | 42 +++++----------------------------------- www/components/grant.foo | 8 -------- www/components/owen.foo | 9 --------- www/pages/index.html | 1 - 4 files changed, 5 insertions(+), 55 deletions(-) delete mode 100644 www/components/grant.foo delete mode 100644 www/components/owen.foo diff --git a/greenwood.config.js b/greenwood.config.js index 86ac98052..970f3e119 100644 --- a/greenwood.config.js +++ b/greenwood.config.js @@ -1,38 +1,10 @@ -const fs = require('fs'); const path = require('path'); const pluginGoogleAnalytics = require('./packages/plugin-google-analytics/src/index'); const pluginPolyfills = require('./packages/plugin-polyfills/src/index'); -const { ResourceInterface } = require('./packages/cli/src/lib/resource-interface'); const META_DESCRIPTION = 'A modern and performant static site generator supporting Web Component based development'; const FAVICON_HREF = '/assets/favicon.ico'; -class FooResource extends ResourceInterface { - constructor(compilation, options) { - super(compilation, options); - - this.extensions = ['.foo']; - this.contentType = 'text/javascript'; - } - - async serve(url) { - return new Promise(async (resolve, reject) => { - try { - let body = await fs.promises.readFile(url, 'utf-8'); - - body = body.replace(/interface (.*){(.*)}/s, ''); - - resolve({ - body, - contentType: this.contentType - }); - } catch (e) { - reject(e); - } - }); - } -} - module.exports = { workspace: path.join(__dirname, 'www'), title: 'Greenwood', @@ -48,15 +20,11 @@ module.exports = { { rel: 'icon', href: FAVICON_HREF }, { name: 'google-site-verification', content: '4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0' } ], - plugins: [{ - type: 'resource', - name: 'plugin-foo', - provider: (compilation, options) => new FooResource(compilation, options) - }, - pluginGoogleAnalytics({ - analyticsId: 'UA-147204327-1' - }), - pluginPolyfills() + plugins: [ + pluginGoogleAnalytics({ + analyticsId: 'UA-147204327-1' + }), + pluginPolyfills() ], markdown: { plugins: [ diff --git a/www/components/grant.foo b/www/components/grant.foo deleted file mode 100644 index 09aeb9905..000000000 --- a/www/components/grant.foo +++ /dev/null @@ -1,8 +0,0 @@ -interface User { - id: number - firstName: string - lastName: string - role: string -} - -console.log('hello from grant.foo! 😎'); \ No newline at end of file diff --git a/www/components/owen.foo b/www/components/owen.foo deleted file mode 100644 index 9516295a8..000000000 --- a/www/components/owen.foo +++ /dev/null @@ -1,9 +0,0 @@ -import './grant.foo'; - -const node = document.createElement('h6'); -const textnode = document.createTextNode('hello from my-custom-file.foo 👋'); - -node.appendChild(textnode); -document.getElementsByTagName('body')[0].appendChild(node); - -console.log('hello from owen.foo! 👋'); \ No newline at end of file diff --git a/www/pages/index.html b/www/pages/index.html index 8f1729671..15303d32d 100644 --- a/www/pages/index.html +++ b/www/pages/index.html @@ -8,7 +8,6 @@ -