diff --git a/greenwood.config.js b/greenwood.config.js index d9eb4f88c..6744bc534 100644 --- a/greenwood.config.js +++ b/greenwood.config.js @@ -11,6 +11,7 @@ const FAVICON_HREF = '/assets/favicon.ico'; module.exports = { workspace: path.join(__dirname, 'www'), + mode: 'mpa', title: 'Greenwood', meta: [ { name: 'description', content: META_DESCRIPTION }, diff --git a/packages/cli/src/config/rollup.config.js b/packages/cli/src/config/rollup.config.js index b9ce136d9..262650b37 100644 --- a/packages/cli/src/config/rollup.config.js +++ b/packages/cli/src/config/rollup.config.js @@ -1,3 +1,4 @@ +/* eslint-disable max-depth, no-loop-func */ const Buffer = require('buffer').Buffer; const fs = require('fs'); const htmlparser = require('node-html-parser'); @@ -9,7 +10,6 @@ const postcss = require('postcss'); const postcssImport = require('postcss-import'); const replace = require('@rollup/plugin-replace'); const { terser } = require('rollup-plugin-terser'); - const tokenSuffix = 'scratch'; const tokenNodeModules = 'node_modules/'; @@ -98,7 +98,8 @@ function greenwoodWorkspaceResolver (compilation) { // https://github.com/rollup/rollup/issues/2873 function greenwoodHtmlPlugin(compilation) { const { projectDirectory, userWorkspace, outputDir, scratchDir } = compilation.context; - const isRemoteUrl = (url = '') => url.indexOf('http') === 0 || url.indexOf('//') === 0; + const { optimization } = compilation.config; + const isRemoteUrl = (url = undefined) => url && (url.indexOf('http') === 0 || url.indexOf('//') === 0); const customResources = compilation.config.plugins.filter((plugin) => { return plugin.type === 'resource'; }).map((plugin) => { @@ -159,25 +160,29 @@ function greenwoodHtmlPlugin(compilation) { // handle if (!isRemoteUrl(parsedAttributes.src) && parsedAttributes.type === 'module' && parsedAttributes.src && !mappedScripts.get(parsedAttributes.src)) { - const { src } = parsedAttributes; - - // TODO avoid using href and set it to the value of rollup fileName instead - // since user paths can still be the same file, - // e.g. ../theme.css and ./theme.css are still the same file - mappedScripts.set(src, true); + if (optimization === 'static') { + // console.debug('dont emit ', parsedAttributes.src); + } else { + const { src } = parsedAttributes; - const srcPath = src.replace('../', './'); - const basePath = srcPath.indexOf(tokenNodeModules) >= 0 - ? projectDirectory - : userWorkspace; - const source = fs.readFileSync(path.join(basePath, srcPath), 'utf-8'); - - this.emitFile({ - type: 'chunk', - id: srcPath.replace('/node_modules', path.join(projectDirectory, tokenNodeModules)), - name: srcPath.split('/')[srcPath.split('/').length - 1].replace('.js', ''), - source - }); + // TODO avoid using href and set it to the value of rollup fileName instead + // since user paths can still be the same file, + // e.g. ../theme.css and ./theme.css are still the same file + mappedScripts.set(src, true); + + const srcPath = src.replace('../', './'); + const basePath = srcPath.indexOf(tokenNodeModules) >= 0 + ? projectDirectory + : userWorkspace; + const source = fs.readFileSync(path.join(basePath, srcPath), 'utf-8'); + + this.emitFile({ + type: 'chunk', + id: srcPath.replace('/node_modules', path.join(projectDirectory, tokenNodeModules)), + name: srcPath.split('/')[srcPath.split('/').length - 1].replace('.js', ''), + source + }); + } } // handle @@ -312,10 +317,31 @@ function greenwoodHtmlPlugin(compilation) { for (const innerBundleId of Object.keys(bundles)) { const { src } = parsedAttributes; const facadeModuleId = bundles[innerBundleId].facadeModuleId; - const pathToMatch = src.replace('../', '').replace('./', ''); + let pathToMatch = src.replace('../', '').replace('./', ''); + + // special handling for node_modules paths + if (pathToMatch.indexOf(tokenNodeModules) >= 0) { + pathToMatch = pathToMatch.replace(`/${tokenNodeModules}`, ''); + + const pathToMatchPieces = pathToMatch.split('/'); + + pathToMatch = pathToMatch.replace(tokenNodeModules, ''); + pathToMatch = pathToMatch.replace(`${pathToMatchPieces[0]}/`, ''); + } if (facadeModuleId && facadeModuleId.indexOf(pathToMatch) > 0) { - newHtml = newHtml.replace(src, `/${innerBundleId}`); + const newSrc = `/${innerBundleId}`; + + newHtml = newHtml.replace(src, newSrc); + + if (optimization !== 'none' && optimization !== 'inline') { + newHtml = newHtml.replace('', ` + + + `); + } + } else if (optimization === 'static' && newHtml.indexOf(pathToMatch) > 0) { + newHtml = newHtml.replace(scriptTag, ''); } } } @@ -331,7 +357,16 @@ function greenwoodHtmlPlugin(compilation) { if (bundleId2.indexOf('.css') > 0) { const bundle2 = bundles[bundleId2]; if (href.indexOf(bundle2.name) >= 0) { - newHtml = newHtml.replace(href, `/${bundle2.fileName}`); + const newHref = `/${bundle2.fileName}`; + + newHtml = newHtml.replace(href, newHref); + + if (optimization !== 'none' && optimization !== 'inline') { + newHtml = newHtml.replace('', ` + + + `); + } } } } @@ -363,10 +398,29 @@ function greenwoodHtmlPlugin(compilation) { style: true }); const headScripts = root.querySelectorAll('script'); + const headLinks = root.querySelectorAll('link'); headScripts.forEach((scriptTag) => { const parsedAttributes = parseTagForAttributes(scriptTag); - + const isScriptSrcTag = parsedAttributes.src && parsedAttributes.type === 'module'; + + if (optimization === 'inline' && isScriptSrcTag && !isRemoteUrl(parsedAttributes.src)) { + const src = parsedAttributes.src; + const basePath = src.indexOf(tokenNodeModules) >= 0 + ? process.cwd() + : outputDir; + const outputPath = path.join(basePath, src); + const js = fs.readFileSync(outputPath, 'utf-8'); + + // scratchFiles[src] = true; + + html = html.replace(``, ` + + `); + } + // handle if (parsedAttributes.type === 'module' && !parsedAttributes.src) { for (const innerBundleId of Object.keys(bundles)) { @@ -381,6 +435,29 @@ function greenwoodHtmlPlugin(compilation) { } }); + if (optimization === 'inline') { + headLinks + .forEach((linkTag) => { + const linkTagAttributes = parseTagForAttributes(linkTag); + const isLocalLinkTag = linkTagAttributes.rel === 'stylesheet' + && !isRemoteUrl(linkTagAttributes.href); + + if (isLocalLinkTag) { + const href = linkTagAttributes.href; + const outputPath = path.join(outputDir, href); + const css = fs.readFileSync(outputPath, 'utf-8'); + + // scratchFiles[href] = true; + + html = html.replace(``, ` + + `); + } + }); + } + await fs.promises.writeFile(htmlPath, html); } else { const sourcePath = `${outputDir}/${bundleId}`; @@ -400,13 +477,29 @@ function greenwoodHtmlPlugin(compilation) { module.exports = getRollupConfig = async (compilation) => { const { scratchDir, outputDir } = compilation.context; - + const defaultRollupPlugins = [ + // TODO replace should come in via plugin-node-modules + replace({ // https://github.com/rollup/rollup/issues/487#issuecomment-177596512 + 'process.env.NODE_ENV': JSON.stringify('production') + }), + nodeResolve(), // TODO move to plugin-node-modules + greenwoodWorkspaceResolver(compilation), + greenwoodHtmlPlugin(compilation), + multiInput(), + json() // TODO make it part plugin-standard-json + ]; // TODO greenwood standard plugins, then "Greenwood" plugins, then user plugins const customRollupPlugins = compilation.config.plugins.filter((plugin) => { return plugin.type === 'rollup'; }).map((plugin) => { return plugin.provider(compilation); }).flat(); + + if (compilation.config.optimization !== 'none') { + defaultRollupPlugins.push( + terser() // TODO extract to plugin-standard-javascript + ); + } return [{ // TODO Avoid .greenwood/ directory, do everything in public/? @@ -424,16 +517,7 @@ module.exports = getRollupConfig = async (compilation) => { } }, plugins: [ - // TODO replace should come in via plugins? - replace({ // https://github.com/rollup/rollup/issues/487#issuecomment-177596512 - 'process.env.NODE_ENV': JSON.stringify('production') - }), - nodeResolve(), // TODO move to plugin - greenwoodWorkspaceResolver(compilation), - greenwoodHtmlPlugin(compilation), - multiInput(), - json(), // TODO bundle as part of import support / transforms API? - terser(), // TODO extract to a plugin + ...defaultRollupPlugins, ...customRollupPlugins ] }]; diff --git a/packages/cli/src/lib/router.js b/packages/cli/src/lib/router.js new file mode 100644 index 000000000..12ac91e80 --- /dev/null +++ b/packages/cli/src/lib/router.js @@ -0,0 +1,43 @@ +/* eslint-disable no-underscore-dangle */ +document.addEventListener('click', function(e) { + e.preventDefault(); + + if (e.path[0].href) { + console.debug('linked clicked was...', e.path[0].href); + const target = e.path[0]; + const route = target.href.replace(window.location.origin, ''); + const routerOutlet = Array.from(document.getElementsByTagName('greenwood-route')).filter(outlet => { + return outlet.getAttribute('data-route') === route; + })[0]; + + console.debug('routerOutlet', routerOutlet); + + if (routerOutlet.getAttribute('data-template') === window.__greenwood.currentTemplate) { + window.__greenwood.currentTemplate = routerOutlet.getAttribute('data-template'); + routerOutlet.loadRoute(); + } else { + console.debug('new template detected, should do a hard reload'); + window.location.href = target; + } + } +}); + +class RouteComponent extends HTMLElement { + loadRoute() { + const route = this.getAttribute('data-route'); + const key = this.getAttribute('data-key'); + console.debug('load route ->', route); + console.debug('with bundle ->', key); + fetch(key) + .then(res => res.text()) + .then((response) => { + history.pushState(response, route, route); + document.getElementsByTagName('router-outlet')[0].innerHTML = response; + }); + } +} + +class RouterOutletComponent extends HTMLElement { } + +customElements.define('greenwood-route', RouteComponent); +customElements.define('router-outlet', RouterOutletComponent); \ No newline at end of file diff --git a/packages/cli/src/lifecycles/config.js b/packages/cli/src/lifecycles/config.js index 7e931844e..956dedb84 100644 --- a/packages/cli/src/lifecycles/config.js +++ b/packages/cli/src/lifecycles/config.js @@ -1,13 +1,16 @@ const fs = require('fs'); const path = require('path'); -// TODO const optimizations = ['strict', 'spa']; -let defaultConfig = { +const modes = ['ssg', 'mpa']; +const optimizations = ['default', 'none', 'static', 'inline']; + +const defaultConfig = { workspace: path.join(process.cwd(), 'src'), devServer: { port: 1984 }, - optimization: '', + mode: modes[0], + optimization: optimizations[0], title: 'My App', meta: [], plugins: [], @@ -23,7 +26,7 @@ module.exports = readAndMergeConfig = async() => { if (fs.existsSync(path.join(process.cwd(), 'greenwood.config.js'))) { const userCfgFile = require(path.join(process.cwd(), 'greenwood.config.js')); - const { workspace, devServer, title, markdown, meta, plugins } = userCfgFile; + const { workspace, devServer, title, markdown, meta, mode, optimization, plugins } = userCfgFile; // workspace validation if (workspace) { @@ -61,12 +64,17 @@ module.exports = readAndMergeConfig = async() => { customConfig.meta = meta; } - // TODO - // 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 (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 (plugins && plugins.length > 0) { const types = ['resource', 'rollup', 'server']; diff --git a/packages/cli/src/lifecycles/serialize.js b/packages/cli/src/lifecycles/serialize.js index 0012fc5c3..a11019da7 100644 --- a/packages/cli/src/lifecycles/serialize.js +++ b/packages/cli/src/lifecycles/serialize.js @@ -2,12 +2,14 @@ const BrowserRunner = require('../lib/browser'); const fs = require('fs'); const path = require('path'); const pluginResourceStandardHtml = require('../plugins/resource/plugin-standard-html'); +const pluginOptimizationMpa = require('../plugins/resource/plugin-optimization-mpa'); module.exports = serializeCompilation = async (compilation) => { const compilationCopy = Object.assign({}, compilation); const browserRunner = new BrowserRunner(); const optimizeResources = [ pluginResourceStandardHtml.provider(compilationCopy), + pluginOptimizationMpa().provider(compilationCopy), ...compilation.config.plugins.filter((plugin) => { const provider = plugin.provider(compilationCopy); diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js index 24619e6d0..8a41808bb 100644 --- a/packages/cli/src/lifecycles/serve.js +++ b/packages/cli/src/lifecycles/serve.js @@ -3,6 +3,7 @@ const path = require('path'); const Koa = require('koa'); const pluginNodeModules = require('../plugins/resource/plugin-node-modules'); +const pluginResourceOptimizationMpa = require('../plugins/resource/plugin-optimization-mpa'); const pluginResourceStandardCss = require('../plugins/resource/plugin-standard-css'); const pluginResourceStandardFont = require('../plugins/resource/plugin-standard-font'); const pluginResourceStandardHtml = require('../plugins/resource/plugin-standard-html'); @@ -26,6 +27,7 @@ function getDevServer(compilation) { pluginResourceStandardImage.provider(compilationCopy), pluginResourceStandardJavaScript.provider(compilationCopy), pluginResourceStandardJson.provider(compilationCopy), + pluginResourceOptimizationMpa().provider(compilationCopy), // custom user resource plugins ...compilation.config.plugins.filter((plugin) => { diff --git a/packages/cli/src/plugins/resource/plugin-optimization-mpa.js b/packages/cli/src/plugins/resource/plugin-optimization-mpa.js new file mode 100644 index 000000000..8f1d72f53 --- /dev/null +++ b/packages/cli/src/plugins/resource/plugin-optimization-mpa.js @@ -0,0 +1,106 @@ +/* + * + * Manages web standard resource related operations for JavaScript. + * This is a Greenwood default plugin. + * + */ +const fs = require('fs'); +const path = require('path'); +const { ResourceInterface } = require('../../lib/resource-interface'); + +class OptimizationMPAResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + this.extensions = ['.html']; + this.contentType = 'text/html'; + this.libPath = '@greenwood/router/router.js'; + } + + async shouldResolve(url) { + return Promise.resolve(url.indexOf(this.libPath) >= 0); + } + + async resolve() { + return new Promise(async (resolve, reject) => { + try { + const routerUrl = path.join(__dirname, '../../', 'lib/router.js'); + + resolve(routerUrl); + } catch (e) { + reject(e); + } + }); + } + + async shouldOptimize(url) { + return Promise.resolve(path.extname(url) === '.html' && this.compilation.config.mode === 'mpa'); + } + + async optimize(url, body) { + return new Promise(async (resolve, reject) => { + try { + let currentTemplate; + const { projectDirectory, scratchDir, outputDir } = this.compilation.context; + const bodyContents = body.match(/(.*)<\/body>/s)[0].replace('', '').replace('', ''); + const outputBundlePath = `${outputDir}/_routes${url.replace(projectDirectory, '')}` + .replace('.greenwood/', '') + .replace('//', '/'); + + const routeTags = this.compilation.graph.map((page) => { + const template = path.extname(page.filename) === '.html' + ? page.route + : page.template; + const key = page.route === '/' + ? '' + : page.route.slice(0, page.route.lastIndexOf('/')); + + if (url.replace(scratchDir, '') === `${page.route}index.html`) { + currentTemplate = template; + } + return ` + + `; + }); + + if (!fs.existsSync(path.dirname(outputBundlePath))) { + fs.mkdirSync(path.dirname(outputBundlePath), { + recursive: true + }); + } + + await fs.promises.writeFile(outputBundlePath, bodyContents); + + body = body.replace('', ` + \n + + + `).replace(/(.*)<\/body>/s, ` + \n + + + ${bodyContents}\n + + + ${routeTags.join('\n')} + + `); + + resolve(body); + } catch (e) { + reject(e); + } + }); + } +} + +module.exports = (options = {}) => { + return { + type: 'resource', + name: 'plugin-optimization-mpa', + provider: (compilation) => new OptimizationMPAResource(compilation, options) + }; +}; \ No newline at end of file diff --git a/packages/cli/src/plugins/resource/plugin-standard-css.js b/packages/cli/src/plugins/resource/plugin-standard-css.js index 7fd7f71b4..1c2ee89f2 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-css.js +++ b/packages/cli/src/plugins/resource/plugin-standard-css.js @@ -39,7 +39,7 @@ class StandardCssResource extends ResourceInterface { } async shouldOptimize(url) { - const isValidCss = path.extname(url) === this.extensions[0]; + const isValidCss = path.extname(url) === this.extensions[0] && this.compilation.config.optimization !== 'none'; return Promise.resolve(isValidCss); } diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js index cf34e7689..46a1cf79c 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-html.js +++ b/packages/cli/src/plugins/resource/plugin-standard-html.js @@ -75,15 +75,13 @@ const getAppTemplate = (contents, userWorkspace) => { const matchPos = appTemplateContents.lastIndexOf(matchNeedle); if (script.rawAttrs !== '') { - appTemplateContents = sliceTemplate(appTemplateContents, matchPos, matchNeedle, ` - \n + appTemplateContents = sliceTemplate(appTemplateContents, matchPos, matchNeedle, `\n \n `); } if (script.rawAttrs === '') { - appTemplateContents = sliceTemplate(appTemplateContents, matchPos, matchNeedle, ` - \n + appTemplateContents = sliceTemplate(appTemplateContents, matchPos, matchNeedle, `\n \n @@ -96,8 +94,7 @@ const getAppTemplate = (contents, userWorkspace) => { const matches = appTemplateContents.match(matchNeedle); const lastLink = matches[matches.length - 1]; - appTemplateContents = appTemplateContents.replace(lastLink, ` - ${lastLink}\n + appTemplateContents = appTemplateContents.replace(lastLink, `${lastLink}\n `); }); @@ -107,8 +104,7 @@ const getAppTemplate = (contents, userWorkspace) => { const matchPos = appTemplateContents.lastIndexOf(matchNeedle); if (style.rawAttrs === '') { - appTemplateContents = sliceTemplate(appTemplateContents, matchPos, matchNeedle, ` - \n + appTemplateContents = sliceTemplate(appTemplateContents, matchPos, matchNeedle, `\n \n 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 index e89532d6a..bd291dbe6 100644 --- 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 @@ -1,6 +1,6 @@ /* * Use Case - * Run Greenwood build command with a bad value for optimization in a custom config. + * Run Greenwood build command with a bad value for mode in a custom config. * * User Result * Should throw an error. @@ -10,7 +10,7 @@ * * User Config * { - * optimization: 'lorumipsum' + * mode: 'lorumipsum' * } * * User Workspace @@ -19,7 +19,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() { @@ -27,12 +27,12 @@ xdescribe('Build Greenwood With: ', function() { await setup.setupTestBed(__dirname); }); - describe('Custom Configuration with a bad value for Optimization', function() { - it('should throw an error that provided optimization is not valid', async function() { + 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 setup.runGreenwoodCommand('build'); } catch (err) { - expect(err).to.contain('Error: provided optimization "loremipsum" is not supported. Please use one of: strict, spa.'); + expect(err).to.contain('Error: provided mode "loremipsum" is not supported. Please use one of: ssg, mpa.'); } }); }); 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 index ee49cb180..92ceb3337 100644 --- a/packages/cli/test/cases/build.config.error-mode/greenwood.config.js +++ b/packages/cli/test/cases/build.config.error-mode/greenwood.config.js @@ -1,3 +1,3 @@ module.exports = { - optimization: 'loremipsum' + 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 new file mode 100644 index 000000000..cfb4dbbe5 --- /dev/null +++ b/packages/cli/test/cases/build.config.error-optimization/build.config.error-optimization.spec.js @@ -0,0 +1,44 @@ +/* + * 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 + * { + * optimization: 'lorumipsum' + * } + * + * User Workspace + * Greenwood default + */ +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 value for optimization', function() { + it('should throw an error that provided optimization is not valid', async function() { + try { + await setup.runGreenwoodCommand('build'); + } catch (err) { + expect(err).to.contain('Error: provided optimization "loremipsum" is not supported. Please use one of: default, none, static, inline.'); + } + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.error-optimization/greenwood.config.js b/packages/cli/test/cases/build.config.error-optimization/greenwood.config.js new file mode 100644 index 000000000..ee49cb180 --- /dev/null +++ b/packages/cli/test/cases/build.config.error-optimization/greenwood.config.js @@ -0,0 +1,3 @@ +module.exports = { + optimization: 'loremipsum' +}; \ 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.mode-mpa/build.config.mode-mpa.spec.js new file mode 100644 index 000000000..63e6439d6 --- /dev/null +++ b/packages/cli/test/cases/build.config.mode-mpa/build.config.mode-mpa.spec.js @@ -0,0 +1,149 @@ +/* + * Use Case + * Run Greenwood with mode setting in Greenwood config set to mpa. + * + * User Result + * Should generate a bare bones Greenwood build with bundle JavaScript and routes. + * + * User Command + * greenwood build + * + * User Config + * { + * mode: 'mpa' + * } + * + * User Workspace + * Greenwood default w/ nested page + * src/ + * pages/ + * about.md + * index.md + */ +const expect = require('chai').expect; +const fs = require('fs'); +const glob = require('glob-promise'); +const { JSDOM } = require('jsdom'); +const path = require('path'); +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'Custom Mode'; + let setup; + + before(async function() { + setup = new TestBed(); + + const greenwoodRouterLibs = (await glob(`${process.cwd()}/packages/cli/src/lib/router.js`)).map((lib) => { + return { + dir: 'node_modules/@greenwood/cli/src/lib/', + name: path.basename(lib) + }; + }); + + this.context = await setup.setupTestBed(__dirname, [ + ...greenwoodRouterLibs + ]); + }); + + describe(LABEL, function() { + + before(async function() { + await setup.runGreenwoodCommand('build'); + }); + + describe('MPA (Multi Page Application)', function() { + let dom; + let aboutDom; + let partials; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + aboutDom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'about/index.html')); + partials = await glob(`${this.context.publicDir}/_routes/**/*.html`); + }); + + it('should have one + + + + + +
+
+ +
+
+ + + + \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-default/src/styles/theme.css b/packages/cli/test/cases/build.config.optimization-default/src/styles/theme.css new file mode 100644 index 000000000..2f3a9deff --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-default/src/styles/theme.css @@ -0,0 +1,5 @@ +* { + margin: 0; + padding: 0; + font-family: 'Comic Sans', sans-serif; +} \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-inline/build.config-optimization-inline.spec.js b/packages/cli/test/cases/build.config.optimization-inline/build.config-optimization-inline.spec.js new file mode 100644 index 000000000..a25c748ce --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-inline/build.config-optimization-inline.spec.js @@ -0,0 +1,115 @@ +/* + * Use Case + * Run Greenwood build command with inline setting for optimization + * + * User Result + * Should generate a Greenwood build that inlines all JS and CSS + + + + + +
+
+ +
+
+ + + + \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-inline/src/styles/theme.css b/packages/cli/test/cases/build.config.optimization-inline/src/styles/theme.css new file mode 100644 index 000000000..2f3a9deff --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-inline/src/styles/theme.css @@ -0,0 +1,5 @@ +* { + margin: 0; + padding: 0; + font-family: 'Comic Sans', sans-serif; +} \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-none/build.config-optimization-none.spec.js b/packages/cli/test/cases/build.config.optimization-none/build.config-optimization-none.spec.js new file mode 100644 index 000000000..a4e957f51 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-none/build.config-optimization-none.spec.js @@ -0,0 +1,113 @@ +/* + * Use Case + * Run Greenwood build command with none setting for optimization + * + * User Result + * Should generate a Greenwood build that does not optimize any + + + + + +
+
+ +
+
+ + + + \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-none/src/styles/theme.css b/packages/cli/test/cases/build.config.optimization-none/src/styles/theme.css new file mode 100644 index 000000000..2f3a9deff --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-none/src/styles/theme.css @@ -0,0 +1,5 @@ +* { + margin: 0; + padding: 0; + font-family: 'Comic Sans', sans-serif; +} \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-static/build.config-optimization-static.spec.js b/packages/cli/test/cases/build.config.optimization-static/build.config-optimization-static.spec.js new file mode 100644 index 000000000..f9cae7bbc --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-static/build.config-optimization-static.spec.js @@ -0,0 +1,81 @@ +/* + * Use Case + * Run Greenwood build command with static setting for optimization. + * + * User Result + * Should generate a Greenwood build that strips all + + + + +
+
+ +
+
+ + + + \ No newline at end of file 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 c7186dea4..948ba8b8a 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 @@ -247,7 +247,7 @@ describe('Build Greenwood With: ', function() { describe(' with reference to node_modules/ path in the tag', function() { it('should have one tag in the tag', function() { - const linkTags = dom.window.document.querySelectorAll('head > link[href]'); + const linkTags = dom.window.document.querySelectorAll('head > link[rel="stylesheet"]'); const prismLinkTag = Array.prototype.slice.call(linkTags).filter(link => { return (/prism-tomorrow.*.css/).test(link.href); }); 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 55a8a95d5..36c116d71 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 @@ -100,7 +100,7 @@ describe('Build Greenwood With: ', function() { }); it('should output one tag in the ', async function() { - const linkTags = dom.window.document.querySelectorAll('head link'); + const linkTags = dom.window.document.querySelectorAll('head link[rel="stylesheet"]'); expect(linkTags.length).to.be.equal(1); }); @@ -174,7 +174,7 @@ describe('Build Greenwood With: ', function() { }); it('should output one tag in the ', async function() { - const linkTags = dom.window.document.querySelectorAll('head link'); + const linkTags = dom.window.document.querySelectorAll('head link[rel="stylesheet"]'); expect(linkTags.length).to.be.equal(1); }); @@ -241,7 +241,7 @@ describe('Build Greenwood With: ', function() { }); it('should output one tag in the ', async function() { - const linkTags = dom.window.document.querySelectorAll('head link'); + const linkTags = dom.window.document.querySelectorAll('head link[rel="stylesheet"]'); expect(linkTags.length).to.be.equal(1); }); diff --git a/packages/cli/test/cases/build.default.workspace-javascript-css/build.default.workspace-javascript-css.spec.js b/packages/cli/test/cases/build.default.workspace-javascript-css/build.default.workspace-javascript-css.spec.js index 2f498f17e..43686ee9a 100644 --- a/packages/cli/test/cases/build.default.workspace-javascript-css/build.default.workspace-javascript-css.spec.js +++ b/packages/cli/test/cases/build.default.workspace-javascript-css/build.default.workspace-javascript-css.spec.js @@ -124,7 +124,7 @@ describe('Build Greenwood With: ', function() { describe(' tag in the ', function() { it('should have one tag in the ', function() { - const linkTags = dom.window.document.querySelectorAll('head > link'); + const linkTags = dom.window.document.querySelectorAll('head > link[rel="stylesheet"]'); expect(linkTags.length).to.be.equal(1); }); diff --git a/packages/cli/test/cases/build.default.workspace-template-page-and-app/build.default.workspace-template-page-and-app.spec.js b/packages/cli/test/cases/build.default.workspace-template-page-and-app/build.default.workspace-template-page-and-app.spec.js index ded29a2e7..3e87eb9ad 100644 --- a/packages/cli/test/cases/build.default.workspace-template-page-and-app/build.default.workspace-template-page-and-app.spec.js +++ b/packages/cli/test/cases/build.default.workspace-template-page-and-app/build.default.workspace-template-page-and-app.spec.js @@ -73,7 +73,7 @@ describe('Build Greenwood With: ', function() { before(function() { scriptTags = dom.window.document.querySelectorAll('head > script'); - linkTags = dom.window.document.querySelectorAll('head > link'); + linkTags = dom.window.document.querySelectorAll('head > link[rel="stylesheet"'); styleTags = dom.window.document.querySelectorAll('head > style'); }); diff --git a/packages/cli/test/cases/build.default.workspace-template-page/build.default.workspace-template-page.spec.js b/packages/cli/test/cases/build.default.workspace-template-page/build.default.workspace-template-page.spec.js index 98a729b64..025aa6e27 100644 --- a/packages/cli/test/cases/build.default.workspace-template-page/build.default.workspace-template-page.spec.js +++ b/packages/cli/test/cases/build.default.workspace-template-page/build.default.workspace-template-page.spec.js @@ -60,7 +60,7 @@ describe('Build Greenwood With: ', function() { before(function() { scriptTags = dom.window.document.querySelectorAll('head > script'); - linkTags = dom.window.document.querySelectorAll('head > link'); + linkTags = dom.window.document.querySelectorAll('head > link[rel="stylesheet"]'); styleTags = dom.window.document.querySelectorAll('head > style'); }); diff --git a/packages/plugin-graphql/src/index.js b/packages/plugin-graphql/src/index.js index fd93d0955..ad8412f6c 100644 --- a/packages/plugin-graphql/src/index.js +++ b/packages/plugin-graphql/src/index.js @@ -70,7 +70,7 @@ class GraphQLResource extends ResourceInterface { - + `); resolve(body); diff --git a/packages/plugin-graphql/src/queries/config.gql b/packages/plugin-graphql/src/queries/config.gql index 1dcc9ac4e..11d119399 100644 --- a/packages/plugin-graphql/src/queries/config.gql +++ b/packages/plugin-graphql/src/queries/config.gql @@ -11,6 +11,7 @@ query { value, href }, + mode, optimization, title, workspace diff --git a/packages/plugin-graphql/src/schema/config.js b/packages/plugin-graphql/src/schema/config.js index f02ae577c..65cfaa2e8 100644 --- a/packages/plugin-graphql/src/schema/config.js +++ b/packages/plugin-graphql/src/schema/config.js @@ -22,6 +22,7 @@ const configTypeDefs = gql` type Config { devServer: DevServer, meta: [Meta], + mode: String, optimization: String, title: String, workspace: String diff --git a/packages/plugin-graphql/test/unit/schema/config.spec.js b/packages/plugin-graphql/test/unit/schema/config.spec.js index 12d268e37..811c67766 100644 --- a/packages/plugin-graphql/test/unit/schema/config.spec.js +++ b/packages/plugin-graphql/test/unit/schema/config.spec.js @@ -39,7 +39,15 @@ describe('Unit Test: Data', function() { describe('Mode', function() { - it('should have a default optimization setting of spa', function() { + it('should have a default mode setting of mode', function() { + expect(config.mode).to.equal(MOCK_CONFIG.config.mode); + }); + + }); + + describe('Optimization', function() { + + it('should have a default optimization setting of default', function() { expect(config.optimization).to.equal(MOCK_CONFIG.config.optimization); }); diff --git a/www/pages/docs/configuration.md b/www/pages/docs/configuration.md index 25c4af377..2cbf66bb0 100644 --- a/www/pages/docs/configuration.md +++ b/www/pages/docs/configuration.md @@ -9,16 +9,23 @@ linkheadings: 3 ## Configuration These are all the supported configuration options in **Greenwood**, which you can define in a _greenwood.config.js_ file in your project's root directory. -A **greenwood.config.js** file reflecting default values would be: +The below is a _greenwood.config.js_ file reflecting default values: ```js module.exports = { - workspace: 'src', // path.join(process.cwd(), 'src') devServer: { port: 1984, host: 'localhost' }, + markdown: { + plugins: [], + settings: {} + }, + meta: [], + mode: 'ssg', + optimization: 'default', + plugins: [], title: 'My App', - meta: [] + workspace: 'src' // assumes process.cwd() }; ``` @@ -91,22 +98,44 @@ 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 | +| ------ | ----------- | --------- | +|`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. | +|`ssg` | (_Default_) Generates a pre-rendered statically generated website from [pages and templates](/docs/layouts/)at build time. | Blog, portfolio, anything really! | + +#### Example +```js +module.exports = { + mode: 'mpa' +} +``` + +> _`spa` (Single Page Application) mode coming soon!_ + + ### Optimization -> ⛔ [_**Coming Soon!**_](https://github.com/ProjectEvergreen/greenwood/issues/354) - + +> _These settings are currently expiremental, and more fine grained control and intelligent based defaults will be coming soon!_ ### Title A default `` element for all pages can be configured with the `title` option. diff --git a/www/pages/docs/data.md b/www/pages/docs/data.md index 85425ce91..2e63b433f 100644 --- a/www/pages/docs/data.md +++ b/www/pages/docs/data.md @@ -236,6 +236,7 @@ query { value, href }, + mode, optimization, title, workspace @@ -263,7 +264,7 @@ async connectedCallback() { ``` ###### Response -This will return an object of youf _greenwood.config.js_ as an object. Example: +This will return an object of your _greenwood.config.js_ as an object. Example: ```javascript { devServer: { diff --git a/www/styles/page.css b/www/styles/page.css index 657a3bb90..c17a0c4a6 100644 --- a/www/styles/page.css +++ b/www/styles/page.css @@ -99,8 +99,13 @@ table { margin-bottom: 20px; } -table th:first-child { - min-width: 150px; +table td:first-child{ + min-width: 100px; + white-space: nowrap; +} + +table td:first-child{ + padding: 0 5px; } table th { diff --git a/www/styles/theme.css b/www/styles/theme.css index f0b63ac91..b7059d875 100644 --- a/www/styles/theme.css +++ b/www/styles/theme.css @@ -15,20 +15,6 @@ body { line-height:1.4; } -table { - background-color: #334a5a; - border-radius: 5px; - padding: 10px!important; - - & tr { - padding: 10px; - } - - & td, & a, & th { - color: #efefef!important; - } -} - img { width: 100%; }