diff --git a/packages/cli/config/webpack.config.common.js b/packages/cli/config/webpack.config.common.js
index 03017da18..e0f1d2303 100644
--- a/packages/cli/config/webpack.config.common.js
+++ b/packages/cli/config/webpack.config.common.js
@@ -13,11 +13,14 @@ const mapUserWorkspaceDirectory = (userPath) => {
return new webpack.NormalModuleReplacementPlugin(
new RegExp(`${directory}`),
(resource) => {
- resource.request = resource.request.replace(new RegExp(`\.\.\/${directory}`), userPath);
+ // workaround to ignore cli/templates default imports when rewriting
+ if (!new RegExp('\/cli\/templates').test(resource.request)) {
+ resource.request = resource.request.replace(new RegExp(`\.\.\/${directory}`), userPath);
+ }
// remove any additional nests, after replacement with absolute path of user workspace + directory
const additionalNestedPathIndex = resource.request.lastIndexOf('..');
-
+
if (additionalNestedPathIndex > -1) {
resource.request = resource.request.substring(additionalNestedPathIndex + 2, resource.request.length);
}
@@ -25,7 +28,7 @@ const mapUserWorkspaceDirectory = (userPath) => {
);
};
-module.exports = (config, context) => {
+module.exports = ({ config, context }) => {
// dynamically map all the user's workspace directories for resolution by webpack
// this essentially helps us keep watch over changes from the user, and greenwood's build pipeline
const mappedUserDirectoriesForWebpack = getUserWorkspaceDirectories(context.userWorkspace).map(mapUserWorkspaceDirectory);
diff --git a/packages/cli/config/webpack.config.develop.js b/packages/cli/config/webpack.config.develop.js
index 833585fea..50b9ad8c9 100644
--- a/packages/cli/config/webpack.config.develop.js
+++ b/packages/cli/config/webpack.config.develop.js
@@ -23,7 +23,7 @@ const rebuild = async() => {
};
module.exports = ({ config, context, graph }) => {
- const configWithContext = commonConfig(config, context, graph);
+ const configWithContext = commonConfig({ config, context, graph });
const { devServer, publicPath } = config;
const { host, port } = devServer;
diff --git a/packages/cli/config/webpack.config.prod.js b/packages/cli/config/webpack.config.prod.js
index cb065dea8..971664fb6 100644
--- a/packages/cli/config/webpack.config.prod.js
+++ b/packages/cli/config/webpack.config.prod.js
@@ -4,7 +4,7 @@ const webpackMerge = require('webpack-merge');
const commonConfig = require(path.join(__dirname, '..', './config/webpack.config.common.js'));
module.exports = ({ config, context, graph }) => {
- const configWithContext = commonConfig(config, context, graph);
+ const configWithContext = commonConfig({ config, context, graph });
return webpackMerge(configWithContext, {
diff --git a/packages/cli/lib/compile.js b/packages/cli/lib/compile.js
index 31c949303..d999086b8 100644
--- a/packages/cli/lib/compile.js
+++ b/packages/cli/lib/compile.js
@@ -24,7 +24,7 @@ module.exports = generateCompilation = () => {
// generate a graph of all pages / components to build
console.log('Generating graph of workspace files...');
- compilation.graph = await generateGraph(compilation);
+ compilation = await generateGraph(compilation);
// generate scaffolding
console.log('Scaffolding out project files...');
diff --git a/packages/cli/lib/config.js b/packages/cli/lib/config.js
index ea5c014a7..0a73f91b1 100644
--- a/packages/cli/lib/config.js
+++ b/packages/cli/lib/config.js
@@ -8,10 +8,13 @@ let defaultConfig = {
port: 1984,
host: 'http://localhost'
},
- publicPath: '/'
+ publicPath: '/',
+ title: 'Greenwood App',
+ meta: []
};
module.exports = readAndMergeConfig = async() => {
+ // eslint-disable-next-line complexity
return new Promise((resolve, reject) => {
try {
// deep clone of default config
@@ -19,7 +22,8 @@ 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, publicPath } = userCfgFile;
+
+ const { workspace, devServer, publicPath, title, meta } = userCfgFile;
// workspace validation
if (workspace) {
@@ -41,6 +45,13 @@ module.exports = readAndMergeConfig = async() => {
}
}
+ if (title) {
+ if (typeof title !== 'string') {
+ reject('Error: greenwood.config.js title must be a string');
+ }
+ customConfig.title = title;
+ }
+
if (publicPath) {
if (typeof publicPath !== 'string') {
reject('Error: greenwood.config.js publicPath must be a string');
@@ -50,7 +61,10 @@ module.exports = readAndMergeConfig = async() => {
}
}
- // devServer checks
+ if (meta && meta.length > 0) {
+ customConfig.meta = meta;
+ }
+
if (devServer && Object.keys(devServer).length > 0) {
if (devServer.host) {
@@ -72,6 +86,7 @@ module.exports = readAndMergeConfig = async() => {
// console.log(`custom port provided => ${customConfig.devServer.port}`);
}
}
+
}
}
diff --git a/packages/cli/lib/graph.js b/packages/cli/lib/graph.js
index d0e1c0b8c..6262b7c67 100644
--- a/packages/cli/lib/graph.js
+++ b/packages/cli/lib/graph.js
@@ -5,7 +5,7 @@ const fm = require('front-matter');
const path = require('path');
const util = require('util');
-const createGraphFromPages = async (pagesDir) => {
+const createGraphFromPages = async (pagesDir, config) => {
let pages = [];
const readdir = util.promisify(fs.readdir);
const readFile = util.promisify(fs.readFile);
@@ -26,7 +26,8 @@ const createGraphFromPages = async (pagesDir) => {
if (isMdFile && !stats.isDirectory()) {
const fileContents = await readFile(filePath, 'utf8');
const { attributes } = fm(fileContents);
- let { label, template } = attributes;
+ let { label, template, title } = attributes;
+ let { meta } = config;
let mdFile = '';
// if template not set, use default
@@ -54,8 +55,8 @@ const createGraphFromPages = async (pagesDir) => {
// set route to the nested pages path and file name(without extension)
route = completeNestedPath + route;
- mdFile = `./${completeNestedPath}${fileRoute}.md`;
- relativeExpectedPath = `'../${completeNestedPath}/${fileName}/${fileName}.js'`;
+ mdFile = `.${completeNestedPath}${fileRoute}.md`;
+ relativeExpectedPath = `'..${completeNestedPath}/${fileName}/${fileName}.js'`;
} else {
mdFile = `.${fileRoute}.md`;
relativeExpectedPath = `'../${fileName}/${fileName}.js'`;
@@ -64,6 +65,11 @@ const createGraphFromPages = async (pagesDir) => {
// generate a random element name
label = label || generateLabelHash(filePath);
+ // set
element text, override with markdown title
+ title = title || config.title;
+
+ // TODO: Allow for other, per page, dynamic, meta data, merge meta array
+
/*
* Variable Definitions
*----------------------
@@ -75,10 +81,11 @@ const createGraphFromPages = async (pagesDir) => {
* fileName: file name without extension/path, so that it can be copied to scratch dir with same name
* relativeExpectedPath: relative import path for generated component within a list.js file to later be
* imported into app.js root component
- * elementLabel: the element name for the generated md page e.g.
+ * title: the head text
+ * meta: og graph meta array of objects { property/name, content }
*/
- pages.push({ mdFile, label, route, template, filePath, fileName, relativeExpectedPath });
+ pages.push({ mdFile, label, route, template, filePath, fileName, relativeExpectedPath, title, meta });
}
if (stats.isDirectory()) {
await walkDirectory(filePath);
@@ -116,9 +123,11 @@ module.exports = generateGraph = async (compilation) => {
return new Promise(async (resolve, reject) => {
try {
- const graph = await createGraphFromPages(compilation.context.pagesDir);
+ const { context, config } = compilation;
+
+ compilation.graph = await createGraphFromPages(context.pagesDir, config);
- resolve(graph);
+ resolve(compilation);
} catch (err) {
reject(err);
}
diff --git a/packages/cli/lib/init.js b/packages/cli/lib/init.js
index 411f99c4f..eb86abaa9 100644
--- a/packages/cli/lib/init.js
+++ b/packages/cli/lib/init.js
@@ -3,6 +3,7 @@ const path = require('path');
const defaultTemplatesDir = path.join(__dirname, '../templates/');
const scratchDir = path.join(process.cwd(), './.greenwood/');
const publicDir = path.join(process.cwd(), './public');
+const metaComponent = path.join(__dirname, '..', 'templates', './components/meta');
module.exports = initContexts = async({ config }) => {
@@ -44,7 +45,8 @@ module.exports = initContexts = async({ config }) => {
? path.join(userTemplatesDir, notFoundPageTemplate)
: path.join(defaultTemplatesDir, notFoundPageTemplate),
indexPageTemplate,
- notFoundPageTemplate
+ notFoundPageTemplate,
+ metaComponent
};
if (!fs.existsSync(scratchDir)) {
diff --git a/packages/cli/lib/scaffold.js b/packages/cli/lib/scaffold.js
index 5f35218e1..896a05a94 100644
--- a/packages/cli/lib/scaffold.js
+++ b/packages/cli/lib/scaffold.js
@@ -23,6 +23,29 @@ const writePageComponentsFromTemplate = async (compilation) => {
});
};
+ const loadPageMeta = async (file, result, { metaComponent }) => {
+ return new Promise((resolve, reject) => {
+ try {
+ const { title, meta, route } = file;
+ const metadata = {
+ title,
+ meta
+ };
+
+ metadata.meta.push({ property: 'og:title', content: title });
+ metadata.meta.push({ property: 'og:url', content: route });
+
+ result = result.replace(/METAIMPORT/, `import '${metaComponent}'`);
+ result = result.replace(/METADATA/, `const metadata = ${JSON.stringify(metadata)}`);
+ result = result.replace(/METAELEMENT/, '');
+
+ resolve(result);
+ } catch (err) {
+ reject(err);
+ }
+ });
+ };
+
return Promise.all(compilation.graph.map(file => {
const context = compilation.context;
@@ -30,6 +53,7 @@ const writePageComponentsFromTemplate = async (compilation) => {
try {
let result = await createPageComponent(file, context);
+ result = await loadPageMeta(file, result, context);
let relPageDir = file.filePath.substring(context.pagesDir.length, file.filePath.length);
const pathLastBackslash = relPageDir.lastIndexOf('/');
diff --git a/packages/cli/templates/components/meta.js b/packages/cli/templates/components/meta.js
new file mode 100644
index 000000000..d9885ce66
--- /dev/null
+++ b/packages/cli/templates/components/meta.js
@@ -0,0 +1,72 @@
+import { html, LitElement } from 'lit-element';
+
+/*
+* Take an attributes object with an array of meta objects, add them to an element and replace/add the element to DOM
+* {
+* title: 'my title',
+* meta: [
+* { property: 'og:site', content: 'greenwood' },
+* { name: 'twitter:site', content: '@PrjEvergreen ' }
+* ]
+* }
+*/
+
+class meta extends LitElement {
+
+ static get properties() {
+ return {
+ attributes: {
+ type: Object
+ }
+ };
+ }
+
+ firstUpdated() {
+ let header = document.head;
+ let meta;
+
+ if (this.attributes) {
+ this.attributes.meta.map(attr => {
+ meta = document.createElement('meta');
+
+ const metaPropertyOrName = Object.keys(attr)[0];
+ const metaPropValue = Object.values(attr)[0];
+ let metaContentVal = Object.values(attr)[1];
+
+ // insert origin domain into url
+ if (metaPropValue === 'og:url') {
+ metaContentVal = window.location.origin + metaContentVal;
+ }
+
+ meta.setAttribute(metaPropertyOrName, metaPropValue);
+ meta.setAttribute('content', metaContentVal);
+
+ const oldmeta = header.querySelector(`[${metaPropertyOrName}="${metaPropValue}"]`);
+
+ // rehydration
+ if (oldmeta) {
+ header.replaceChild(meta, oldmeta);
+ } else {
+ header.appendChild(meta);
+ }
+ });
+ let title = document.createElement('title');
+
+ title.innerText = this.attributes.title;
+ const oldTitle = document.head.querySelector('title');
+
+ header.replaceChild(title, oldTitle);
+ }
+
+ }
+
+ render() {
+ return html`
+
+
+
+ `;
+ }
+}
+
+customElements.define('eve-meta', meta);
\ No newline at end of file
diff --git a/packages/cli/templates/page-template.js b/packages/cli/templates/page-template.js
index 16ff0fe5a..4355f1987 100644
--- a/packages/cli/templates/page-template.js
+++ b/packages/cli/templates/page-template.js
@@ -1,9 +1,12 @@
import { html, LitElement } from 'lit-element';
MDIMPORT;
+METAIMPORT;
+METADATA;
class PageTemplate extends LitElement {
render() {
return html`
+ METAELEMENT
diff --git a/test/cli/cases/build.config.default/build.config.default.spec.js b/test/cli/cases/build.config.default/build.config.default.spec.js
index 0c4fa90a3..4f056bd51 100644
--- a/test/cli/cases/build.config.default/build.config.default.spec.js
+++ b/test/cli/cases/build.config.default/build.config.default.spec.js
@@ -30,7 +30,7 @@ describe('Build Greenwood With: ', async function() {
before(async function() {
await setup.runGreenwoodCommand('build');
});
- runSmokeTest(['public', 'index', 'not-found', 'hello'], LABEL);
+ runSmokeTest(['public', 'index', 'not-found', 'hello', 'meta'], LABEL);
});
after(function() {
diff --git a/test/cli/cases/build.config.error-title/build.config.error-title.spec.js b/test/cli/cases/build.config.error-title/build.config.error-title.spec.js
new file mode 100644
index 000000000..9ac5a145f
--- /dev/null
+++ b/test/cli/cases/build.config.error-title/build.config.error-title.spec.js
@@ -0,0 +1,44 @@
+/*
+ * Use Case
+ * Run Greenwood build command with a bad value for title in a custom config.
+ *
+ * User Result
+ * Should throw an error.
+ *
+ * User Command
+ * greenwood build
+ *
+ * User Config
+ * {
+ * title: {}
+ * }
+ *
+ * User Workspace
+ * Greenwood default
+ */
+const expect = require('chai').expect;
+const TestBed = require('../../test-bed');
+
+describe('Build Greenwood With: ', () => {
+ let setup;
+
+ before(async () => {
+ setup = new TestBed();
+ setup.setupTestBed(__dirname);
+ });
+
+ describe('Custom Configuration with a bad value for Title', () => {
+ it('should throw an error that title must be a string', async () => {
+ try {
+ await setup.runGreenwoodCommand('build');
+ } catch (err) {
+ expect(err).to.contain('greenwood.config.js title must be a string');
+ }
+ });
+ });
+
+ after(function() {
+ setup.teardownTestBed();
+ });
+
+});
\ No newline at end of file
diff --git a/test/cli/cases/build.config.error-title/greenwood.config.js b/test/cli/cases/build.config.error-title/greenwood.config.js
new file mode 100644
index 000000000..e76c49235
--- /dev/null
+++ b/test/cli/cases/build.config.error-title/greenwood.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ title: {}
+};
\ No newline at end of file
diff --git a/test/cli/cases/build.config.meta/build.config.meta.spec.js b/test/cli/cases/build.config.meta/build.config.meta.spec.js
new file mode 100644
index 000000000..ba3a341b0
--- /dev/null
+++ b/test/cli/cases/build.config.meta/build.config.meta.spec.js
@@ -0,0 +1,76 @@
+/*
+ * Use Case
+ * Run Greenwood with meta config object and default workspace.
+ *
+ * User Result
+ * Should generate a bare bones Greenwood build. (same as build.default.spec.js) with meta data
+ *
+ * User Command
+ * greenwood build
+ *
+ * User Config
+ * {
+ * title: 'My Custom Greenwood App',
+ * meta: [
+ * { property: 'og:site', content: 'greenwood' },
+ * { name: 'twitter:site', content: '@PrjEvergreen' }
+ * ]
+ * }
+ *
+ * User Workspace
+ * Greenwood default
+ * src/
+ * pages/
+ * index.md
+ * hello.md
+ */
+const fs = require('fs');
+const { JSDOM } = require('jsdom');
+const path = require('path');
+const expect = require('chai').expect;
+const runSmokeTest = require('../../smoke-test');
+const TestBed = require('../../test-bed');
+
+describe('Build Greenwood With: ', async function() {
+ const LABEL = 'Custom Meta Configuration and Default Workspace';
+ let setup;
+
+ before(async function() {
+ setup = new TestBed();
+ this.context = setup.setupTestBed(__dirname);
+ });
+
+ describe(LABEL, function() {
+ before(async function() {
+ await setup.runGreenwoodCommand('build');
+ });
+ runSmokeTest(['public', 'index', 'not-found', 'hello', 'meta'], LABEL);
+
+ describe('Custom Meta Index Page', function() {
+ let dom;
+
+ beforeEach(async function() {
+ dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html'));
+ });
+
+ it('should output an index.html file within the default hello page directory', function() {
+ expect(fs.existsSync(path.join(this.context.publicDir, './index.html'))).to.be.true;
+ });
+
+ it('should have our custom config tag with og:site property in the ', function() {
+ const metaElement = dom.window.document.querySelector('head meta[property="og:site"]');
+
+ expect(metaElement.getAttribute('content')).to.be.equal('greenwood');
+ });
+
+ it('should have our custom config tag with twitter:site name in the ', function() {
+ const metaElement = dom.window.document.querySelector('head meta[name="twitter:site"]');
+
+ expect(metaElement.getAttribute('content')).to.be.equal('@PrjEvergreen');
+ });
+ });
+ });
+ after(function() {
+ setup.teardownTestBed();
+ });
+});
\ No newline at end of file
diff --git a/test/cli/cases/build.config.meta/greenwood.config.js b/test/cli/cases/build.config.meta/greenwood.config.js
new file mode 100644
index 000000000..9fa0da1bb
--- /dev/null
+++ b/test/cli/cases/build.config.meta/greenwood.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ meta: [
+ { property: 'og:site', content: 'greenwood' },
+ { name: 'twitter:site', content: '@PrjEvergreen' }
+ ]
+};
\ No newline at end of file
diff --git a/test/cli/cases/build.config.meta/src/pages/hello.md b/test/cli/cases/build.config.meta/src/pages/hello.md
new file mode 100644
index 000000000..fb8cf32d5
--- /dev/null
+++ b/test/cli/cases/build.config.meta/src/pages/hello.md
@@ -0,0 +1,7 @@
+---
+label: 'hello'
+title: 'Hello Page'
+---
+### Hello World
+
+This is an example page built by Greenwood. Make your own in _src/pages_!
\ No newline at end of file
diff --git a/test/cli/cases/build.config.meta/src/pages/index.md b/test/cli/cases/build.config.meta/src/pages/index.md
new file mode 100644
index 000000000..1c1a50fbb
--- /dev/null
+++ b/test/cli/cases/build.config.meta/src/pages/index.md
@@ -0,0 +1,3 @@
+### Greenwood
+
+This is the home page built by Greenwood. Make your own pages in src/pages/index.js!
\ No newline at end of file
diff --git a/test/cli/cases/build.config.title/build.config.title.spec.js b/test/cli/cases/build.config.title/build.config.title.spec.js
new file mode 100644
index 000000000..1e288f611
--- /dev/null
+++ b/test/cli/cases/build.config.title/build.config.title.spec.js
@@ -0,0 +1,120 @@
+/*
+ * Use Case
+ * Run Greenwood with string title in config and default workspace.
+ *
+ * User Result
+ * Should generate a bare bones Greenwood build. (same as build.default.spec.js) with custom title in header
+ *
+ * User Command
+ * greenwood build
+ *
+ * User Config
+ * {
+ * title: 'My Custom Greenwood App'
+ * }
+ *
+ * User Workspace
+ * Greenwood default
+ * src/
+ * pages/
+ * index.md
+ * hello.md
+ */
+const fs = require('fs');
+const { JSDOM } = require('jsdom');
+const path = require('path');
+const expect = require('chai').expect;
+const runSmokeTest = require('../../smoke-test');
+const TestBed = require('../../test-bed');
+
+describe('Build Greenwood With: ', async function() {
+ const LABEL = 'Custom Title Configuration and Default Workspace';
+ let setup;
+
+ before(async function() {
+ setup = new TestBed();
+ this.context = setup.setupTestBed(__dirname);
+ });
+
+ describe(LABEL, function() {
+ before(async function() {
+ await setup.runGreenwoodCommand('build');
+ });
+ runSmokeTest(['public', 'not-found', 'hello', 'meta'], LABEL);
+
+ describe('Custom Title', function() {
+ const indexPageTitle = 'My Custom Greenwood App';
+ const indexPageHeading = 'Greenwood';
+ const indexPageBody = 'This is the home page built by Greenwood. Make your own pages in src/pages/index.js!';
+ let dom;
+
+ beforeEach(async function() {
+ dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html'));
+ });
+
+ it('should output an index.html file within the default public directory', function() {
+ expect(fs.existsSync(path.join(this.context.publicDir, './index.html'))).to.be.true;
+ });
+
+ it('should have our custom config meta
tag in the ', function() {
+ const title = dom.window.document.querySelector('head title').textContent;
+
+ expect(title).to.be.equal(indexPageTitle);
+ });
+
+ // rest of index smoke-test because is changed for this case
+ it('should have a