diff --git a/README.md b/README.md index d308f247b..049568447 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A modern and performant static site generator supporting Web Component based dev ## Getting Started By default, Greenwood will generate a site for you in _public/_. ```shell -$ greenwood +$ greenwood build ``` Fun! But naturally you'll want to make your own pages. So create a folder called _src/pages/_ and create a page called _index.md_. @@ -137,6 +137,40 @@ This is an example page built by Greenwood. Make your own in src/pages! ## API Here are some of the features and capabiliites of Greenwood. +### Configure + +Custom greenwood configurations can be added to a `greenwood.config.js` file in your root directory. For example, you may want to change the `src` folder to something else such as `www`. By default, you can use a path relative to the current working directory. You can also use an absolute path. + +```js +module.exports = { + workspace: 'www' +}; + +``` + +#### PublicPath + +If you're hosting at yourdomain.com/mysite/ as the root to your site, you can change the public path by adding it within a `greenwood.config.js`: + +```js +module.exports = { + publicPath: '/mysite/', +}; +``` + +#### Dev Server + +You can adjust your dev server host and port, if you prefer to use something other than the default by adding it with a `greenwood.config.js`. The host url is automatically prepended with `http://` by default. + +```js +module.exports = { + devServer: { + port: 1984, + host: 'localhost' + } +}; +``` + ### Global CSS / Assets > TODO > https://github.com/ProjectEvergreen/greenwood/issues/7 diff --git a/packages/cli/config/webpack.config.common.js b/packages/cli/config/webpack.config.common.js index 23a413647..c713deecb 100644 --- a/packages/cli/config/webpack.config.common.js +++ b/packages/cli/config/webpack.config.common.js @@ -25,7 +25,7 @@ const mapUserWorkspaceDirectory = (userPath) => { ); }; -module.exports = (context, graph) => { +module.exports = (config, context, graph) => { // 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); @@ -39,7 +39,7 @@ module.exports = (context, graph) => { output: { path: context.publicDir, filename: '[name].[hash].bundle.js', - publicPath: '/' + publicPath: config.publicPath }, module: { diff --git a/packages/cli/config/webpack.config.develop.js b/packages/cli/config/webpack.config.develop.js index 31151c93e..833585fea 100644 --- a/packages/cli/config/webpack.config.develop.js +++ b/packages/cli/config/webpack.config.develop.js @@ -6,8 +6,6 @@ const generateCompilation = require('../lib/compile'); const webpackMerge = require('webpack-merge'); const commonConfig = require(path.join(__dirname, '..', './config/webpack.config.common.js')); -const host = 'localhost'; -const port = 1981; let isRebuilding = false; const rebuild = async() => { @@ -24,16 +22,17 @@ const rebuild = async() => { } }; -module.exports = ({ context, graph }) => { - const configWithContext = commonConfig(context, graph); - const publicPath = configWithContext.output.publicPath; +module.exports = ({ config, context, graph }) => { + const configWithContext = commonConfig(config, context, graph); + const { devServer, publicPath } = config; + const { host, port } = devServer; return webpackMerge(configWithContext, { mode: 'development', entry: [ - `webpack-dev-server/client?http://${host}:${port}`, + `webpack-dev-server/client?${host}:${port}`, path.join(context.scratchDir, 'app', 'app.js') ], @@ -50,7 +49,7 @@ module.exports = ({ context, graph }) => { new FilewatcherPlugin({ watchFileRegex: [`/${context.userWorkspace}/`], onReadyCallback: () => { - console.log(`Now serving Development Server available at http://${host}:${port}`); + console.log(`Now serving Development Server available at ${host}:${port}`); }, // eslint-disable-next-line no-unused-vars onChangeCallback: async (path) => { diff --git a/packages/cli/config/webpack.config.prod.js b/packages/cli/config/webpack.config.prod.js index 7aebb5391..cb065dea8 100644 --- a/packages/cli/config/webpack.config.prod.js +++ b/packages/cli/config/webpack.config.prod.js @@ -3,8 +3,8 @@ const path = require('path'); const webpackMerge = require('webpack-merge'); const commonConfig = require(path.join(__dirname, '..', './config/webpack.config.common.js')); -module.exports = ({ context, graph }) => { - const configWithContext = commonConfig(context, graph); +module.exports = ({ config, context, graph }) => { + const configWithContext = commonConfig(config, context, graph); return webpackMerge(configWithContext, { diff --git a/packages/cli/index.js b/packages/cli/index.js index 8bc41a585..0373786ce 100644 --- a/packages/cli/index.js +++ b/packages/cli/index.js @@ -40,11 +40,10 @@ if (program.parse.length === 0) { const run = async() => { process.env.NODE_ENV = MODE === 'develop' ? 'development' : 'production'; - - const compilation = await generateCompilation(); - + try { - + const compilation = await generateCompilation(); + switch (MODE) { case 'build': @@ -72,7 +71,7 @@ const run = async() => { } process.exit(0); // eslint-disable-line no-process-exit } catch (err) { - console.error(err); + console.error(err.red); process.exit(1); // eslint-disable-line no-process-exit } }; diff --git a/packages/cli/lib/compile.js b/packages/cli/lib/compile.js index 1226bc2f2..31c949303 100644 --- a/packages/cli/lib/compile.js +++ b/packages/cli/lib/compile.js @@ -1,29 +1,30 @@ require('colors'); +const initConfig = require('./config'); const initContext = require('./init'); const generateGraph = require('./graph'); const generateScaffolding = require('./scaffold'); -// TODO would like to move graph and scaffold to the top more maybe? module.exports = generateCompilation = () => { return new Promise(async (resolve, reject) => { try { let compilation = { graph: [], - context: {} + context: {}, + config: {} }; + // read from defaults/config file + console.log('Reading project config'); + compilation.config = await initConfig(); + // determine whether to use default template or user detected workspace console.log('Initializing project workspace contexts'); - const context = await initContext(compilation); - - compilation.context = context; + compilation.context = await initContext(compilation); // generate a graph of all pages / components to build console.log('Generating graph of workspace files...'); - const graph = await generateGraph(compilation); - - compilation.graph = graph; + compilation.graph = 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 new file mode 100644 index 000000000..2d2442def --- /dev/null +++ b/packages/cli/lib/config.js @@ -0,0 +1,75 @@ +const fs = require('fs'); +const path = require('path'); +const url = require('url'); + +let config = { + workspace: path.join(process.cwd(), 'src'), + devServer: { + port: 1984, + host: 'http://localhost' + }, + publicPath: '/', + // TODO add global meta data see issue #5 + // https://github.com/ProjectEvergreen/greenwood/issues/5 + meta: { + title: '', + description: '', + author: '', + domain: '' + } +}; + +module.exports = readAndMergeConfig = async() => { + return new Promise((resolve, reject) => { + try { + if (fs.existsSync(path.join(process.cwd(), 'greenwood.config.js'))) { + const userCfgFile = require(path.join(process.cwd(), 'greenwood.config.js')); + + // prepend userCfgFile devServer.host with http by default + userCfgFile.devServer.host = 'http://' + userCfgFile.devServer.host; + + const { workspace, devServer, publicPath } = userCfgFile; + + if (workspace) { + if (typeof workspace !== 'string') { + reject('Error: greenwood.config.js workspace path must be a string'); + } + + if (!path.isAbsolute(workspace)) { + // prepend relative path with current directory + userCfgFile.workspace = path.join(process.cwd(), workspace); + } + + if (!fs.existsSync(workspace)) { + reject('Error: greenwood.config.js workspace doesn\'t exist! \n' + + 'common issues to check might be: \n' + + '- typo in your workspace directory name, or in greenwood.config.js \n' + + '- if using relative paths, make sure your workspace is in the same cwd as _greenwood.config.js_ \n' + + '- consider using an absolute path, e.g. path.join(__dirname, \'my\', \'custom\', \'path\') // <__dirname>/my/custom/path/ '); + } + } + + if (publicPath && typeof publicPath !== 'string') { + reject('Error: greenwood.config.js publicPath must be a string'); + } + + if (devServer && Object.keys(devServer).length > 0) { + + if (url.parse(devServer.host).hostname === null) { + reject('Error: greenwood.config.js devServer host type must be a valid url'); + } + + if (!Number.isInteger(devServer.port)) { + reject('Error: greenwood.config.js devServer port must be an integer'); + } + } + + config = { ...config, ...userCfgFile }; + } + resolve(config); + + } catch (err) { + reject(err); + } + }); +}; \ No newline at end of file diff --git a/packages/cli/lib/graph.js b/packages/cli/lib/graph.js index 1aa6cd605..0bf05ff9c 100644 --- a/packages/cli/lib/graph.js +++ b/packages/cli/lib/graph.js @@ -28,8 +28,6 @@ const createGraphFromPages = async (pagesDir) => { let { label, template } = attributes; let mdFile = ''; - // Limitation Note: label must be included in md file front-matter as wc-md-loader requires it - // if template not set, use default template = template || 'page'; diff --git a/packages/cli/lib/init.js b/packages/cli/lib/init.js index 5d89bd4cb..d6fa29040 100644 --- a/packages/cli/lib/init.js +++ b/packages/cli/lib/init.js @@ -4,25 +4,23 @@ const defaultTemplatesDir = path.join(__dirname, '../templates/'); const scratchDir = path.join(process.cwd(), './.greenwood/'); const publicDir = path.join(process.cwd(), './public'); -module.exports = initContexts = async() => { +module.exports = initContexts = async({ config }) => { return new Promise((resolve, reject) => { try { - // TODO: replace user workspace src path based on config see issue #40 - // https://github.com/ProjectEvergreen/greenwood/issues/40 - const userWorkspace = path.join(process.cwd(), 'src'); + const userWorkspace = path.join(config.workspace); const userPagesDir = path.join(userWorkspace, 'pages/'); const userTemplatesDir = path.join(userWorkspace, 'templates/'); const userAppTemplate = path.join(userTemplatesDir, 'app-template.js'); const userPageTemplate = path.join(userTemplatesDir, 'page-template.js'); - + const userHasWorkspace = fs.existsSync(userWorkspace); const userHasWorkspacePages = fs.existsSync(userPagesDir); const userHasWorkspaceTemplates = fs.existsSync(userTemplatesDir); const userHasWorkspacePageTemplate = fs.existsSync(userPageTemplate); const userHasWorkspaceAppTemplate = fs.existsSync(userAppTemplate); - + let context = { scratchDir, publicDir, @@ -38,7 +36,7 @@ module.exports = initContexts = async() => { indexPageTemplate: 'index.html', notFoundPageTemplate: '404.html' }; - + if (!fs.existsSync(scratchDir)) { fs.mkdirSync(scratchDir); } diff --git a/packages/cli/templates/hello.md b/packages/cli/templates/hello.md index bc93e10f2..d0c315dca 100644 --- a/packages/cli/templates/hello.md +++ b/packages/cli/templates/hello.md @@ -1,6 +1,5 @@ --- label: 'hello' -template: 'page' --- ### Hello World diff --git a/packages/cli/templates/index.md b/packages/cli/templates/index.md index f0176789c..19300efd8 100644 --- a/packages/cli/templates/index.md +++ b/packages/cli/templates/index.md @@ -1,6 +1,5 @@ --- label: 'index' -template: 'page' --- ### Greenwood diff --git a/test/cli.spec.js b/test/cli.spec.js index db41a1df4..c8bf3b395 100644 --- a/test/cli.spec.js +++ b/test/cli.spec.js @@ -270,6 +270,7 @@ describe('building greenwood with user workspace that doesn\'t contain app templ await fs.remove(CONTEXT.scratchDir); }); }); + describe('building greenwood with user workspace that doesn\'t contain page template', () => { before(async() => { setup = new TestSetup(); @@ -360,4 +361,68 @@ describe('building greenwood with user workspace that doesn\'t contain page temp await fs.remove(CONTEXT.publicDir); await fs.remove(CONTEXT.scratchDir); }); + +}); + +describe('building greenwood with user provided config file', () => { + before(async () => { + setup = new TestSetup(); + CONTEXT = await setup.init(); + + // read user config file and copy it to app root + const userCfgFile = require(CONTEXT.userCfgPath); + + await fs.copy(CONTEXT.userCfgPath, CONTEXT.userCfgRootPath); + + // set new user source based on config file + CONTEXT.userSrc = path.join(__dirname, '..', userCfgFile.workspace); + + // copy test app to configured source + await fs.copy(CONTEXT.testApp, CONTEXT.userSrc); + await setup.run(['./packages/cli/index.js', 'build']); + + blogPageHtmlPath = path.join(CONTEXT.publicDir, 'blog', '20190326', 'index.html'); + }); + + it('should output one JS bundle', async() => { + expect(await glob.promise(path.join(CONTEXT.publicDir, './**/index.*.bundle.js'))).to.have.lengthOf(1); + }); + + it('should contain a nested blog page directory', () => { + expect(fs.existsSync(path.join(CONTEXT.publicDir, 'blog', '20190326'))).to.be.true; + }); + + describe('nested generated blog page directory', () => { + const defaultHeading = 'Blog Page'; + const defaultBody = 'This is the blog page built by Greenwood.'; + let dom; + + beforeEach(async() => { + dom = await JSDOM.fromFile(blogPageHtmlPath); + }); + + it('should contain a nested blog page with an index html file', () => { + expect(fs.existsSync(blogPageHtmlPath)).to.be.true; + }); + + it('should have the expected heading text within the blog page in the blog directory', async() => { + const heading = dom.window.document.querySelector('h3').textContent; + + expect(heading).to.equal(defaultHeading); + }); + + it('should have the expected paragraph text within the blog page in the blog directory', async() => { + let paragraph = dom.window.document.querySelector('p').textContent; + + expect(paragraph).to.equal(defaultBody); + }); + }); + + after(async() => { + await fs.remove(CONTEXT.userSrc); + await fs.remove(CONTEXT.userCfgRootPath); + await fs.remove(CONTEXT.publicDir); + await fs.remove(CONTEXT.scratchDir); + }); + }); \ No newline at end of file diff --git a/test/fixtures/mock-app/greenwood.config.js b/test/fixtures/mock-app/greenwood.config.js new file mode 100644 index 000000000..931555e03 --- /dev/null +++ b/test/fixtures/mock-app/greenwood.config.js @@ -0,0 +1,9 @@ +module.exports = { + workspace: 'src2', + publicPath: '/', + devServer: { + port: 1984, + host: 'localhost' + }, + meta: {} +}; \ No newline at end of file diff --git a/test/fixtures/mock-app/src/templates/404.html b/test/fixtures/mock-app/src/templates/404.html index 7a9f2dbfc..643d52b5a 100644 --- a/test/fixtures/mock-app/src/templates/404.html +++ b/test/fixtures/mock-app/src/templates/404.html @@ -6,7 +6,8 @@ Greenwood App - + <%= htmlWebpackPlugin.options.spaIndexFallbackScript %> + diff --git a/test/fixtures/mock-app/src/templates/index.html b/test/fixtures/mock-app/src/templates/index.html index dc197441d..394a8b792 100644 --- a/test/fixtures/mock-app/src/templates/index.html +++ b/test/fixtures/mock-app/src/templates/index.html @@ -32,6 +32,8 @@ + <%= htmlWebpackPlugin.options.spaIndexFallbackScript %> +