diff --git a/.eslintignore b/.eslintignore index c7b12a705..579a76834 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,5 @@ **/public/** **/node_modules/** !.eslintrc.js -!.mocharc.js \ No newline at end of file +!.mocharc.js +packages/plugin-babel/test/cases/**/*main.js \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 336e46cb1..e8fb2385d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,13 +1,7 @@ -const path = require('path'); - module.exports = { - parser: 'babel-eslint', parserOptions: { ecmaVersion: 2018, - sourceType: 'module', - babelOptions: { - configFile: path.join(__dirname, './packages/cli/src/config/babel.config.js') - } + sourceType: 'module' }, env: { browser: true, diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b518c385a..e02a3fe3f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -3,6 +3,53 @@ ## Welcome! We're excited for your interest in Greenwood, and maybe even your contribution! +> _We encourage all contributors to have first read about the project's vision and motivation's on the website's [About page](https://www.greenwoodjs.io/about/). Greenwood is opinionated in the sense that is designed to support development for the web platform and deliver a first class developer experience tailored around that, so that anyone can create a modern and performant website (or webapp, if you prefer). So if that page is the "why", this page is the "how"._ + +## Technical Design Overview + +The Greenwood GitHub repository is a combination [Yarn workspace](https://classic.yarnpkg.com/en/docs/workspaces/) and [Lerna monorepo](https://github.com/lerna/lerna). The root level _package.json_ defines the workspaces and shared tooling used throughout the project, like for linting, testing, etc. + +The two main directories are: +- [_packages/_](https://github.com/ProjectEvergreen/greenwood/tree/master/packages) - Packages published to NPM under the `@greenwood/` scope +- [_www/_](https://github.com/ProjectEvergreen/greenwood/tree/master/www) - [website](https://www.greenwoodjs.io) / documentation code + + +> _This guide is mainly intended to walk through the **cli** package, it being the principal pacakge within the project supporting all other packages._ + +### CLI + +The CLI is the main entry point for Greenwood, similar to how the [front-controller pattern](https://en.wikipedia.org/wiki/Front_controller) works. When users run a command like `greenwood build`, they are effectively invoking the file _src/index.js_ within the `@greenwood/cli` package. + +At a high level, this is how a command goes through the CLI: +1. Each documented command a user can run maps to a script in the _commands/_ directory. +1. Each command can invoke any number of lifecycles from the _lifecycles/_ directory. +1. Lifecycles capture specific steps needed to build a site, serve it, generate a content dependency graph, etc. + + +### Layout +The [layout](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src) of the CLI package is as follows: + +- _index.js_ - Front controller +- _commands/_ - map to runnable userland commands +- _config/_ - Tooling configuration that Greenwood creates +- _data/_ - Custom GraphQL server and client side utilities +- _lib/_ - Customfized Local third party libraries and utility files +- _lifecycles/_ - Tasks that can be composed by commands to support the full needs of that command +- _plugins/_ - Custom defaukt plugins maintained by the CLI project +- _templates/_ - Default templates and / or pages provided by Grennwood. + +We'll focus on the most important two here: + + +### Lifecycles +Aside from the config and graph lifecycles, all lifecycles (and config files and plugins) typically expect a compilation object to be passed in. + +Lifeycles include handling: +- starting a production or development server for a compilation +- optimizing a compilation for production +- prerendering a compilation for production +- fetching external (content) data sources + ## Issues Please make sure to have the following prepared (where applicable) @@ -14,8 +61,8 @@ Please make sure to have the following prepared (where applicable) ## Pull Requests Pull requests are the best! To best help facililate contributions to the project, here are some requests: -- We generally we prefer an issue be opened first, to help faciliate general discussion outside of the code review process itself and align on the ask and any expections. However, for typos in docs and minor "chore" like tasks a PR is usually sufficient. When in doubt, open an issue. -- For bugs, please consider reviewing the issue tracker. +- We generally prefer an issue be opened first, to help faciliate general discussion outside of the code review process itself and align on the ask and any expections. However, for typos in docs and minor "chore" like tasks a PR is usually sufficient. When in doubt, open an issue. +- For bugs, please consider reviewing the issue tracker first. - For branching, we generally follow the convention `/issue--`, e.g. _bug/issue-12-fixed-bug-with-yada-yada-yada_ - To test the CI build scripts locally, run the `yarn` commands mentioned in the below section on CI. @@ -31,13 +78,13 @@ A preview is also made available within the status checks section of the PR in G ## Local Development To develop for the project, you'll want to follow these steps: -1. Have [NodeJS LTS](https://nodejs.org) installed (>= 10.x) and [Yarn](https://yarnpkg.com/) +1. Have [NodeJS LTS](https://nodejs.org) installed (>= 12.x) and [Yarn](https://yarnpkg.com/) 1. Clone the repository 1. Run `yarn install` 1. Run `yarn lerna bootstrap` -### Tasks -The Greenwood website is currently built by Greenwood itself, and all files for it are located in this repository in the _www/_ directory. In addition to unit tests, you will want to verify all changes by running the website locally. +### Scripts +The [Greenwood website](https://www.greenwoodjs.io/) is currently built by Greenwood, and all files for it are located in this repository under the [_www/_ directory](https://github.com/ProjectEvergreen/greenwood/tree/master/www) workspace. In addition to unit tests, you will want to verify any changes by running the website locally. Below are the development tasks available for working on this project: - `yarn develop` - Develop for the website locally using the dev server at `localhost:1984` in your browser. @@ -45,7 +92,7 @@ Below are the development tasks available for working on this project: - `yarn serve` - Builds the website for production and runs it on a local webserver at `localhost:8000` ### Packages -Greenwood is organized into packages as a monorepo, managed by [Lerna](https://lerna.js.org/) and [Yarn Workspaces](https://yarnpkg.com/lang/en/docs/workspaces/). You can find all of these in the _packages/_ directory. Each package will manage its own: +As mentioned above, Greenwood is organized into packages as a monorepo, managed by [Lerna](https://lerna.js.org/) and [Yarn Workspaces](https://yarnpkg.com/lang/en/docs/workspaces/). You can find all of these in the _packages/_ directory. Each package will manage its own: - Dependencies - README - Test Cases @@ -65,7 +112,7 @@ Yarn workspaces will automatically handle installing _node_modules_ in the appro ## Unit Testing -[TDD](https://en.wikipedia.org/wiki/Test-driven_development) is the recommended approach for developing for Greenwood and for the style of test writing we use [BDD style testing](https://en.wikipedia.org/wiki/Behavior-driven_development); "cases". Cases are used to capture the various configurations and expected outputs of Greenwood when running its commands, in a way that is closer to how a user would be expecting Greenwood to work. +[TDD](https://en.wikipedia.org/wiki/Test-driven_development) is the recommended approach for developing for Greenwood and for the style of test writing we use [BDD style testing](https://en.wikipedia.org/wiki/Behavior-driven_development); "cases". Cases are used to capture the various configurations and expected outputs of Gre enwood when running its commands, in a way that is closer to how a user would be expecting Greenwood to work. ### Running Tests To run tests in watch mode, use: @@ -87,7 +134,7 @@ Below are some tips to help with running / debugging tests: > **PLEASE DO NOT COMMIT ANY OF THESE ABOVE CHANGES THOUGH** ### Writing Tests -Cases follow a convention starting with the command (e.g. `build`) and and the capability and features being tested, like configuration with a particular option (e.g. `publicPath`): +Cases follow a convention starting with the command (e.g. `build`) and and the capability and features being tested, like configuration with a particular option (e.g. `port`): ```shell ...spec.js ``` @@ -95,7 +142,7 @@ Cases follow a convention starting with the command (e.g. `build`) and and the c Examples: - _build.default.spec.js_ - Would test `greenwood build` with no config and no workspace. - _build.config.workspace-custom.spec.js_ - Would test `greenwood build` with a config that had a custom `workspace` -- _build.config.workspace-public-path.spec.js_ - Would test `greenwood build` with a config that had a custom `workspace` and `publicPath` set. +- _build.config.workspace-dev-server-port.spec.js_ - Would test `greenwood build` with a config that had a custom `workspace` and `devServer.port` set. ### Notes Here are some thigns to keep in mind while writing your tests, due to the asynchronous nature of Greenwwood: @@ -104,7 +151,7 @@ Here are some thigns to keep in mind while writing your tests, due to the asynch - Avoid arrow functions in mocha tests (e.g. `() => `) as this [can cause unexpected behaviors.](https://mochajs.org/#arrow-functions). Just use `function` instead. ## Internet Explorer -For situations that require testing Internet Explorer or Edge browser, Microsoft [provides Virtual Machines](https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/) for various combinations of Windows and Internet Explorer versions. [VirtualBox](https://www.virtualbox.org/) is a good platform to use for these VMs. +For situations that require testing Internet Explorer or Edge browser, Microsoft provides [Virtual Machines](https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/) for various combinations of Windows and Internet Explorer versions. [VirtualBox](https://www.virtualbox.org/) is a good platform to use for these VMs. To test from a VM, you can 1. Run `yarn serve` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e29cc7006..2c2cf9912 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: - node: [10, 12] + node: [12, 14] steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..255b62fb3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +name: Release Branch Integration + +on: + push: + branches: + - release/** + +jobs: + + build: + runs-on: ubuntu-18.04 + + strategy: + matrix: + node-version: [12.x] + + steps: + - uses: actions/checkout@v1 + - name: Install Chromium Library Dependencies + run: | + sh ./.github/workflows/chromium-lib-install.sh + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: Installing project dependencies + run: | + yarn install --frozen-lockfile && yarn lerna bootstrap + - name: Lint + run: | + yarn lint + - name: Test + run: | + yarn test + - name: Build + run: | + yarn clean && yarn build + env: + CI: true \ No newline at end of file diff --git a/README.md b/README.md index 60c5b9286..5bf400037 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ## Overview Greenwood is a modern and performant static site generator supporting Web Component based development. For more information about how to get started, lookup our docs, or learn more about the project, please visit our [website](https://www.greenwoodjs.io/). -> Greenwood is currently working towards a [1.0 release](https://github.com/ProjectEvergreen/greenwood/issues/418) with plans in our [next release (v.0.10.0)](https://github.com/ProjectEvergreen/greenwood/pull/436) to introduce some exciting [new changes and concepts](https://github.com/ProjectEvergreen/greenwood/releases/tag/v0.10.0-alpha.0) to the project. Check out our [roadmap](https://github.com/ProjectEvergreen/greenwood/projects) to see what we're working on next and feel free to reach out through our [issue tracker](https://github.com/ProjectEvergreen/greenwood/issues) if you have any issues. Additionally, please review our [Request for Contributions doc](https://docs.google.com/document/d/1MwDkszKvq81QgIYa8utJgyUgSpLZQx9eKCWjIikvfHU/) if would like to help us in building Greenwood! ✌️ +> Greenwood is currently working towards a [1.0 release](https://github.com/ProjectEvergreen/greenwood/issues/418) with our [recent release (v.0.10.0)](https://github.com/ProjectEvergreen/greenwood/pull/436) introducing some exciting [new changes and concepts](https://github.com/ProjectEvergreen/greenwood/releases/tag/v0.10.0) to the project. Check out our [roadmap](https://github.com/ProjectEvergreen/greenwood/projects) to see what we're working on next and feel free to reach out through our [issue tracker](https://github.com/ProjectEvergreen/greenwood/issues) if you have any issues. Additionally, please review our [Request for Contributions doc](https://docs.google.com/document/d/1MwDkszKvq81QgIYa8utJgyUgSpLZQx9eKCWjIikvfHU/) if would like to help us in building Greenwood! ✌️ ## Getting Started Our website has a complete [Getting Started](http://www.greenwoodjs.io/getting-started) section that will walk you through creating a Greenwood project from scratch. @@ -31,13 +31,13 @@ Then in your _package.json_, you can run the CLI like so: "scripts": { "build": "greenwood build", "start": "greenwood develop", - "eject": "greenwood eject", + "serve": "greenwood serve" } ``` -- `npm run build`: generates a static build of your project -- `npm start`: starts a local development server for your project -- `npm run eject`: ejects configurations to your working directory for additional customizations +- `greenwood build`: Generates a production build of your project +- `greenwood develop`: Starts a local development server for your project +- `greenwood serve`: Generates a production build of the project and serves it locally on a simple web server. ## Documentation All of our documentation is on our [website](https://www.greenwoodjs.io/) (which itself is built by Greenwood!). See our website documentation to learn more about: diff --git a/greenwood.config.js b/greenwood.config.js index dd0707819..7e11126e4 100644 --- a/greenwood.config.js +++ b/greenwood.config.js @@ -1,12 +1,16 @@ const path = require('path'); const pluginGoogleAnalytics = require('./packages/plugin-google-analytics/src/index'); +const pluginGraphQL = require('./packages/plugin-graphql/src/index'); +const pluginImportCss = require('./packages/plugin-import-css/src/index'); const pluginPolyfills = require('./packages/plugin-polyfills/src/index'); +const pluginPostCss = require('./packages/plugin-postcss/src/index'); const META_DESCRIPTION = 'A modern and performant static site generator supporting Web Component based development'; const FAVICON_HREF = '/assets/favicon.ico'; module.exports = { workspace: path.join(__dirname, 'www'), + mode: 'mpa', title: 'Greenwood', meta: [ { name: 'description', content: META_DESCRIPTION }, @@ -21,16 +25,20 @@ module.exports = { { name: 'google-site-verification', content: '4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0' } ], plugins: [ - ...pluginGoogleAnalytics({ + pluginGoogleAnalytics({ analyticsId: 'UA-147204327-1' }), - ...pluginPolyfills() + ...pluginGraphQL(), + pluginPolyfills(), + pluginPostCss(), + ...pluginImportCss() ], markdown: { plugins: [ - require('rehype-slug'), - require('rehype-autolink-headings'), - require('remark-github') + '@mapbox/rehype-prism', + 'rehype-slug', + 'rehype-autolink-headings', + 'remark-github' ] } }; \ No newline at end of file diff --git a/lerna.json b/lerna.json index 5ef854578..d51a27d4c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.0", + "version": "0.10.0-alpha.10", "packages": [ "packages/*", "www" diff --git a/netlify.toml b/netlify.toml index 47e01dfb3..36dece2b5 100644 --- a/netlify.toml +++ b/netlify.toml @@ -3,4 +3,7 @@ command = "yarn build" [build.processing] - skip_processing = true \ No newline at end of file + skip_processing = true + +[build.environment] + NODE_VERSION = "12.13.0" \ No newline at end of file diff --git a/nyc.config.js b/nyc.config.js index 6e7059bbe..b56f00d50 100644 --- a/nyc.config.js +++ b/nyc.config.js @@ -3,10 +3,10 @@ module.exports = { all: true, include: [ - 'packages/cli/src/data/*.js', + 'packages/cli/src/commands/*.js', 'packages/cli/src/lib/*.js', 'packages/cli/src/lifecycles/*.js', - 'packages/cli/src/tasks/*.js', + 'packages/cli/src/plugins/*.js', 'packages/plugin-*/src/*.js' ], @@ -19,10 +19,10 @@ module.exports = { checkCoverage: true, - statements: 85, - branches: 75, - functions: 90, - lines: 85, + statements: 80, + branches: 65, + functions: 85, + lines: 80, watermarks: { statements: [75, 85], diff --git a/package.json b/package.json index 0d10367f6..cfbc38af2 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "clean": "rimraf ./**/.greenwood/** && rimraf ./**/public/** && rimraf ./coverage", "clean:deps": "rimraf **/node_modules/**", "build": "node . build", - "serve": "yarn build && cd ./public && ws", + "serve": "node . serve", "develop": "node . develop", "test": "export BROWSERSLIST_IGNORE_OLD_DATA=true && nyc mocha", "test:tdd": "yarn test --watch", @@ -27,9 +27,9 @@ }, "devDependencies": { "@ls-lint/ls-lint": "^1.9.2", - "babel-eslint": "^10.0.3", "chai": "^4.2.0", "eslint": "^6.8.0", + "glob-promise": "^3.4.0", "jsdom": "^14.0.0", "lerna": "^3.16.4", "mocha": "^6.1.4", diff --git a/packages/cli/package.json b/packages/cli/package.json index 2f5775d2a..35ed43c3f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,19 +1,17 @@ { "name": "@greenwood/cli", - "version": "0.9.0", + "version": "0.10.0-alpha.10", "description": "Greenwood CLI.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli", "author": "Owen Buckley ", "license": "MIT", "keywords": [ "Greenwood", - "Web Components", - "Lit Element", - "Lit Html", - "Static Site Generator" + "Static Site Generator", + "Web Components" ], "engines": { - "node": ">=10.x" + "node": ">=12.x" }, "bin": { "greenwood": "./src/index.js" @@ -25,55 +23,42 @@ "access": "public" }, "dependencies": { - "@babel/core": "^7.8.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.8.3", - "@babel/preset-env": "^7.10.4", - "@babel/runtime": "^7.8.3", - "@webcomponents/webcomponentsjs": "^2.3.0", - "apollo-cache-inmemory": "^1.6.3", - "apollo-client": "^2.6.4", - "apollo-link-http": "^1.5.16", - "apollo-server": "^2.9.12", - "babel-loader": "^8.0.5", - "chalk": "^2.4.2", - "colors": "^1.3.3", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^9.0.0", + "@rollup/plugin-replace": "^2.3.4", + "@webcomponents/webcomponentsjs": "^2.4.4", + "acorn": "^8.0.1", + "acorn-walk": "^8.0.0", "commander": "^2.20.0", - "copy-webpack-plugin": "^5.0.3", - "core-js": "^3.4.1", - "css-loader": "^2.1.1", - "css-to-string-loader": "^0.1.3", "cssnano": "^4.1.10", - "file-loader": "^3.0.1", - "filewatcher-webpack-plugin": "^1.2.0", - "front-matter": "^3.0.1", - "fs-extra": "^8.1.0", - "graphql": "^14.5.8", - "graphql-tag": "^2.10.1", - "html-webpack-plugin": "^3.2.0", - "lit-element": "^2.0.1", - "lit-redux-router": "^0.9.3", - "local-web-server": "^2.6.1", + "es-module-shims": "^0.5.2", + "front-matter": "^4.0.2", + "koa": "^2.13.0", + "livereload": "^0.9.1", "markdown-toc": "^1.2.0", - "node-fetch": "^2.6.0", - "postcss-loader": "^3.0.0", - "postcss-nested": "^4.1.2", - "postcss-preset-env": "^6.7.0", - "puppeteer": "^1.20.0", - "pwa-helpers": "^0.9.1", - "redux": "^4.0.1", - "redux-thunk": "^2.3.0", - "style-loader": "^0.23.1", - "wc-markdown-loader": "~0.2.0", - "webpack": "^4.29.6", - "webpack-cli": "^3.3.0", - "webpack-dev-server": "^3.2.1", - "webpack-manifest-plugin": "^2.0.4", - "webpack-merge": "^4.2.1" + "node-html-parser": "^1.2.21", + "postcss": "^7.0.32", + "postcss-import": "^12.0.0", + "puppeteer": "^5.3.0", + "rehype-raw": "^5.0.0", + "rehype-stringify": "^8.0.0", + "remark-frontmatter": "^2.0.0", + "remark-parse": "^8.0.3", + "remark-rehype": "^7.0.0", + "rollup": "^2.34.1", + "rollup-plugin-multi-input": "^1.1.1", + "rollup-plugin-terser": "^7.0.0", + "unified": "^9.2.0" }, "devDependencies": { - "glob-promise": "^3.4.0", + "@mapbox/rehype-prism": "^0.5.0", + "lit-element": "^2.4.0", + "lodash-es": "^4.17.20", + "postcss-nested": "^4.1.2", + "pwa-helpers": "^0.9.1", + "redux": "^4.0.5", "rehype-autolink-headings": "^4.0.0", - "rehype-slug": "^3.0.0" + "rehype-slug": "^3.0.0", + "simpledotcss": "^1.0.0" } } diff --git a/packages/cli/src/commands/build.js b/packages/cli/src/commands/build.js new file mode 100644 index 000000000..069b88fb8 --- /dev/null +++ b/packages/cli/src/commands/build.js @@ -0,0 +1,55 @@ +const bundleCompilation = require('../lifecycles/bundle'); +const copyAssets = require('../lifecycles/copy'); +const { devServer } = require('../lifecycles/serve'); +const fs = require('fs'); +const generateCompilation = require('../lifecycles/compile'); +const serializeCompilation = require('../lifecycles/serialize'); +const { ServerInterface } = require('../lib/server-interface'); + +module.exports = runProductionBuild = async () => { + + return new Promise(async (resolve, reject) => { + + try { + const compilation = await generateCompilation(); + const port = compilation.config.devServer.port; + const outputDir = compilation.context.outputDir; + + devServer(compilation).listen(port, async () => { + console.info(`Started local development server at localhost:${port}`); + + // custom user server plugins + const servers = [...compilation.config.plugins.filter((plugin) => { + return plugin.type === 'server'; + }).map((plugin) => { + const provider = plugin.provider(compilation); + + if (!(provider instanceof ServerInterface)) { + console.warn(`WARNING: ${plugin.name}'s provider is not an instance of ServerInterface.`); + } + + return provider; + })]; + + await Promise.all(servers.map(async (server) => { + server.start(); + + return Promise.resolve(server); + })); + + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } + + await serializeCompilation(compilation); + await bundleCompilation(compilation); + await copyAssets(compilation); + + resolve(); + }); + } catch (err) { + reject(err); + } + }); + +}; \ No newline at end of file diff --git a/packages/cli/src/commands/develop.js b/packages/cli/src/commands/develop.js new file mode 100644 index 000000000..55c54ef9a --- /dev/null +++ b/packages/cli/src/commands/develop.js @@ -0,0 +1,39 @@ +const generateCompilation = require('../lifecycles/compile'); +const pluginLiveReloadServer = require('../plugins/server/plugin-livereload')()[0]; +const { ServerInterface } = require('../lib/server-interface'); +const { devServer } = require('../lifecycles/serve'); + +module.exports = runDevServer = async () => { + + return new Promise(async (resolve, reject) => { + + try { + const compilation = await generateCompilation(); + const { port } = compilation.config.devServer; + + devServer(compilation).listen(port, () => { + + console.info(`Started local development server at localhost:${port}`); + // custom user server plugins + const servers = [...compilation.config.plugins.concat([pluginLiveReloadServer]).filter((plugin) => { + return plugin.type === 'server'; + }).map((plugin) => { + const provider = plugin.provider(compilation); + + if (!(provider instanceof ServerInterface)) { + console.warn(`WARNING: ${plugin.name}'s provider is not an instance of ServerInterface.`); + } + + return provider; + })]; + + return Promise.all(servers.map(async (server) => { + return server.start(); + })); + }); + } catch (err) { + reject(err); + } + + }); +}; \ No newline at end of file diff --git a/packages/cli/src/commands/eject.js b/packages/cli/src/commands/eject.js new file mode 100644 index 000000000..c2108c757 --- /dev/null +++ b/packages/cli/src/commands/eject.js @@ -0,0 +1,27 @@ +const fs = require('fs'); +const generateCompilation = require('../lifecycles/compile'); +const path = require('path'); + +module.exports = ejectConfiguration = async () => { + return new Promise(async (resolve, reject) => { + try { + const compilation = await generateCompilation(); + const configFilePaths = fs.readdirSync(path.join(__dirname, '../config')); + + configFilePaths.forEach((configFile) => { + const from = path.join(__dirname, '../config', configFile); + const to = `${compilation.context.projectDirectory}/${configFile}`; + + fs.copyFileSync(from, to); + + console.log(`Ejected ${configFile} successfully.`); + }); + + console.debug('all configuration files ejected.'); + + resolve(); + } catch (err) { + reject(err); + } + }); +}; \ No newline at end of file diff --git a/packages/cli/src/commands/serve.js b/packages/cli/src/commands/serve.js new file mode 100644 index 000000000..efbd450cd --- /dev/null +++ b/packages/cli/src/commands/serve.js @@ -0,0 +1,20 @@ +const generateCompilation = require('../lifecycles/compile'); +const { prodServer } = require('../lifecycles/serve'); + +module.exports = runProdServer = async () => { + + return new Promise(async (resolve, reject) => { + + try { + const compilation = await generateCompilation(); + const port = 8080; + + prodServer(compilation).listen(port, () => { + console.info(`Started production test server at localhost:${port}`); + }); + } catch (err) { + reject(err); + } + + }); +}; \ No newline at end of file diff --git a/packages/cli/src/config/.browserslistrc b/packages/cli/src/config/.browserslistrc deleted file mode 100644 index 020b65cce..000000000 --- a/packages/cli/src/config/.browserslistrc +++ /dev/null @@ -1,3 +0,0 @@ -> 1% -not op_mini all -ie 11 \ No newline at end of file diff --git a/packages/cli/src/config/postcss.config.js b/packages/cli/src/config/postcss.config.js deleted file mode 100644 index 38d376edc..000000000 --- a/packages/cli/src/config/postcss.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - plugins: { - 'postcss-preset-env': {}, // stage 2+ - 'postcss-nested': {}, - 'cssnano': {} - } -}; \ No newline at end of file diff --git a/packages/cli/src/config/rollup.config.js b/packages/cli/src/config/rollup.config.js new file mode 100644 index 000000000..785aa675f --- /dev/null +++ b/packages/cli/src/config/rollup.config.js @@ -0,0 +1,504 @@ +/* eslint-disable max-depth, no-loop-func */ +const Buffer = require('buffer').Buffer; +const fs = require('fs'); +const htmlparser = require('node-html-parser'); +const json = require('@rollup/plugin-json'); +const multiInput = require('rollup-plugin-multi-input').default; +const { nodeResolve } = require('@rollup/plugin-node-resolve'); +const path = require('path'); +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/'; + +const parseTagForAttributes = (tag) => { + return tag.rawAttrs.split(' ').map((attribute) => { + if (attribute.indexOf('=') > 0) { + const attributePieces = attribute.split('='); + return { + [attributePieces[0]]: attributePieces[1].replace(/"/g, '').replace(/'/g, '') + }; + } else { + return undefined; + } + }).filter(attribute => attribute) + .reduce((accum, attribute) => { + return Object.assign(accum, { + ...attribute + }); + }, {}); +}; + +async function getOptimizedSource(url, plugins, compilation) { + const initSoure = fs.readFileSync(url, 'utf-8'); + let optimizedSource = await plugins.reduce(async (bodyPromise, resource) => { + const body = await bodyPromise; + const shouldOptimize = await resource.shouldOptimize(url, body); + + if (shouldOptimize) { + const optimizedBody = await resource.optimize(url, body); + + return Promise.resolve(optimizedBody); + } else { + return Promise.resolve(body); + } + }, Promise.resolve(initSoure)); + + // if no custom user optimization found, fallback to standard Greenwood default optimization + if (optimizedSource === initSoure) { + const standardPluginsPath = path.join(__dirname, '../', 'plugins/resource'); + const standardPlugins = (await fs.promises.readdir(standardPluginsPath)) + .filter(filename => filename.indexOf('plugin-standard') === 0) + .map((filename) => { + return require(`${standardPluginsPath}/${filename}`); + }).map((plugin) => { + return plugin.provider(compilation); + }); + + optimizedSource = await standardPlugins.reduce(async (sourcePromise, resource) => { + const source = await sourcePromise; + const shouldOptimize = await resource.shouldOptimize(url, source); + + if (shouldOptimize) { + const defaultOptimizedSource = await resource.optimize(url, source); + + return Promise.resolve(defaultOptimizedSource); + } else { + return Promise.resolve(source); + } + }, Promise.resolve(optimizedSource)); + } + + return Promise.resolve(optimizedSource); +} + +function greenwoodWorkspaceResolver (compilation) { + const { userWorkspace, scratchDir } = compilation.context; + + return { + name: 'greenwood-workspace-resolver', + resolveId(source) { + if ((source.indexOf('./') === 0 || source.indexOf('/') === 0) && path.extname(source) !== '.html' && fs.existsSync(path.join(userWorkspace, source))) { + return source.replace(source, path.join(userWorkspace, source)); + } + + // handle inline script / style bundling + if (source.indexOf(`-${tokenSuffix}`) > 0 && fs.existsSync(path.join(scratchDir, source))) { + return source.replace(source, path.join(scratchDir, source)); + } + + return null; + } + }; +} + +// https://github.com/rollup/rollup/issues/2873 +function greenwoodHtmlPlugin(compilation) { + const { projectDirectory, userWorkspace, outputDir, scratchDir } = compilation.context; + 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) => { + return plugin.provider(compilation); + }); + + return { + name: 'greenwood-html-plugin', + // tell Rollup how to handle HTML entry points + // and other custom user resource types like .ts, .gql, etc + async load(id) { + const extension = path.extname(id); + + switch (extension) { + + case '.html': + return Promise.resolve(''); + default: + const resourceHandler = (await Promise.all(customResources.map(async (resource) => { + const shouldServe = await resource.shouldServe(id); + + return shouldServe + ? resource + : null; + }))).filter(resource => resource); + + if (resourceHandler.length) { + const response = await resourceHandler[0].serve(id); + + return Promise.resolve(response.body); + } + break; + + } + }, + + // crawl through all entry HTML files and emit JavaScript chunks and CSS assets along the way + // for bundling with Rollup + buildStart(options) { + const mappedStyles = []; + const mappedScripts = new Map(); + + for (const input in options.input) { + try { + const inputHtml = options.input[input]; + const html = fs.readFileSync(inputHtml, 'utf-8'); + const root = htmlparser.parse(html, { + script: true, + style: true + }); + const headScripts = root.querySelectorAll('script'); + const headLinks = root.querySelectorAll('link'); + + headScripts.forEach((scriptTag) => { + const parsedAttributes = parseTagForAttributes(scriptTag); + + // handle + if (!isRemoteUrl(parsedAttributes.src) && parsedAttributes.type === 'module' && parsedAttributes.src && !mappedScripts.get(parsedAttributes.src)) { + if (optimization === 'static') { + // console.debug('dont emit ', parsedAttributes.src); + } else { + const { src } = parsedAttributes; + + 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 + if (parsedAttributes.type === 'module' && scriptTag.rawText !== '') { + const id = Buffer.from(scriptTag.rawText).toString('base64').slice(0, 8).toLowerCase(); + + if (!mappedScripts.get(id)) { + const filename = `${id}-${tokenSuffix}.js`; + const source = ` + // ${filename} + ${scriptTag.rawText} + `.trim(); + + fs.writeFileSync(path.join(scratchDir, filename), source); + mappedScripts.set(id, true); + + this.emitFile({ + type: 'chunk', + id: filename, + name: filename.replace('.js', ''), + source + }); + } + } + }); + + headLinks.forEach((linkTag) => { + const parsedAttributes = parseTagForAttributes(linkTag); + + // handle + if (!isRemoteUrl(parsedAttributes.href) && parsedAttributes.rel === 'stylesheet' && !mappedStyles[parsedAttributes.href]) { + let { href } = parsedAttributes; + + if (href.charAt(0) === '/') { + href = href.slice(1); + } + + const basePath = href.indexOf(tokenNodeModules) >= 0 + ? projectDirectory + : userWorkspace; + const filePath = path.join(basePath, href.replace('../', './')); + const source = fs.readFileSync(filePath, 'utf-8'); + const to = `${outputDir}/${href}`; + const hash = Buffer.from(source).toString('base64').toLowerCase(); + const fileName = href + .replace('.css', `.${hash.slice(0, 8)}.css`) + .replace('../', '') + .replace('./', ''); + + if (!fs.existsSync(path.dirname(to)) && href.indexOf(tokenNodeModules) < 0) { + fs.mkdirSync(path.dirname(to), { + recursive: true + }); + } + + mappedStyles[parsedAttributes.href] = { + type: 'asset', + fileName: fileName.indexOf(tokenNodeModules) >= 0 + ? path.basename(fileName) + : fileName, + name: href, + source + }; + } + }); + } catch (e) { + console.error(e); + } + } + + // this is a giant work around because PostCSS and some plugins can only be run async + // and so have to use with await but _outside_ sync code, like parser / rollup + // https://github.com/cssnano/cssnano/issues/68 + // https://github.com/postcss/postcss/issues/595 + Promise.all(Object.keys(mappedStyles).map(async (assetKey) => { + const asset = mappedStyles[assetKey]; + const source = mappedStyles[assetKey].source; + const basePath = asset.name.indexOf(tokenNodeModules) >= 0 + ? projectDirectory + : userWorkspace; + const result = await postcss() + .use(postcssImport()) + .process(source, { + from: path.join(basePath, asset.name) + }); + + asset.source = result.css; + + return new Promise((resolve, reject) => { + try { + this.emitFile(asset); + resolve(); + } catch (e) { + reject(e); + } + }); + })); + }, + + // crawl through all entry HTML files and map bundled JavaScript and CSS filenames + // back to original + if (!isRemoteUrl(parsedAttributes.src) && parsedAttributes.type === 'module' && parsedAttributes.src) { + for (const innerBundleId of Object.keys(bundles)) { + const { src } = parsedAttributes; + const facadeModuleId = bundles[innerBundleId].facadeModuleId; + 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) { + 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, ''); + } + } + } + }); + + headLinks.forEach((linkTag) => { + const parsedAttributes = parseTagForAttributes(linkTag); + const { href } = parsedAttributes; + + // handle + if (parsedAttributes.rel === 'stylesheet') { + for (const bundleId2 of Object.keys(bundles)) { + if (bundleId2.indexOf('.css') > 0) { + const bundle2 = bundles[bundleId2]; + if (href.indexOf(bundle2.name) >= 0) { + const newHref = `/${bundle2.fileName}`; + + newHtml = newHtml.replace(href, newHref); + + if (optimization !== 'none' && optimization !== 'inline') { + newHtml = newHtml.replace('', ` + + + `); + } + } + } + } + } + }); + + bundle.fileName = bundle.facadeModuleId.replace('.greenwood', 'public'); + bundle.code = newHtml; + } + } catch (e) { + console.error('ERROR', e); + } + } + }, + + async writeBundle(outputOptions, bundles) { + const scratchFiles = {}; + + for (const bundleId of Object.keys(bundles)) { + const bundle = bundles[bundleId]; + + if (bundle.isEntry && path.extname(bundle.facadeModuleId) === '.html') { + const htmlPath = bundle.facadeModuleId.replace('.greenwood', 'public'); + let html = fs.readFileSync(htmlPath, 'utf-8'); + const root = htmlparser.parse(html, { + script: true, + 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)) { + if (innerBundleId.indexOf(`-${tokenSuffix}`) > 0 && path.extname(innerBundleId) === '.js') { + const bundledSource = fs.readFileSync(path.join(outputDir, innerBundleId), 'utf-8') + .replace(/\.\//g, '/'); // force absolute paths + + html = html.replace(scriptTag.rawText, bundledSource); + scratchFiles[innerBundleId] = true; + } + } + } + }); + + 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}`; + const optimizedSource = await getOptimizedSource(sourcePath, customResources, compilation); + + await fs.promises.writeFile(sourcePath, optimizedSource); + } + } + + // cleanup any scratch files + return Promise.all(Object.keys(scratchFiles).map(async (file) => { + return await fs.promises.unlink(path.join(outputDir, file)); + })); + } + }; +} + +module.exports = getRollupConfig = async (compilation) => { + const { scratchDir, outputDir } = compilation.context; + const defaultRollupPlugins = [ + replace({ // https://github.com/rollup/rollup/issues/487#issuecomment-177596512 + 'process.env.NODE_ENV': JSON.stringify('production') + }), + nodeResolve(), + greenwoodWorkspaceResolver(compilation), + greenwoodHtmlPlugin(compilation), + multiInput(), + json() + ]; + 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() + ); + } + + return [{ + input: `${scratchDir}**/*.html`, + output: { + dir: outputDir, + entryFileNames: '[name].[hash].js', + chunkFileNames: '[name].[hash].js' + }, + onwarn: (messageObj) => { + if ((/EMPTY_BUNDLE/).test(messageObj.code)) { + return; + } else { + console.debug(messageObj.message); + } + }, + plugins: [ + ...defaultRollupPlugins, + ...customRollupPlugins + ] + }]; + +}; \ No newline at end of file diff --git a/packages/cli/src/config/webpack.config.common.js b/packages/cli/src/config/webpack.config.common.js deleted file mode 100644 index ee3a0fda0..000000000 --- a/packages/cli/src/config/webpack.config.common.js +++ /dev/null @@ -1,172 +0,0 @@ -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const fs = require('fs'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const path = require('path'); -const webpack = require('webpack'); - -const getUserWorkspaceDirectories = (source) => { - return fs.readdirSync(source) - .map(name => path.join(source, name)) - .filter(path => fs.lstatSync(path).isDirectory()); -}; -const mapUserWorkspaceDirectories = (directoryPath, userWorkspaceDirectory) => { - const directoryName = directoryPath.replace(`${userWorkspaceDirectory}/`, ''); - const userWorkspaceDirectoryRoot = userWorkspaceDirectory.split('/').slice(-1); - - return new webpack.NormalModuleReplacementPlugin( - // https://github.com/ProjectEvergreen/greenwood/issues/132 - new RegExp(`\\.\\.\\/${directoryName}.+$(? { - - // workaround to ignore cli/templates default imports when rewriting - if (!new RegExp('\/cli\/templates').test(resource.content)) { - resource.request = resource.request.replace(new RegExp(`\\.\\.\\/${directoryName}`), directoryPath); - } - - // 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); - } - } - - ); -}; - -module.exports = ({ config, context }) => { - const { userWorkspace } = context; - - // dynamically map all the user's workspace directories for resolution by webpack - // this essentially helps us keep watch over changes from the user's workspace forgreenwood's build pipeline - const mappedUserDirectoriesForWebpack = getUserWorkspaceDirectories(userWorkspace) - .map((directory) => { - return mapUserWorkspaceDirectories(directory, userWorkspace); - }); - - // if user has an assets/ directory in their workspace, automatically copy it for them - const userAssetsDirectoryForWebpack = fs.existsSync(context.assetDir) ? [{ - from: context.assetDir, - to: path.join(context.publicDir, 'assets') - }] : []; - - const commonCssLoaders = [ - { loader: 'css-loader' }, - { - loader: 'postcss-loader', - options: { - config: { - path: context.postcssConfig - } - } - } - ]; - - // gets Index Hooks to pass as options to HtmlWebpackPlugin - const customOptions = Object.assign({}, ...config.plugins - .filter((plugin) => plugin.type === 'index') - .map((plugin) => plugin.provider({ config, context })) - .filter((providerResult) => { - return Object.keys(providerResult).map((key) => { - if (key !== 'type') { - return providerResult[key]; - } - }); - })); - - // utilizes webpack plugins passed in directly by the user - const customWebpackPlugins = config.plugins - .filter((plugin) => plugin.type === 'webpack') - .map((plugin) => plugin.provider({ config, context })); - - return { - - resolve: { - extensions: ['.js', '.json', '.gql', '.graphql'], - alias: { - '@greenwood/cli/data': context.dataDir, - '@greenwood/cli/templates/app-template': path.join(context.scratchDir, 'app', 'app-template.js') - } - }, - - entry: { - index: path.join(context.scratchDir, 'app', 'app.js') - }, - - output: { - path: path.join(context.publicDir, '.', config.publicPath), - filename: '[name].[hash].bundle.js', - chunkFilename: '[name].[hash].bundle.js', - publicPath: config.publicPath - }, - - module: { - rules: [{ - test: /\.js$/, - loader: 'babel-loader', - options: { - configFile: context.babelConfig - } - }, { - test: /\.md$/, - loader: 'wc-markdown-loader', - options: { - defaultStyle: false, - shadowRoot: false, - preset: { ...config.markdown } - } - }, { - test: /\.css$/, - exclude: new RegExp(`${config.themeFile}`), - loaders: [ - { loader: 'css-to-string-loader' }, - ...commonCssLoaders - ] - }, { - test: new RegExp(`${config.themeFile}`), - loaders: [ - { loader: 'style-loader' }, - ...commonCssLoaders - ] - }, { - test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, - loader: 'url-loader?limit=10000&mimetype=application/font-woff' - }, { - test: /\.(ttf|eot|svg|jpe?g|png|gif|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/, - loader: 'file-loader' - }, { - test: /\.(graphql|gql)$/, - loader: 'graphql-tag/loader' - }] - }, - - plugins: [ - new HtmlWebpackPlugin({ - filename: path.join(context.publicDir, context.indexPageTemplate), - template: path.join(context.scratchDir, context.indexPageTemplate), - chunksSortMode: 'dependency', - ...customOptions - }), - - new HtmlWebpackPlugin({ - filename: path.join(context.publicDir, context.notFoundPageTemplate), - template: path.join(context.scratchDir, context.notFoundPageTemplate), - chunksSortMode: 'dependency', - ...customOptions - }), - - ...mappedUserDirectoriesForWebpack, - - new CopyWebpackPlugin(userAssetsDirectoryForWebpack), - - new webpack.NormalModuleReplacementPlugin( - /\.md/, - (resource) => { - resource.request = resource.request.replace(/^\.\//, context.pagesDir); - } - ), - - ...customWebpackPlugins - ] - }; -}; \ No newline at end of file diff --git a/packages/cli/src/config/webpack.config.develop.js b/packages/cli/src/config/webpack.config.develop.js deleted file mode 100644 index ccf19c428..000000000 --- a/packages/cli/src/config/webpack.config.develop.js +++ /dev/null @@ -1,93 +0,0 @@ -const path = require('path'); -const ManifestPlugin = require('webpack-manifest-plugin'); -const FilewatcherPlugin = require('filewatcher-webpack-plugin'); -const generateCompilation = require('../lifecycles/compile'); -const webpackMerge = require('webpack-merge'); -const commonConfig = require('./webpack.config.common.js'); - -let isRebuilding = false; - -const rebuild = async() => { - if (!isRebuilding) { - isRebuilding = true; - - // rebuild web components - await generateCompilation(); - - // debounce - setTimeout(() => { - isRebuilding = false; - }, 1000); - } -}; - -module.exports = ({ config, context, graph }) => { - config.publicPath = '/'; - - const configWithContext = commonConfig({ config, context, graph }); - const { devServer, publicPath } = config; - const { host, port } = devServer; - - // decorate HtmlWebpackPlugin instance with devServer specific SPA handling for index.html - configWithContext.plugins[0].options.hookGreenwoodSpaIndexFallback = ` - - `, - - // decorate HtmlWebpackPlugin instance with devServer specific SPA handling for 404.html - configWithContext.plugins[1].options.hookGreenwoodSpaIndexFallback = ` - - - - `; - - return webpackMerge(configWithContext, { - - mode: 'development', - - entry: [ - `webpack-dev-server/client?http://${host}:${port}`, - path.join(context.scratchDir, 'app', 'app.js') - ], - - devServer: { - port, - host, - disableHostCheck: true, - historyApiFallback: true, - hot: false, - inline: true - }, - - plugins: [ - new FilewatcherPlugin({ - watchFileRegex: [`/${context.userWorkspace}/`], - onReadyCallback: () => { - console.log(`Now serving Development Server available at ${host}:${port}`); - }, - onChangeCallback: async () => { - rebuild(); - }, - usePolling: true, - atomic: true, - ignored: '/node_modules/' - }), - - new ManifestPlugin({ - fileName: 'manifest.json', - publicPath - }) - ] - }); -}; \ No newline at end of file diff --git a/packages/cli/src/config/webpack.config.prod.js b/packages/cli/src/config/webpack.config.prod.js deleted file mode 100644 index 62764d0d7..000000000 --- a/packages/cli/src/config/webpack.config.prod.js +++ /dev/null @@ -1,17 +0,0 @@ -const webpackMerge = require('webpack-merge'); -const commonConfig = require('./webpack.config.common.js'); - -module.exports = ({ config, context, graph }) => { - const configWithContext = commonConfig({ config, context, graph }); - - return webpackMerge(configWithContext, { - - mode: 'production', - - performance: { - hints: 'warning' - } - - }); - -}; \ No newline at end of file diff --git a/packages/cli/src/data/common.js b/packages/cli/src/data/common.js deleted file mode 100644 index 278a7370e..000000000 --- a/packages/cli/src/data/common.js +++ /dev/null @@ -1,46 +0,0 @@ -// https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0#gistcomment-2775538 -function hashString(queryKeysString) { - let h = 0; - - for (let i = 0; i < queryKeysString.length; i += 1) { - h = Math.imul(31, h) + queryKeysString.charCodeAt(i) | 0; // eslint-disable-line no-bitwise - } - - return Math.abs(h).toString(); -} - -function getQueryKeysFromSelectionSet(selectionSet) { - let queryKeys = ''; - - for (let key in selectionSet) { - - if (key === 'selections') { - queryKeys += selectionSet[key] - .filter(selection => selection.name.value !== '__typename') // __typename is added by server.js - .map(selection => selection.name.value).join(''); - } - } - - if (selectionSet.kind === 'SelectionSet') { - selectionSet.selections.forEach(selection => { - if (selection.selectionSet) { - queryKeys += getQueryKeysFromSelectionSet(selection.selectionSet); - } - }); - } - - return queryKeys; -} - -function getQueryHash(query, variables = {}) { - const queryKeys = getQueryKeysFromSelectionSet(query.definitions[0].selectionSet); - const variableValues = Object.keys(variables).length > 0 - ? `_${Object.values(variables).join('').replace(/\//g, '')}` // handle / which will translate to filepaths - : ''; - - return hashString(`${queryKeys}${variableValues}`); -} - -module.exports = { - getQueryHash -}; \ No newline at end of file diff --git a/packages/cli/src/data/queries/children.gql b/packages/cli/src/data/queries/children.gql deleted file mode 100644 index 3f7303b29..000000000 --- a/packages/cli/src/data/queries/children.gql +++ /dev/null @@ -1,10 +0,0 @@ -query($parent: String!) { - children(parent: $parent) { - id, - title, - link, - filePath, - fileName, - template - } -} \ No newline at end of file diff --git a/packages/cli/src/data/queries/graph.gql b/packages/cli/src/data/queries/graph.gql deleted file mode 100644 index 7aaf1fd02..000000000 --- a/packages/cli/src/data/queries/graph.gql +++ /dev/null @@ -1,10 +0,0 @@ -query { - graph { - id, - title, - link, - filePath, - fileName, - template - } -} \ No newline at end of file diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index dd2fadcfc..876d5244a 100755 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -5,39 +5,44 @@ // https://github.com/ProjectEvergreen/greenwood/issues/141 process.setMaxListeners(0); -require('colors'); - -const chalk = require('chalk'); const program = require('commander'); -const generateCompilation = require('./lifecycles/compile'); -const runProdBuild = require('./tasks/build'); -const runDevServer = require('./tasks/develop'); -const ejectConfigFiles = require('./tasks/eject'); -const scriptPkg = require('../package.json'); -let compilation = {}; +const runProductionBuild = require('./commands/build'); +const runDevServer = require('./commands/develop'); +const runProdServer = require('./commands/serve'); +const ejectConfiguration = require('./commands/eject'); +const greenwoodPackageJson = require('../package.json'); + let cmdOption = {}; -let MODE = ''; +let command = ''; -console.log(`${chalk.rgb(175, 207, 71)('-------------------------------------------------------')}`); -console.log(`${chalk.rgb(175, 207, 71)('Welcome to Greenwood ♻️')}`); -console.log(`${chalk.rgb(175, 207, 71)('-------------------------------------------------------')}`); +console.info('-------------------------------------------------------'); +console.info('Welcome to Greenwood ♻️'); +console.info('-------------------------------------------------------'); program - .version(scriptPkg.version) + .version(greenwoodPackageJson.version) .arguments('') - .usage(`${chalk.green('')} [options]`); + .usage(' [options]'); program .command('build') .description('Build a static site for production.') .action((cmd) => { - MODE = cmd._name; + command = cmd._name; }); + program .command('develop') .description('Start a local development server.') .action((cmd) => { - MODE = cmd._name; + command = cmd._name; + }); + +program + .command('serve') + .description('View a production build locally with a basic web server.') + .action((cmd) => { + command = cmd._name; }); program @@ -45,7 +50,7 @@ program .option('-a, --all', 'eject all configurations including babel, postcss, browserslistrc') .description('Eject greenwood configurations.') .action((cmd) => { - MODE = cmd._name; + command = cmd._name; cmdOption.all = cmd.all; }); @@ -55,52 +60,46 @@ if (program.parse.length === 0) { program.help(); } -const run = async() => { - +const run = async() => { try { + console.info(`Running Greenwood with the ${command} command.`); + process.env.__GWD_COMMAND__ = command; - switch (MODE) { - - case 'build': - compilation = await generateCompilation(); + switch (command) { - console.log('Building project for production.'.yellow); - - await runProdBuild(compilation); - - console.log('...................................'.yellow); - console.log('Static site generation complete!'); - console.log('...................................'.yellow); + case 'build': + await runProductionBuild(); break; case 'develop': - compilation = await generateCompilation(); + await runDevServer(); - console.log('Starting local development server'.yellow); - - await runDevServer(compilation); + break; + case 'serve': + process.env.__GWD_COMMAND__ = 'build'; - console.log('Development mode activiated'.green); + await runProductionBuild(); + await runProdServer(); break; case 'eject': - console.log('Ejecting configurations'.yellow); - - await ejectConfigFiles(cmdOption.all); - - console.log(`Configurations ejected successfully to ${process.cwd()}`.green); + await ejectConfiguration(); break; default: - console.log('Error: missing command. try checking --help if you\'re encountering issues'); + console.warn(` + Error: not able to detect command. try using the --help flag if + you're encountering issues running Greenwood. Visit our docs for more + info at https://www.greenwoodjs.io/docs/. + `); break; } process.exit(0); // eslint-disable-line no-process-exit } catch (err) { - console.error(`${err}`.red); + console.error(err); process.exit(1); // eslint-disable-line no-process-exit } }; -run(); +run(); \ No newline at end of file diff --git a/packages/cli/src/lib/browser.js b/packages/cli/src/lib/browser.js index 362ea3637..0ed78ff5b 100644 --- a/packages/cli/src/lib/browser.js +++ b/packages/cli/src/lib/browser.js @@ -36,17 +36,17 @@ class BrowserRunner { await page.setRequestInterception(true); - // only allow puppeteer to load necessary scripts needed for pre-rendering of the site itself + // only allow puppeteer to load necessary (local) scripts needed for pre-rendering of the site itself page.on('request', interceptedRequest => { const interceptedRequestUrl = interceptedRequest.url(); if ( - interceptedRequestUrl.indexOf('bundle.js') >= 0 || // webpack bundles, webcomponents-bundle.js - interceptedRequestUrl === requestUrl || // pages / routes - interceptedRequestUrl.indexOf('localhost:4000') >= 0 // Apollo GraphQL server + interceptedRequestUrl.indexOf('http://127.0.0.1') >= 0 || + interceptedRequestUrl.indexOf('localhost') >= 0 ) { interceptedRequest.continue(); } else { + // console.warn('aborting request', interceptedRequestUrl); interceptedRequest.abort(); } }); @@ -72,6 +72,8 @@ class BrowserRunner { // Serialize page. const content = await page.content(); + // console.debug('content????', content); + await page.close(); return content; diff --git a/packages/cli/src/lib/resource-interface.js b/packages/cli/src/lib/resource-interface.js new file mode 100644 index 000000000..cf32b4ea9 --- /dev/null +++ b/packages/cli/src/lib/resource-interface.js @@ -0,0 +1,69 @@ +const path = require('path'); + +class ResourceInterface { + constructor(compilation, options = {}) { + this.compilation = compilation; + this.options = options; + this.extensions = []; + this.contentType = ''; + } + + // test if this plugin should change a relative URL from the browser to an absolute path on disk + // like for node_modules/ resolution. not commonly needed by most resource plugins + // return true | false + // eslint-disable-next-line no-unused-vars + async shouldResolve(url) { + return Promise.resolve(false); + } + + // return an absolute path + async resolve(url) { + return Promise.resolve(url); + } + + // test if this plugin should be used to process a given url / header combo the browser and retu + // ex: ` - `; - - let html = content - .replace(polyfill, '') - .replace('', apolloScript); + console.info('serializing page...', route); + + return await browserRunner + .serialize(`${serverUrl}${route}`) + .then(async (indexHtml) => { + const outputPath = `${outputDir}${route}index.html`; + console.info(`Serializing complete for page ${route}.`); + + const htmlOptimized = await optimizeResources.reduce(async (htmlPromise, resource) => { + const html = await htmlPromise; + const shouldOptimize = await resource.shouldOptimize(outputPath, html); + + return shouldOptimize + ? resource.optimize(outputPath, html) + : Promise.resolve(html); + }, Promise.resolve(indexHtml)); - if (isStrictOptimization) { // no javascript - html = html.replace(/`); - - await fs.writeFile(indexContentsPath, indexContentsPolyfilled); - - await dataServer(compilation).listen().then((server) => { - console.log(`dataServer started at ${server.url}`); - }); - - // "serialize" our SPA into a static site - const webServer = localWebServer.listen({ - port: PORT, - https: false, - directory: context.publicDir, - spa: context.indexPageTemplate - }); - - await runBrowser(compilation); + const pages = compilation.graph; + const port = compilation.config.devServer.port; + const outputDir = compilation.context.scratchDir; + const serverAddress = `http://127.0.0.1:${port}`; + console.info(`Serializing pages at ${serverAddress}`); + console.debug('pages to generate', `\n ${pages.map(page => page.path).join('\n ')}`); + + await runBrowser(serverAddress, pages, outputDir); + + console.info('done serializing all pages'); browserRunner.close(); - webServer.close(); resolve(); } catch (err) { reject(err); } - }); }; \ No newline at end of file diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js new file mode 100644 index 000000000..5b99b8772 --- /dev/null +++ b/packages/cli/src/lifecycles/serve.js @@ -0,0 +1,206 @@ +const fs = require('fs'); +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'); +const pluginResourceStandardImage = require('../plugins/resource/plugin-standard-image'); +const pluginResourceStandardJavaScript = require('../plugins/resource/plugin-standard-javascript'); +const pluginResourceStandardJson = require('../plugins/resource/plugin-standard-json'); +const pluginLiveReloadResource = require('../plugins/server/plugin-livereload')()[1]; +const pluginUserWorkspace = require('../plugins/resource/plugin-user-workspace'); +const { ResourceInterface } = require('../lib/resource-interface'); + +function getDevServer(compilation) { + const app = new Koa(); + const compilationCopy = Object.assign({}, compilation); + const resources = [ + // Greenwood default standard resource and import plugins + pluginUserWorkspace.provider(compilation), + pluginNodeModules.provider(compilation), + pluginResourceStandardCss.provider(compilationCopy), + pluginResourceStandardFont.provider(compilationCopy), + pluginResourceStandardHtml.provider(compilationCopy), + pluginResourceStandardImage.provider(compilationCopy), + pluginResourceStandardJavaScript.provider(compilationCopy), + pluginResourceStandardJson.provider(compilationCopy), + pluginResourceOptimizationMpa().provider(compilationCopy), + + // custom user resource plugins + ...compilation.config.plugins.filter((plugin) => { + return plugin.type === 'resource'; + }).map((plugin) => { + const provider = plugin.provider(compilationCopy); + + if (!(provider instanceof ResourceInterface)) { + console.warn(`WARNING: ${plugin.name}'s provider is not an instance of ResourceInterface.`); + } + + return provider; + }) + ]; + + // resolve urls to paths first + app.use(async (ctx, next) => { + ctx.url = await resources.reduce(async (responsePromise, resource) => { + const response = await responsePromise; + const { url } = ctx; + const resourceShouldResolveUrl = await resource.shouldResolve(url); + + return resourceShouldResolveUrl + ? resource.resolve(url) + : Promise.resolve(response); + }, Promise.resolve('')); + + await next(); + }); + + // then handle serving urls + app.use(async (ctx, next) => { + const responseAccumulator = { + body: ctx.body, + contentType: ctx.response.contentType + }; + + const reducedResponse = await resources.reduce(async (responsePromise, resource) => { + const response = await responsePromise; + const { url } = ctx; + const { headers } = ctx.response; + const shouldServe = await resource.shouldServe(url, { + request: ctx.headers, + response: headers + }); + + if (shouldServe) { + const resolvedResource = await resource.serve(url, { + request: ctx.headers, + response: headers + }); + + return Promise.resolve({ + ...response, + ...resolvedResource + }); + } else { + return Promise.resolve(response); + } + }, Promise.resolve(responseAccumulator)); + + ctx.set('Content-Type', reducedResponse.contentType); + ctx.body = reducedResponse.body; + + await next(); + }); + + // allow intercepting of urls (response) + app.use(async (ctx) => { + const modifiedResources = resources.concat( + pluginLiveReloadResource.provider(compilation) + ); + const responseAccumulator = { + body: ctx.body, + contentType: ctx.response.headers['content-type'] + }; + + const reducedResponse = await modifiedResources.reduce(async (responsePromise, resource) => { + const response = await responsePromise; + const { url } = ctx; + const { headers } = ctx.response; + const shouldIntercept = await resource.shouldIntercept(url, response.body, { + request: ctx.headers, + response: headers + }); + + if (shouldIntercept) { + const interceptedResponse = await resource.intercept(url, response.body, { + request: ctx.headers, + response: headers + }); + + return Promise.resolve({ + ...response, + ...interceptedResponse + }); + } else { + return Promise.resolve(response); + } + }, Promise.resolve(responseAccumulator)); + + ctx.set('Content-Type', reducedResponse.contentType); + ctx.body = reducedResponse.body; + }); + + return app; +} + +function getProdServer(compilation) { + const app = new Koa(); + + app.use(async ctx => { + const { outputDir } = compilation.context; + const { url } = ctx.request; + + if (url.endsWith('/') || url.endsWith('.html')) { + const barePath = url.endsWith('/') ? path.join(url, 'index.html') : url; + const contents = await fs.promises.readFile(path.join(outputDir, barePath), 'utf-8'); + + ctx.set('Content-Type', 'text/html'); + ctx.body = contents; + } + + if (url.endsWith('.js')) { + const contents = await fs.promises.readFile(path.join(outputDir, url), 'utf-8'); + + ctx.set('Content-Type', 'text/javascript'); + ctx.body = contents; + } + + if (url.endsWith('.css')) { + const contents = await fs.promises.readFile(path.join(outputDir, url), 'utf-8'); + + ctx.set('Content-Type', 'text/css'); + ctx.body = contents; + } + + if (url.indexOf('assets/')) { + const assetPath = path.join(outputDir, url); + const ext = path.extname(assetPath); + const type = ext === '.svg' + ? `${ext.replace('.', '')}+xml` + : ext.replace('.', ''); + + if (['.jpg', '.png', '.gif', '.svg'].includes(ext)) { + ctx.set('Content-Type', `image/${type}`); + + if (ext === '.svg') { + ctx.body = await fs.promises.readFile(assetPath, 'utf-8'); + } else { + ctx.body = await fs.promises.readFile(assetPath); + } + } else if (['.woff2', '.woff', '.ttf'].includes(ext)) { + ctx.set('Content-Type', `font/${type}`); + ctx.body = await fs.promises.readFile(assetPath); + } else if (['.ico'].includes(ext)) { + ctx.set('Content-Type', 'image/x-icon'); + ctx.body = await fs.promises.readFile(assetPath); + } + } + + if (url.endsWith('.json')) { + const contents = await fs.promises.readFile(path.join(outputDir, url), 'utf-8'); + + ctx.set('Content-Type', 'application/json'); + ctx.body = JSON.parse(contents); + } + }); + + return app; +} + +module.exports = { + devServer: getDevServer, + prodServer: getProdServer +}; \ No newline at end of file diff --git a/packages/cli/src/plugins/resource/plugin-node-modules.js b/packages/cli/src/plugins/resource/plugin-node-modules.js new file mode 100644 index 000000000..836a25bea --- /dev/null +++ b/packages/cli/src/plugins/resource/plugin-node-modules.js @@ -0,0 +1,250 @@ +/* + * + * Detects and fully resolves requests to node_modules and handles creating an importMap. + * + */ +const acorn = require('acorn'); +const fs = require('fs'); +const path = require('path'); +const { ResourceInterface } = require('../../lib/resource-interface'); +const walk = require('acorn-walk'); + +const importMap = {}; + +const getPackageEntryPath = (packageJson) => { + let entry = packageJson.module + ? packageJson.module // favor ESM entry points first + : packageJson.exports // next favor export maps + ? Object.keys(packageJson.exports) + : packageJson.main // then favor main + ? packageJson.main + : 'index.js'; // lastly, fallback to index.js + + // use .mjs version if it exists, for packages like redux + if (!Array.isArray(entry) && fs.existsSync(`${process.cwd()}/node_modules/${packageJson.name}/${entry.replace('.js', '.mjs')}`)) { + entry = entry.replace('.js', '.mjs'); + } + + return entry; +}; + +const walkModule = (module, dependency) => { + walk.simple(acorn.parse(module, { + ecmaVersion: '2020', + sourceType: 'module' + }), { + ImportDeclaration(node) { + let { value: sourceValue } = node.source; + + if (path.extname(sourceValue) === '' && sourceValue.indexOf('http') !== 0 && sourceValue.indexOf('./') < 0) { + if (!importMap[sourceValue]) { + // found a _new_ bare import for ${sourceValue} + // we should add this to the importMap and walk its package.json for more transitive deps + importMap[sourceValue] = `/node_modules/${sourceValue}`; + } + + walkPackageJson(path.join(process.cwd(), 'node_modules', sourceValue, 'package.json')); + } else if (sourceValue.indexOf('./') < 0) { + // adding a relative import + importMap[sourceValue] = `/node_modules/${sourceValue}`; + } else { + // walk this module for all its dependencies + sourceValue = sourceValue.indexOf('.js') < 0 + ? `${sourceValue}.js` + : sourceValue; + + if (fs.existsSync(path.join(process.cwd(), 'node_modules', dependency, sourceValue))) { + const moduleContents = fs.readFileSync(path.join(process.cwd(), 'node_modules', dependency, sourceValue)); + walkModule(moduleContents, dependency); + } + } + }, + ExportNamedDeclaration(node) { + const sourceValue = node && node.source ? node.source.value : ''; + + if (sourceValue !== '' && sourceValue.indexOf('.') !== 0 && sourceValue.indexOf('http') !== 0) { + importMap[sourceValue] = `/node_modules/${sourceValue}`; + } + } + }); +}; + +const walkPackageJson = (packageJson = {}) => { + // while walking a package.json we need to find its entry point, e.g. index.js + // and then walk that for import / export statements + // and walk its package.json for its dependencies + + Object.keys(packageJson.dependencies || {}).forEach(dependency => { + const dependencyPackageRootPath = path.join(process.cwd(), './node_modules', dependency); + const dependencyPackageJsonPath = path.join(dependencyPackageRootPath, 'package.json'); + const dependencyPackageJson = require(dependencyPackageJsonPath); + const entry = getPackageEntryPath(dependencyPackageJson); + const isJavascriptPackage = Array.isArray(entry) || typeof entry === 'string' && entry.endsWith('.js') || entry.endsWith('.mjs'); + + if (isJavascriptPackage) { + + // https://nodejs.org/api/packages.html#packages_determining_module_system + if (Array.isArray(entry)) { + // we have an exportMap + const exportMap = entry; + + exportMap.forEach((entry) => { + const exportMapEntry = dependencyPackageJson.exports[entry]; + let packageExport; + + if (Array.isArray(exportMapEntry)) { + let fallbackPath; + let esmPath; + + exportMapEntry.forEach((mapItem) => { + switch (typeof mapItem) { + + case 'string': + fallbackPath = mapItem; + break; + case 'object': + const entryTypes = Object.keys(mapItem); + + if (entryTypes.import) { + esmPath = entryTypes.import; + } else if (entryTypes.require) { + console.error('The package you are importing needs commonjs support. Please use our commonjs plugin to fix this error.'); + fallbackPath = entryTypes.require; + } else if (entryTypes.default) { + console.warn('The package you are requiring may need commonjs support. If this module is not working for you, consider adding our commonjs plugin.'); + fallbackPath = entryTypes.default; + } + break; + default: + console.warn(`Sorry, we were unable to detect the module type for ${mapItem} :(. please consider opening an issue to let us know about your use case.`); + break; + + } + }); + + packageExport = esmPath + ? esmPath + : fallbackPath; + } else if ((exportMapEntry.endsWith('.js') || exportMapEntry.endsWith('.mjs')) && exportMapEntry.indexOf('*') < 0) { + // is not an export array, or package.json, or wildcard + packageExport = exportMapEntry; + } + + if (packageExport) { + importMap[`${dependency}/${entry.replace('./', '')}`] = `/node_modules/${dependency}/${packageExport.replace('./', '')}`; + } + }); + } else { + const packageEntryPointPath = path.join(process.cwd(), './node_modules', dependency, entry); + const packageEntryModule = fs.readFileSync(packageEntryPointPath, 'utf-8'); + + walkModule(packageEntryModule, dependency); + importMap[dependency] = `/node_modules/${dependency}/${entry}`; + walkPackageJson(dependencyPackageJson); + } + } + }); +}; + +class NodeModulesResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + this.extensions = ['*']; + } + + async shouldResolve(url) { + return Promise.resolve(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); + } + }); + } + + async shouldServe(url) { + return Promise.resolve(path.extname(url) === '.mjs' + || (path.extname(url) === '' && fs.existsSync(`${url}.js`)) + || (path.extname(url) === '.js' && (/node_modules/).test(url))); + } + + async serve(url) { + return new Promise(async(resolve, reject) => { + try { + const fullUrl = path.extname(url) === '' + ? `${url}.js` + : url; + // const fullUrl = path.extname(url) === '' + // ? fs.existsSync(`${url}.mjs`) // test for .mjs first + // ? `${url}.mjs` + // : `${url}.js` + // : url; + const body = await fs.promises.readFile(fullUrl); + + resolve({ + body, + contentType: 'text/javascript' + }); + } catch (e) { + reject(e); + } + }); + } + + async shouldIntercept(url, body, headers) { + return Promise.resolve(headers.response['content-type'] === 'text/html'); + } + + async intercept(url, body) { + return new Promise((resolve, reject) => { + try { + const { userWorkspace } = this.compilation.context; + let newContents = body; + + newContents = newContents.replace(/type="module"/g, 'type="module-shim"'); + + 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')) + : {}; + + // walk the project's pacakge.json for all its direct dependencies + // for each entry found in dependencies, find its entry point + // then walk its entry point (e.g. index.js) for imports / exports to add to the importMap + // and then walk its package.json for transitive dependencies and all those import / exports + walkPackageJson(userPackageJson); + + newContents = newContents.replace('', ` + + + + `); + + resolve({ + body: 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-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 new file mode 100644 index 000000000..1c2ee89f2 --- /dev/null +++ b/packages/cli/src/plugins/resource/plugin-standard-css.js @@ -0,0 +1,66 @@ +/* + * + * Manages web standard resource related operations for CSS. + * This is a Greenwood default plugin. + * + */ +const fs = require('fs'); +const cssnano = require('cssnano'); +const path = require('path'); +const postcss = require('postcss'); +const { ResourceInterface } = require('../../lib/resource-interface'); + +class StandardCssResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + this.extensions = ['.css']; + this.contentType = 'text/css'; + } + + async shouldServe(url) { + const isCssFile = path.extname(url) === this.extensions[0]; + + return Promise.resolve(isCssFile); + } + + async serve(url) { + return new Promise(async (resolve, reject) => { + try { + const css = await fs.promises.readFile(url, 'utf-8'); + + resolve({ + body: css, + contentType: this.contentType + }); + } catch (e) { + reject(e); + } + }); + } + + async shouldOptimize(url) { + const isValidCss = path.extname(url) === this.extensions[0] && this.compilation.config.optimization !== 'none'; + + return Promise.resolve(isValidCss); + } + + async optimize(url, body) { + return new Promise(async (resolve, reject) => { + try { + const { outputDir, userWorkspace } = this.compilation.context; + const workspaceUrl = url.replace(outputDir, userWorkspace); + const css = (await postcss([cssnano]).process(body, { from: workspaceUrl })).css; + + resolve(css); + } catch (e) { + reject(e); + } + }); + } +} + +module.exports = { + type: 'resource', + name: 'plugin-standard-css', + provider: (compilation, options) => new StandardCssResource(compilation, options) +}; \ No newline at end of file diff --git a/packages/cli/src/plugins/resource/plugin-standard-font.js b/packages/cli/src/plugins/resource/plugin-standard-font.js new file mode 100644 index 000000000..5632c31b5 --- /dev/null +++ b/packages/cli/src/plugins/resource/plugin-standard-font.js @@ -0,0 +1,38 @@ +/* + * + * 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 StandardFontResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + this.extensions = ['.woff2', '.woff', '.ttf']; + } + + async serve(url) { + return new Promise(async (resolve, reject) => { + try { + const contentType = path.extname(url).replace('.', ''); + const body = await fs.promises.readFile(url); + + resolve({ + body, + contentType + }); + } catch (e) { + reject(e); + } + }); + } +} + +module.exports = { + type: 'resource', + name: 'plugin-standard-font', + provider: (compilation, options) => new StandardFontResource(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 new file mode 100644 index 000000000..74fe515c3 --- /dev/null +++ b/packages/cli/src/plugins/resource/plugin-standard-html.js @@ -0,0 +1,286 @@ +/* + * + * Manages web standard resource related operations for HTML and markdown. + * This is a Greenwood default plugin. + * + */ +const frontmatter = require('front-matter'); +const fs = require('fs'); +const htmlparser = require('node-html-parser'); +const path = require('path'); +const rehypeStringify = require('rehype-stringify'); +const rehypeRaw = require('rehype-raw'); +const remarkFrontmatter = require('remark-frontmatter'); +const remarkParse = require('remark-parse'); +const remarkRehype = require('remark-rehype'); +const { ResourceInterface } = require('../../lib/resource-interface'); +const unified = require('unified'); + +// general refactoring +const getPageTemplate = (barePath, workspace, template) => { + const templatesDir = path.join(workspace, 'templates'); + const pageIsHtmlPath = `${barePath.substring(0, barePath.lastIndexOf('/index'))}.html`; + + if (template && fs.existsSync(`${templatesDir}/${template}.html`)) { + // use a predefined template, usually from markdown frontmatter + contents = fs.readFileSync(`${templatesDir}/${template}.html`, 'utf-8'); + } else if (fs.existsSync(`${barePath}.html`) || fs.existsSync(pageIsHtmlPath)) { + // if the page is already HTML, use that as the template + const indexPath = fs.existsSync(pageIsHtmlPath) + ? pageIsHtmlPath + : `${barePath}.html`; + + contents = fs.readFileSync(indexPath, 'utf-8'); + } else if (fs.existsSync(`${templatesDir}/page.html`)) { + // else look for default page template + contents = fs.readFileSync(`${templatesDir}/page.html`, 'utf-8'); + } 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; +}; + +const getAppTemplate = (contents, userWorkspace) => { + + function sliceTemplate(template, pos, needle, replacer) { + return template.slice(0, pos) + template.slice(pos).replace(needle, replacer); + } + + const appTemplatePath = `${userWorkspace}/templates/app.html`; + let appTemplateContents = contents || ''; + + if (fs.existsSync(appTemplatePath)) { + const root = htmlparser.parse(contents, { + script: true, + style: true, + noscript: true, + pre: true + }); + const body = root.querySelector('body').innerHTML; + const headScripts = root.querySelectorAll('head script'); + const headLinks = root.querySelectorAll('head link'); + const headStyles = root.querySelectorAll('head style'); + + appTemplateContents = fs.readFileSync(appTemplatePath, 'utf-8'); + appTemplateContents = appTemplateContents.replace(/<\/page-outlet>/, body); + + headScripts.forEach((script) => { + const matchNeedle = ''; + const matchPos = appTemplateContents.lastIndexOf(matchNeedle); + + if (script.rawAttrs !== '') { + appTemplateContents = sliceTemplate(appTemplateContents, matchPos, matchNeedle, `\n + \n + `); + } + + if (script.rawAttrs === '') { + appTemplateContents = sliceTemplate(appTemplateContents, matchPos, matchNeedle, `\n + \n + `); + } + }); + + headLinks.forEach((link) => { + const matchNeedle = / + `); + }); + + headStyles.forEach((style) => { + const matchNeedle = ''; + const matchPos = appTemplateContents.lastIndexOf(matchNeedle); + + if (style.rawAttrs === '') { + appTemplateContents = sliceTemplate(appTemplateContents, matchPos, matchNeedle, `\n + \n + `); + } + }); + } + + return appTemplateContents; +}; + +const getUserScripts = (contents) => { + if (process.env.__GWD_COMMAND__ === 'build') { // eslint-disable-line no-underscore-dangle + contents = contents.replace('', ` + + + `); + } + return contents; +}; + +const getMetaContent = (url, config, contents) => { + const title = config.title || ''; + const metaContent = config.meta.map(item => { + let metaHtml = ''; + + 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'); + + contents = contents.replace(/(.*)<\/title>/, ''); + contents = contents.replace('<head>', `<head><title>${title}`); + contents = contents.replace('', metaContent); + + return contents; +}; + +class StandardHtmlResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + + this.extensions = ['.html', '.md']; + this.contentType = 'text/html'; + } + + getRelativeUserworkspaceUrl(url) { + return url.replace(this.compilation.context.userWorkspace, ''); + } + + async shouldServe(url) { + const { userWorkspace } = this.compilation.context; + const relativeUrl = this.getRelativeUserworkspaceUrl(url); + const barePath = relativeUrl.endsWith('/') + ? `${userWorkspace}/pages${relativeUrl}index` + : `${userWorkspace}/pages${relativeUrl.replace('.html', '')}`; + + return Promise.resolve((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) { + return new Promise(async (resolve, reject) => { + try { + const config = Object.assign({}, this.compilation.config); + const { userWorkspace } = this.compilation.context; + const normalizedUrl = this.getRelativeUserworkspaceUrl(url); + let body = ''; + let template = null; + let processedMarkdown = null; + const barePath = normalizedUrl.endsWith('/') + ? `${userWorkspace}/pages${normalizedUrl}index` + : `${userWorkspace}/pages${normalizedUrl.replace('.html', '')}`; + const isMarkdownContent = fs.existsSync(`${barePath}.md`) + || fs.existsSync(`${barePath.substring(0, barePath.lastIndexOf('/index'))}.md`) + || fs.existsSync(`${barePath.replace('/index', '.md')}`); + + if (isMarkdownContent) { + const markdownPath = fs.existsSync(`${barePath}.md`) + ? `${barePath}.md` + : fs.existsSync(`${barePath.substring(0, barePath.lastIndexOf('/index'))}.md`) + ? `${barePath.substring(0, barePath.lastIndexOf('/index'))}.md` + : `${userWorkspace}/pages${url.replace('/index.html', '.md')}`; + const markdownContents = await fs.promises.readFile(markdownPath, 'utf-8'); + const rehypePlugins = []; + const remarkPlugins = []; + + config.markdown.plugins.forEach(plugin => { + if (plugin.indexOf('rehype-') >= 0) { + rehypePlugins.push(require(plugin)); + } + + if (plugin.indexOf('remark-') >= 0) { + remarkPlugins.push(require(plugin)); + } + }); + + const settings = config.markdown.settings || {}; + const fm = frontmatter(markdownContents); + processedMarkdown = await unified() + .use(remarkParse, settings) // parse markdown into AST + .use(remarkFrontmatter) // extract frontmatter from AST + .use(remarkPlugins) // apply userland remark plugins + .use(remarkRehype, { allowDangerousHtml: true }) // convert from markdown to HTML AST + .use(rehypeRaw) // support mixed HTML in markdown + .use(rehypePlugins) // apply userland rehype plugins + .use(rehypeStringify) // convert AST to HTML string + .process(markdownContents); + + // configure via frontmatter + if (fm.attributes) { + const { attributes } = fm; + + if (attributes.title) { + config.title = `${config.title} - ${attributes.title}`; + } + + if (attributes.template) { + template = attributes.template; + } + } + } + + body = getPageTemplate(barePath, userWorkspace, template); + body = getAppTemplate(body, userWorkspace); + body = getUserScripts(body); + body = getMetaContent(normalizedUrl, config, body); + + if (processedMarkdown) { + body = body.replace(/\(.*)<\/content-outlet>/s, processedMarkdown.contents); + } + + resolve({ + body, + contentType: this.contentType + }); + } catch (e) { + reject(e); + } + }); + } + + async shouldOptimize(url) { + return Promise.resolve(path.extname(url) === '.html'); + } + + async optimize(url, body) { + return new Promise((resolve, reject) => { + try { + body = body.replace(/ + + `); + + resolve({ body: contents }); + } catch (e) { + reject(e); + } + }); + } +} + +module.exports = (options = {}) => { + return [{ + type: 'server', + name: 'plugin-live-reload:server', + provider: (compilation) => new LiveReloadServer(compilation, options) + }, { + type: 'resource', + name: 'plugin-live-reload:resource', + provider: (compilation) => new LiveReloadResource(compilation, options) + }]; +}; \ No newline at end of file diff --git a/packages/cli/src/tasks/build.js b/packages/cli/src/tasks/build.js deleted file mode 100644 index 24932ec73..000000000 --- a/packages/cli/src/tasks/build.js +++ /dev/null @@ -1,42 +0,0 @@ -const webpack = require('webpack'); -const serializeBuild = require('../lifecycles/serialize'); - -module.exports = runProductionBuild = async(compilation) => { - return new Promise(async (resolve, reject) => { - - try { - console.log('Building SPA from compilation...'); - await runWebpack(compilation); - await serializeBuild(compilation); - - resolve(); - } catch (err) { - reject(err); - } - }); -}; - -// eslint-disable-next-line no-unused-vars -const runWebpack = async (compilation) => { - const webpackConfig = require(compilation.context.webpackProd)(compilation); - - return new Promise(async (resolve, reject) => { - - try { - return webpack(webpackConfig, (err, stats) => { - if (err || stats.hasErrors()) { - if (stats && stats.hasErrors()) { - err = stats.toJson('minimal').errors[0]; - } - reject(err); - } else { - console.log('webpack build complete'); - resolve(); - } - }); - } catch (err) { - reject(err); - } - - }); -}; \ No newline at end of file diff --git a/packages/cli/src/tasks/develop.js b/packages/cli/src/tasks/develop.js deleted file mode 100644 index dd5e07b75..000000000 --- a/packages/cli/src/tasks/develop.js +++ /dev/null @@ -1,25 +0,0 @@ -const dataServer = require('../data/server'); -const webpack = require('webpack'); -const WebpackDevServer = require('webpack-dev-server'); - -module.exports = runDevServer = async (compilation) => { - return new Promise(async (resolve, reject) => { - - try { - await dataServer(compilation).listen().then((server) => { - console.log(`dataServer started at ${server.url}`); - }); - - const webpackConfig = require(compilation.context.webpackDevelop)(compilation); - const devServerConfig = webpackConfig.devServer; - - let compiler = webpack(webpackConfig); - let webpackServer = new WebpackDevServer(compiler, devServerConfig); - - webpackServer.listen(devServerConfig.port); - } catch (err) { - reject(err); - } - - }); -}; \ No newline at end of file diff --git a/packages/cli/src/tasks/eject.js b/packages/cli/src/tasks/eject.js deleted file mode 100644 index c324856d5..000000000 --- a/packages/cli/src/tasks/eject.js +++ /dev/null @@ -1,26 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -module.exports = ejectConfigFiles = async (copyAllFiles) => { - return new Promise(async (resolve, reject) => { - try { - - if (copyAllFiles) { - configFilePaths = fs.readdirSync(path.join(__dirname, '../config')); - } else { - configFilePaths = [ - 'webpack.config.common.js', - 'webpack.config.develop.js', - 'webpack.config.prod.js' - ]; - } - configFilePaths.forEach(configFile => { - fs.copyFileSync(path.join(__dirname, '../config', configFile), path.join(process.cwd(), configFile)); - console.log(`Ejecting ${configFile}`.blue); - }); - resolve(); - } catch (err) { - reject(err); - } - }); -}; \ No newline at end of file diff --git a/packages/cli/src/templates/404.html b/packages/cli/src/templates/404.html deleted file mode 100644 index 983391817..000000000 --- a/packages/cli/src/templates/404.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - 404 - Not Found - - <%= htmlWebpackPlugin.options.hookSpaIndexFallback %> - - - - -

404 Not Found

- - \ No newline at end of file diff --git a/packages/cli/src/templates/app-template.js b/packages/cli/src/templates/app-template.js deleted file mode 100644 index f26096e26..000000000 --- a/packages/cli/src/templates/app-template.js +++ /dev/null @@ -1,13 +0,0 @@ -import { html, LitElement } from 'lit-element'; - -class AppComponent extends LitElement { - - render() { - return html` - -

404 Not found

- `; - } -} - -customElements.define('eve-app', AppComponent); diff --git a/packages/cli/src/templates/index.html b/packages/cli/src/templates/app.html similarity index 64% rename from packages/cli/src/templates/index.html rename to packages/cli/src/templates/app.html index 841dd7a8c..3366aee89 100644 --- a/packages/cli/src/templates/index.html +++ b/packages/cli/src/templates/app.html @@ -2,25 +2,17 @@ - - - <%= htmlWebpackPlugin.options.hookGreenwoodPolyfills %> - - <%= htmlWebpackPlugin.options.hookGreenwoodAnalytics %> - - <%= htmlWebpackPlugin.options.hookGreenwoodSpaIndexFallback %> - + - - - - +
+

Welcome to my website!

+ +
- - \ No newline at end of file + \ No newline at end of file diff --git a/packages/cli/src/templates/base-template.js b/packages/cli/src/templates/base-template.js deleted file mode 100644 index 2028a48a0..000000000 --- a/packages/cli/src/templates/base-template.js +++ /dev/null @@ -1,104 +0,0 @@ -import { html, LitElement } from 'lit-element'; -import { connectRouter } from 'lit-redux-router'; -import { applyMiddleware, createStore, compose as origCompose, combineReducers } from 'redux'; -import { lazyReducerEnhancer } from 'pwa-helpers/lazy-reducer-enhancer.js'; -import thunk from 'redux-thunk'; -import client from '@greenwood/cli/data/client'; -import ConfigQuery from '@greenwood/cli/data/queries/config'; -import GraphQuery from '@greenwood/cli/data/queries/graph'; -import '@greenwood/cli/templates/app-template'; -// eslint-disable-next-line no-underscore-dangle -const compose = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || origCompose; - -const store = createStore((state) => state, - compose(lazyReducerEnhancer(combineReducers), applyMiddleware(thunk)) -); - -import '../index/index'; - -connectRouter(store); - -class BaseAppComponent extends LitElement { - - async connectedCallback() { - super.connectedCallback(); - const route = window.location.pathname; - const response = await Promise.all([ - await client.query({ - query: ConfigQuery - }), - await client.query({ - query: GraphQuery - }) - ]); - const { config } = response[0].data; - const currentPage = response[1].data.graph.filter((page) => { - return route === page.link; - })[0]; - - const currentPageTitleSuffix = !currentPage || currentPage.link === '/' - ? '' - : ` - ${currentPage.title}`; - const fullTitle = `${config.title}${currentPageTitleSuffix}`; - - this.setDocumentTitle(fullTitle); - this.setMeta(config.meta, currentPage); - } - - setDocumentTitle(title = 'test') { - const head = document.head; - const titleElement = head.getElementsByTagName('title')[0]; - - titleElement.innerHTML = title; - } - - setMeta(meta = [], currentPage = {}) { - let header = document.head; - - meta.forEach(metaItem => { - const metaType = metaItem.rel // type of meta - ? 'rel' - : metaItem.name - ? 'name' - : 'property'; - const metaTypeValue = metaItem[metaType]; // value of the meta - let meta = document.createElement('meta'); - - if (metaType === 'rel') { - // change to a tag instead - meta = document.createElement('link'); - - meta.setAttribute('rel', metaTypeValue); - meta.setAttribute('href', metaItem.href); - } else { - const metaContent = metaItem.property === 'og:url' - ? `${metaItem.content}${currentPage.link}` - : metaItem.content; - - meta.setAttribute(metaType, metaItem[metaType]); - meta.setAttribute('content', metaContent); - } - - const oldmeta = header.querySelector(`[${metaType}="${metaTypeValue}"]`); - - // rehydration - if (oldmeta) { - header.replaceChild(meta, oldmeta); - } else { - header.appendChild(meta); - } - }); - } - - createRenderRoot() { - return this; - } - - render() { - return html` - - `; - } -} - -customElements.define('app-root', BaseAppComponent); \ No newline at end of file diff --git a/packages/cli/src/templates/index.md b/packages/cli/src/templates/index.md deleted file mode 100644 index 35a27e304..000000000 --- a/packages/cli/src/templates/index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -label: 'index' ---- - -### 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/packages/cli/src/templates/page-template.js b/packages/cli/src/templates/page-template.js deleted file mode 100644 index 894fa40e0..000000000 --- a/packages/cli/src/templates/page-template.js +++ /dev/null @@ -1,15 +0,0 @@ -import { html, LitElement } from 'lit-element'; - -class PageTemplate extends LitElement { - render() { - return html` -
-
- -
-
- `; - } -} - -customElements.define('page-template', PageTemplate); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.babel/babel.config.js b/packages/cli/test/cases/build.config.babel/babel.config.js deleted file mode 100644 index aa5d5753c..000000000 --- a/packages/cli/test/cases/build.config.babel/babel.config.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - sourceType: 'unambiguous', - ignore: [/[\/\\]core-js/, /@babel[\/\\]runtime/], - presets: [ - [ - '@babel/preset-env', - { - useBuiltIns: 'entry', - corejs: { - version: 3, - proposals: true - }, - configPath: __dirname - } - ] - ], - - // Here we've deliberately removed plugins so that it will compile but won't compile correctly which we can test for - plugins: [] -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.babel/build.config.babel.spec.js b/packages/cli/test/cases/build.config.babel/build.config.babel.spec.js deleted file mode 100644 index 21c3301c0..000000000 --- a/packages/cli/test/cases/build.config.babel/build.config.babel.spec.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Use Case - * Run Greenwood with a custom babel config - * - * User Result - * Should generate and fail to build a bare bones greenwood application using a purpose built babel config - * - * User Command - * greenwood build - * - * User Workspace - * Greenwood default - * src/ - * pages/ - * hello.md - * index.md - */ -const path = require('path'); -const { JSDOM } = require('jsdom'); -const expect = require('chai').expect; -const TestBed = require('../../../../../test/test-bed'); - -describe('Build Greenwood With: ', function() { - const LABEL = 'Custom babel configuration'; - 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('index page should not compile properly', function() { - let dom; - - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); - }); - - it('should not contain any components within app-root', function() { - // prove that our custom broken babel config is being used - const outlet = dom.window.document.querySelector('body > app-root').innerHTML; - expect(outlet).to.equal(''); - }); - - }); - }); - - after(function() { - setup.teardownTestBed(); - }); - -}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.babel/src/pages/index.md b/packages/cli/test/cases/build.config.babel/src/pages/index.md deleted file mode 100644 index 1c1a50fbb..000000000 --- a/packages/cli/test/cases/build.config.babel/src/pages/index.md +++ /dev/null @@ -1,3 +0,0 @@ -### 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/packages/cli/test/cases/build.config.default/build.config.default.spec.js b/packages/cli/test/cases/build.config.default/build.config.default.spec.js index 48e9ae025..7445bb1ea 100644 --- a/packages/cli/test/cases/build.config.default/build.config.default.spec.js +++ b/packages/cli/test/cases/build.config.default/build.config.default.spec.js @@ -18,7 +18,7 @@ const runSmokeTest = require('../../../../../test/smoke-test'); const TestBed = require('../../../../../test/test-bed'); describe('Build Greenwood With: ', function() { - const LABEL = 'Empty Configuration and Default Workspace'; + const LABEL = 'Empty User Configuration and No Workspace'; let setup; before(async function() { @@ -30,7 +30,8 @@ describe('Build Greenwood With: ', function() { before(async function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'index', 'not-found', 'hello'], LABEL); + + runSmokeTest(['public', 'index'], LABEL); }); after(function() { 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 3997d341a..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 @@ -27,12 +27,12 @@ describe('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-theme-file/build.config.error-theme-file.spec.js b/packages/cli/test/cases/build.config.error-optimization/build.config.error-optimization.spec.js similarity index 58% rename from packages/cli/test/cases/build.config.error-theme-file/build.config.error-theme-file.spec.js rename to packages/cli/test/cases/build.config.error-optimization/build.config.error-optimization.spec.js index aa658c0c4..cfb4dbbe5 100644 --- a/packages/cli/test/cases/build.config.error-theme-file/build.config.error-theme-file.spec.js +++ b/packages/cli/test/cases/build.config.error-optimization/build.config.error-optimization.spec.js @@ -1,6 +1,6 @@ /* * Use Case - * Run Greenwood build command with a bad value for themeFile 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 * { - * themeFile: '{}' + * optimization: 'lorumipsum' * } * * User Workspace @@ -27,12 +27,12 @@ describe('Build Greenwood With: ', function() { await setup.setupTestBed(__dirname); }); - describe('Custom Configuration with a bad value for theme file', function() { - it('should throw an error that themeFile must be a filename', async function() { + 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: greenwood.config.js themeFile must be a valid filename. got {} instead.'); + expect(err).to.contain('Error: provided optimization "loremipsum" is not supported. Please use one of: default, none, static, inline.'); } }); }); 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.error-theme-file/greenwood.config.js b/packages/cli/test/cases/build.config.error-theme-file/greenwood.config.js deleted file mode 100644 index 49b96e59a..000000000 --- a/packages/cli/test/cases/build.config.error-theme-file/greenwood.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - themeFile: '{}' -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.markdown-custom.plugins/build.config.markdown-custom.spec.js b/packages/cli/test/cases/build.config.markdown-custom.plugins/build.config.markdown-custom.spec.js index 96b60be95..831473367 100644 --- a/packages/cli/test/cases/build.config.markdown-custom.plugins/build.config.markdown-custom.spec.js +++ b/packages/cli/test/cases/build.config.markdown-custom.plugins/build.config.markdown-custom.spec.js @@ -11,8 +11,9 @@ * User Config * markdown: { * plugins: [ - * require('rehype-slug'), - * require('rehype-autolink-headings') + * '@mapbox/rehype-prism', + * 'rehype-slug', + * 'rehype-autolink-headings' * ] * } * @@ -40,20 +41,37 @@ describe('Build Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'index', 'not-found'], LABEL); + runSmokeTest(['public', 'index'], LABEL); - describe('Custom Markdown Presets', function() { + describe('Custom Markdown Plugins', function() { let dom; before(async function() { dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); }); + it('should use our custom rehype plugin to add syntax highlighting', function() { + let pre = dom.window.document.querySelectorAll('body pre'); + let code = dom.window.document.querySelectorAll('body pre code'); + + expect(pre.length).to.equal(1); + expect(pre[0].getAttribute('class')).to.equal('language-js'); + + expect(code.length).to.equal(1); + expect(code[0].getAttribute('class')).to.equal('language-js'); + }); + it('should use our custom markdown preset rehype-autolink-headings and rehype-slug plugins', function() { - let heading = dom.window.document.querySelector('h3 > a'); - expect(heading.getAttribute('href')).to.equal('#greenwood'); + let heading = dom.window.document.querySelector('h1 > a'); + + expect(heading.getAttribute('href')).to.equal('#greenwood-markdown-syntax-highlighting-test'); }); + it('should use our custom markdown preset rremark-TBD plugins', function() { + let heading = dom.window.document.querySelector('h3 > a'); + + expect(heading.getAttribute('href')).to.equal('#lower-heading-test'); + }); }); }); diff --git a/packages/cli/test/cases/build.config.markdown-custom.plugins/greenwood.config.js b/packages/cli/test/cases/build.config.markdown-custom.plugins/greenwood.config.js index f91df519d..9d7d9c31b 100644 --- a/packages/cli/test/cases/build.config.markdown-custom.plugins/greenwood.config.js +++ b/packages/cli/test/cases/build.config.markdown-custom.plugins/greenwood.config.js @@ -2,8 +2,9 @@ module.exports = { markdown: { settings: {}, plugins: [ - require('rehype-slug'), - require('rehype-autolink-headings') + '@mapbox/rehype-prism', + 'rehype-slug', + 'rehype-autolink-headings' ] } }; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.markdown-custom.plugins/src/pages/index.md b/packages/cli/test/cases/build.config.markdown-custom.plugins/src/pages/index.md new file mode 100644 index 000000000..fbdd5bb80 --- /dev/null +++ b/packages/cli/test/cases/build.config.markdown-custom.plugins/src/pages/index.md @@ -0,0 +1,9 @@ +# Greenwood Markdown Syntax Highlighting Test + +### Lower Heading Test + +This is some markdown being rendered by Greenwood. + +```js +console.log('hello world'); +``` \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.markdown-custom.settings/build.config.markdown-custom.settings.spec.js b/packages/cli/test/cases/build.config.markdown-custom.settings/build.config.markdown-custom.settings.spec.js index b204eb690..eaffca661 100644 --- a/packages/cli/test/cases/build.config.markdown-custom.settings/build.config.markdown-custom.settings.spec.js +++ b/packages/cli/test/cases/build.config.markdown-custom.settings/build.config.markdown-custom.settings.spec.js @@ -18,7 +18,7 @@ * pages/ * index.md */ -const fs = require('fs'); +const { JSDOM } = require('jsdom'); const path = require('path'); const expect = require('chai').expect; const TestBed = require('../../../../../test/test-bed'); @@ -34,15 +34,23 @@ describe('Build Greenwood With: ', function() { }); describe(LABEL, function() { + + before(async function() { + await setup.runGreenwoodCommand('build'); + }); + describe('Custom Markdown Presets', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); // gfm: false disables things like fenced code blocks https://www.npmjs.com/package/remark-parse#optionsgfm - it('should intentionally fail to compile using our custom markdown preset settings', async function() { - try { - await setup.runGreenwoodCommand('build'); - } catch (err) { - expect(fs.existsSync(path.join(this.context.publicDir, './index.html'))).to.be.false; - } + it('should intentionally fail to compile code fencing using our custom markdown preset settings', async function() { + let pre = dom.window.document.querySelector('pre > code'); + + expect(pre).to.equal(null); }); }); diff --git a/packages/cli/test/cases/build.config.meta/build.config.meta.spec.js b/packages/cli/test/cases/build.config.meta/build.config.meta.spec.js index 1777cfec4..ff99f4bc1 100644 --- a/packages/cli/test/cases/build.config.meta/build.config.meta.spec.js +++ b/packages/cli/test/cases/build.config.meta/build.config.meta.spec.js @@ -37,8 +37,6 @@ const expect = require('chai').expect; const runSmokeTest = require('../../../../../test/smoke-test'); const TestBed = require('../../../../../test/test-bed'); -const mainBundleScriptRegex = /index.*.bundle\.js/; - describe('Build Greenwood With: ', function() { const LABEL = 'Custom Meta Configuration and Nested Workspace'; const meta = greenwoodConfig.meta; @@ -62,73 +60,19 @@ describe('Build Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'not-found', 'hello'], LABEL); + runSmokeTest(['public', 'index'], LABEL); - // hardcoding index smoke test here because of the nested route describe('Index (home) page with custom meta data', function() { let dom; - beforeEach(async function() { + before(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 a tag in the <head>', function() { const title = dom.window.document.querySelector('head title').textContent; - expect(title).to.be.equal('My Custom Greenwood App'); - }); - - it('should have one <script> tag in the <body> for the main bundle', function() { - const scriptTags = dom.window.document.querySelectorAll('body > script'); - const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src.replace('file:///', ''); - - return mainBundleScriptRegex.test(src); - }); - - expect(bundledScript.length).to.be.equal(1); - }); - - it('should have one <script> tag in the <body> for the main bundle loaded with async', function() { - const scriptTags = dom.window.document.querySelectorAll('body > script'); - const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src.replace('file:///', ''); - - return mainBundleScriptRegex.test(src); - }); - - expect(bundledScript[0].getAttribute('async')).to.be.equal(''); - }); - - it('should have one <script> tag for Apollo state', function() { - const scriptTags = dom.window.document.querySelectorAll('script'); - const bundleScripts = Array.prototype.slice.call(scriptTags).filter(script => { - return script.getAttribute('data-state') === 'apollo'; - }); - - expect(bundleScripts.length).to.be.equal(1); - }); - - it('should have only one <script> tag in the <head>', function() { - const scriptTags = dom.window.document.querySelectorAll('head > script'); - - expect(scriptTags.length).to.be.equal(1); - }); - - it('should have a router outlet tag in the <body>', function() { - const outlet = dom.window.document.querySelectorAll('body eve-app'); - - expect(outlet.length).to.be.equal(1); - }); - - it('should have the correct route tags in the <body>', function() { - const routes = dom.window.document.querySelectorAll('body lit-route'); - - expect(routes.length).to.be.equal(4); + expect(title).to.be.equal(greenwoodConfig.title); }); it('should have the expected heading text within the index page in the public directory', function() { @@ -156,7 +100,7 @@ describe('Build Greenwood With: ', function() { const ogUrlMeta = metaFilter('og:url'); const metaElement = dom.window.document.querySelector(`head meta[property="${ogUrlMeta.property}"]`); - expect(metaElement.getAttribute('content')).to.be.equal(`${ogUrlMeta.content}/`); + expect(metaElement.getAttribute('content')).to.be.equal(ogUrlMeta.content); }); it('should have a <meta> tag with custom twitter:site content in the <head>', function() { @@ -170,7 +114,7 @@ describe('Build Greenwood With: ', function() { describe('Nested About page meta data', function() { let dom; - beforeEach(async function() { + before(async function() { dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'about', './index.html')); }); @@ -203,7 +147,7 @@ describe('Build Greenwood With: ', function() { describe('favicon', function() { let dom; - beforeEach(async function() { + before(async function() { dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); }); 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 <script> tag in the <head> for the router', function() { + const scriptTags = dom.window.document.querySelectorAll('head > script[type]'); + + expect(scriptTags.length).to.be.equal(1); + expect(scriptTags[0].href).to.be.contain(/router.*.js/); + expect(scriptTags[0].type).to.be.equal('module'); + }); + + it('should have one router.js file in the output directory', function() { + const routerJsFiles = fs.readdirSync(this.context.publicDir).filter(file => file.indexOf('router') === 0); + + expect(routerJsFiles.length).to.be.equal(1); + }); + + it('should have one expected inline <script> tag in the <head> for router global variables', function() { + const inlineScriptTags = Array.from(dom.window.document.querySelectorAll('head > script')) + .filter(tag => !tag.type); + + expect(inlineScriptTags.length).to.be.equal(1); + expect(inlineScriptTags[0].textContent).to.contain('window.__greenwood = window.__greenwood || {};'); + expect(inlineScriptTags[0].textContent).to.contain('window.__greenwood.currentTemplate = "page"'); + }); + + it('should have one <router-outlet> tag in the <body> for the content', function() { + const routerOutlets = dom.window.document.querySelectorAll('body > router-outlet'); + + expect(routerOutlets.length).to.be.equal(1); + }); + + it('should have two <greenwood-route> tags in the <body> for the content', function() { + const routeTags = dom.window.document.querySelectorAll('body > greenwood-route'); + + expect(routeTags.length).to.be.equal(2); + }); + + it('should have the expected properties for each <greenwood-route> tag for the about page', function() { + const aboutRouteTag = Array + .from(dom.window.document.querySelectorAll('body > greenwood-route')) + .filter(tag => tag.dataset.route === '/about/'); + const dataset = aboutRouteTag[0].dataset; + + expect(aboutRouteTag.length).to.be.equal(1); + expect(dataset.template).to.be.equal('test'); + expect(dataset.key).to.be.equal('/_routes/about/index.html'); + }); + + it('should have the expected properties for each <greenwood-route> tag for the home page', function() { + const aboutRouteTag = Array + .from(dom.window.document.querySelectorAll('body > greenwood-route')) + .filter(tag => tag.dataset.route === '/'); + const dataset = aboutRouteTag[0].dataset; + + expect(aboutRouteTag.length).to.be.equal(1); + expect(dataset.template).to.be.equal('page'); + expect(dataset.key).to.be.equal('/_routes/index.html'); + }); + + it('should have the expected number of _route partials in the output directory for each page', function() { + expect(partials.length).to.be.equal(2); + }); + + it('should have the expected partial output to match the contents of the home page in the <router-outlet> tag in the <body>', function() { + const aboutPartial = fs.readFileSync(path.join(this.context.publicDir, '_routes/about/index.html'), 'utf-8'); + const aboutRouterOutlet = aboutDom.window.document.querySelectorAll('body > router-outlet')[0]; + + expect(aboutRouterOutlet.innerHTML).to.contain(aboutPartial); + }); + + it('should have the expected partial output to match the contents of the about page in the <router-outlet> tag in the <body>', function() { + const homePartial = fs.readFileSync(path.join(this.context.publicDir, '_routes/index.html'), 'utf-8'); + const homeRouterOutlet = dom.window.document.querySelectorAll('body > router-outlet')[0]; + + expect(homeRouterOutlet.innerHTML).to.contain(homePartial); + }); + + }); + + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.error-public-path/greenwood.config.js b/packages/cli/test/cases/build.config.mode-mpa/greenwood.config.js similarity index 53% rename from packages/cli/test/cases/build.config.error-public-path/greenwood.config.js rename to packages/cli/test/cases/build.config.mode-mpa/greenwood.config.js index 6cf29c5bd..646368dd0 100644 --- a/packages/cli/test/cases/build.config.error-public-path/greenwood.config.js +++ b/packages/cli/test/cases/build.config.mode-mpa/greenwood.config.js @@ -1,3 +1,3 @@ module.exports = { - publicPath: 123 + mode: 'mpa' }; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.mode-mpa/src/pages/about.md b/packages/cli/test/cases/build.config.mode-mpa/src/pages/about.md new file mode 100644 index 000000000..b90299ace --- /dev/null +++ b/packages/cli/test/cases/build.config.mode-mpa/src/pages/about.md @@ -0,0 +1,7 @@ +--- +template: test +--- + +### Greenwood + +This is the about page built by Greenwood. \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.mode-mpa/src/pages/index.md b/packages/cli/test/cases/build.config.mode-mpa/src/pages/index.md new file mode 100644 index 000000000..b17b38a67 --- /dev/null +++ b/packages/cli/test/cases/build.config.mode-mpa/src/pages/index.md @@ -0,0 +1,3 @@ +### Greenwood + +This is the home page built by Greenwood. \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.mode/build.config.mode.spec.js b/packages/cli/test/cases/build.config.mode/build.config.mode.spec.js deleted file mode 100644 index 88085b2b3..000000000 --- a/packages/cli/test/cases/build.config.mode/build.config.mode.spec.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Use Case - * Run Greenwood with optimization setting in Greenwood config set to strict. - * - * User Result - * Should generate a bare bones Greenwood build with bundle JavaScript and routes. - * - * User Command - * greenwood build - * - * User Config - * { - * optimization: 'spa' - * } - * - * User Workspace - * Greenwood default w/ nested page - * src/ - * pages/ - * about/ - * index.md - * hello.md - * index.md - */ -const { JSDOM } = require('jsdom'); -const path = require('path'); -const expect = require('chai').expect; -const runSmokeTest = require('../../../../../test/smoke-test'); -const TestBed = require('../../../../../test/test-bed'); - -describe('Build Greenwood With: ', function() { - const LABEL = 'Custom Mode'; - 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', 'not-found', 'hello'], LABEL); - - describe('Strict', function() { - let dom; - - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); - }); - - it('should have no <script> tag in the <body>', function() { - const scriptTags = dom.window.document.querySelectorAll('body > script'); - - expect(scriptTags.length).to.be.equal(0); - }); - - it('should have no <script> tags in the <head>', function() { - const scriptTags = dom.window.document.querySelectorAll('head > script'); - - expect(scriptTags.length).to.be.equal(0); - }); - - it('should have no <script> tags for Apollo state', function() { - const scriptTags = dom.window.document.querySelectorAll('script'); - const bundleScripts = Array.prototype.slice.call(scriptTags).filter(script => { - return script.getAttribute('data-state') === 'apollo'; - }); - - expect(bundleScripts.length).to.be.equal(0); - }); - - it('should have a router outlet tag in the <body>', function() { - const outlet = dom.window.document.querySelectorAll('body eve-app'); - - expect(outlet.length).to.be.equal(1); - }); - - it('should have only 2 route tags in the <body>', function() { - const routes = dom.window.document.querySelectorAll('body lit-route'); - - expect(routes.length).to.be.equal(2); - }); - }); - - }); - - after(function() { - setup.teardownTestBed(); - }); - -}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.mode/greenwood.config.js b/packages/cli/test/cases/build.config.mode/greenwood.config.js deleted file mode 100644 index d7becfa7a..000000000 --- a/packages/cli/test/cases/build.config.mode/greenwood.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - optimization: 'strict' -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.mode/src/pages/about/index.md b/packages/cli/test/cases/build.config.mode/src/pages/about/index.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/cli/test/cases/build.config.mode/src/pages/hello.md b/packages/cli/test/cases/build.config.mode/src/pages/hello.md deleted file mode 100644 index fb8cf32d5..000000000 --- a/packages/cli/test/cases/build.config.mode/src/pages/hello.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -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/packages/cli/test/cases/build.config.mode/src/pages/index.md b/packages/cli/test/cases/build.config.mode/src/pages/index.md deleted file mode 100644 index 1c1a50fbb..000000000 --- a/packages/cli/test/cases/build.config.mode/src/pages/index.md +++ /dev/null @@ -1,3 +0,0 @@ -### 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/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js b/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js new file mode 100644 index 000000000..12f459a87 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js @@ -0,0 +1,107 @@ +/* + * Use Case + * Run Greenwood build command with default setting for optimization + * + * User Result + * Should generate a Greenwood build that preloads all <script> and <link> tags + * + * User Command + * greenwood build + * + * Default Config + * + * Custom Workspace + * src/ + * components/ + * header.js + * pages/ + * index.html + * styles/ + * theme.css + */ +const expect = require('chai').expect; +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 = 'Default Optimization Configuration'; + 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('Output for JavaScript / CSS tags and files', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); + }); + + describe('<script> tag and preloading', function() { + it('should contain one javasccript file in the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, '*.js'))).to.have.lengthOf(1); + }); + + it('should have the expected <script> tag in the <head>', function() { + const scriptTags = dom.window.document.querySelectorAll('head script'); + + expect(scriptTags.length).to.be.equal(1); + }); + + it('should have the expect modulepreload <link> tag for the same <script> tag src in the <head>', function() { + const preloadScriptTags = Array + .from(dom.window.document.querySelectorAll('head link[rel="modulepreload"]')) + .filter(link => link.getAttribute('as') === 'script'); + + expect(preloadScriptTags.length).to.be.equal(1); + expect(preloadScriptTags[0].href).to.match(/header.*.js/); + }); + + it('should contain the expected content from <app-header> in the <body>', function() { + const header = dom.window.document.querySelectorAll('body header'); + + expect(header.length).to.be.equal(1); + expect(header[0].textContent).to.be.equal('This is the header component.'); + }); + }); + + describe('<link> tag and preloading', function() { + it('should contain one style.css in the output directory', async function() { + expect(await glob.promise(`${path.join(this.context.publicDir, 'styles')}/theme.*.css`)).to.have.lengthOf(1); + }); + + it('should have the expected <link> tag in the <head>', function() { + const linkTags = Array + .from(dom.window.document.querySelectorAll('head link[rel="preload"]')) + .filter(tag => tag.getAttribute('as') === 'style'); + + expect(linkTags.length).to.be.equal(1); + }); + + it('should have the expect preload <link> tag for the same <link> tag href in the <head>', function() { + const preloadLinkTags = Array + .from(dom.window.document.querySelectorAll('head link[rel="preload"]')) + .filter(link => link.getAttribute('as') === 'style'); + + expect(preloadLinkTags.length).to.be.equal(1); + expect(preloadLinkTags[0].href).to.match(/\/styles\/theme.*.css/); + expect(preloadLinkTags[0].getAttribute('crossorigin')).to.equal('anonymous'); + }); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-default/src/components/header.js b/packages/cli/test/cases/build.config.optimization-default/src/components/header.js new file mode 100644 index 000000000..61ee46391 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-default/src/components/header.js @@ -0,0 +1,19 @@ +class HeaderComponent extends HTMLElement { + constructor() { + super(); + + this.root = this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.root.innerHTML = this.getTemplate(); + } + + getTemplate() { + return ` + <header>This is the header component.</header> + `; + } +} + +customElements.define('app-header', HeaderComponent); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-default/src/pages/index.html b/packages/cli/test/cases/build.config.optimization-default/src/pages/index.html new file mode 100644 index 000000000..57ef25887 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-default/src/pages/index.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/components/header.js"></script> + <link rel="stylesheet" href="/styles/theme.css"></link> + </head> + + <body> + + <div class="gwd-content-outlet"> + <div> + <app-header></app-header> + </div> + </div> + + </body> + +</html> \ 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 <script> and <link> tags. + * + * User Command + * greenwood build + * + * User Config + * { + * optimization: 'inline' + * } + * + * Custom Workspace + * src/ + * components/ + * header.js + * pages/ + * index.html + * styles/ + * theme.css + */ +const expect = require('chai').expect; +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 = 'Inline Optimization Configuration'; + 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('Output for JavaScript / CSS tags and files', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); + }); + + it('should contain no link <tags> in the <head> tag', function() { + const linkTags = dom.window.document.querySelectorAll('head link'); + + expect(linkTags.length).to.be.equal(0); + }); + + describe('<script> tag and files', function() { + it('should contain one <script> tags in the <head>', function() { + const allScriptTags = dom.window.document.querySelectorAll('head script'); + + expect(allScriptTags.length).to.be.equal(1); + }); + + it('should contain no <script> tags in the <head> with a src', function() { + const allSrcScriptTags = dom.window.document.querySelectorAll('head script[src]'); + + expect(allSrcScriptTags.length).to.be.equal(0); + }); + + xit('should contain no Javascript files in the output directory', async function() { + const jsFiles = await glob.promise(`${this.context.publicDir}**/**/*.js`); + + expect(jsFiles).to.have.lengthOf(0); + }); + + it('should contain one <script> tag with the expected JS content inlined of type="module"', function() { + const scriptTag = dom.window.document.querySelectorAll('head script')[0]; + + expect(scriptTag.type).to.be.equal('module'); + // eslint-disable-next-line max-len + expect(scriptTag.textContent).to.be.contain('class e extends HTMLElement{constructor(){super(),this.root=this.attachShadow({mode:"open"}),this.root.innerHTML="\\n <header>This is the header component.</header>\\n "}}customElements.define("app-header",e);'); + }); + + it('should contain the expected content from <app-header> in the <body>', function() { + const header = dom.window.document.querySelectorAll('body header'); + + expect(header.length).to.be.equal(1); + expect(header[0].textContent).to.be.equal('This is the header component.'); + }); + }); + + describe('<link> tags as <style> tags and file output', function() { + xit('should contain no CSS files in the output directory', async function() { + const cssFiles = await glob.promise(`${this.context.publicDir}**/**/*.css`); + + expect(cssFiles).to.have.lengthOf(0); + }); + + it('should contain one <style> tag with the expected CSS content inlined', function() { + const styleTags = dom.window.document.querySelectorAll('head style'); + + // one for puppeteer + expect(styleTags.length).to.be.equal(2); + expect(styleTags[1].textContent).to.be.contain('*{margin:0;padding:0;font-family:Comic Sans,sans-serif}'); + }); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-inline/greenwood.config.js b/packages/cli/test/cases/build.config.optimization-inline/greenwood.config.js new file mode 100644 index 000000000..2384dd687 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-inline/greenwood.config.js @@ -0,0 +1,3 @@ +module.exports = { + optimization: 'inline' +}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-inline/src/components/header.js b/packages/cli/test/cases/build.config.optimization-inline/src/components/header.js new file mode 100644 index 000000000..ac4106aa7 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-inline/src/components/header.js @@ -0,0 +1,12 @@ +class HeaderComponent extends HTMLElement { + constructor() { + super(); + + this.root = this.attachShadow({ mode: 'open' }); + this.root.innerHTML = ` + <header>This is the header component.</header> + `; + } +} + +customElements.define('app-header', HeaderComponent); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-inline/src/pages/index.html b/packages/cli/test/cases/build.config.optimization-inline/src/pages/index.html new file mode 100644 index 000000000..57ef25887 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-inline/src/pages/index.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/components/header.js"></script> + <link rel="stylesheet" href="/styles/theme.css"></link> + </head> + + <body> + + <div class="gwd-content-outlet"> + <div> + <app-header></app-header> + </div> + </div> + + </body> + +</html> \ 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 <script> and <link> tags + * + * User Command + * greenwood build + * + * User Config + * { + * optimization: 'none' + * } + * + * Custom Workspace + * src/ + * components/ + * header.js + * pages/ + * index.html + * styles/ + * theme.css + */ +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 = 'None Optimization Configuration'; + 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('Output for JavaScript / CSS tags and files', function() { + let dom; + let cssFiles; + let jsFiles; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); + jsFiles = await glob.promise(path.join(this.context.publicDir, '*.js')); + cssFiles = await glob.promise(`${path.join(this.context.publicDir, 'styles')}/theme.*.css`); + }); + + it('should contain no <link> preload tags in the <head>', function() { + const preloadTags = dom.window.document.querySelectorAll('head link[rel="preload"]'); + + expect(preloadTags.length).to.be.equal(0); + }); + + describe('<script> tag and preloading', function() { + it('should contain one unminifed javasccript file in the output directory', async function() { + expect(jsFiles).to.have.lengthOf(1); + }); + + it('should output the contents of the JavaScript file unminified', function() { + const js = fs.readFileSync(jsFiles[0], 'utf-8'); + + // eslint-disable-next-line max-len + expect(js).to.be.contain('class HeaderComponent extends HTMLElement {\n constructor() {\n super();\n\n this.root = this.attachShadow({ mode: \'open\' });\n this.root.innerHTML = `\n <header>This is the header component.</header>\n `;\n }\n}\n\ncustomElements.define(\'app-header\', HeaderComponent);\n'); + }); + + it('should have the expected <script> tag in the <head>', function() { + const scriptTags = dom.window.document.querySelectorAll('head script'); + + expect(scriptTags.length).to.be.equal(1); + }); + + it('should contain the expected content from <app-header> in the <body>', function() { + const header = dom.window.document.querySelectorAll('body header'); + + expect(header.length).to.be.equal(1); + expect(header[0].textContent).to.be.equal('This is the header component.'); + }); + }); + + describe('<link> tags should not be preloaded', function() { + it('should contain one style.css in the output directory with unminifed content', async function() { + expect(cssFiles).to.have.lengthOf(1); + }); + + it('should output the contents of the CSS file unminified', function() { + const css = fs.readFileSync(cssFiles[0], 'utf-8'); + + expect(css).to.be.contain('{\n margin: 0;\n padding: 0;\n font-family: \'Comic Sans\', sans-serif;\n}'); + }); + + it('should have only one expected <link> tag in the <head>', function() { + const linkTags = dom.window.document.querySelectorAll('head link'); + + expect(linkTags.length).to.be.equal(1); + }); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-none/greenwood.config.js b/packages/cli/test/cases/build.config.optimization-none/greenwood.config.js new file mode 100644 index 000000000..41b1c4362 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-none/greenwood.config.js @@ -0,0 +1,3 @@ +module.exports = { + optimization: 'none' +}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-none/src/components/header.js b/packages/cli/test/cases/build.config.optimization-none/src/components/header.js new file mode 100644 index 000000000..ac4106aa7 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-none/src/components/header.js @@ -0,0 +1,12 @@ +class HeaderComponent extends HTMLElement { + constructor() { + super(); + + this.root = this.attachShadow({ mode: 'open' }); + this.root.innerHTML = ` + <header>This is the header component.</header> + `; + } +} + +customElements.define('app-header', HeaderComponent); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-none/src/pages/index.html b/packages/cli/test/cases/build.config.optimization-none/src/pages/index.html new file mode 100644 index 000000000..57ef25887 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-none/src/pages/index.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/components/header.js"></script> + <link rel="stylesheet" href="/styles/theme.css"></link> + </head> + + <body> + + <div class="gwd-content-outlet"> + <div> + <app-header></app-header> + </div> + </div> + + </body> + +</html> \ 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 <script> tags and files from the final HTML and output. + * + * User Command + * greenwood build + * + * User Config + * { + * optimization: 'static' + * } + * + * Custom Workspace + * src/ + * components/ + * header.js + * pages/ + * index.html + */ +const expect = require('chai').expect; +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 = 'Static Optimization Configuration'; + 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('JavaScript <script> tag and file static optimization', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); + }); + + it('should emit no javascript files to the output directory', async function() { + const jsFiles = await glob.promise(path.join(this.context.publicDir, '*.js')); + + expect(jsFiles).to.have.lengthOf(0); + }); + + it('should contain no <link> tags in the <head>', function() { + const linkTags = dom.window.document.querySelectorAll('head link'); + + expect(linkTags.length).to.be.equal(0); + }); + + it('should have no <script> tags in the <head>', function() { + const scriptTags = dom.window.document.querySelectorAll('head script'); + + expect(scriptTags.length).to.be.equal(0); + }); + + it('should contain the expected content from <app-header> in the <body>', function() { + const header = dom.window.document.querySelectorAll('body header'); + + expect(header.length).to.be.equal(1); + expect(header[0].textContent).to.be.equal('This is the header component.'); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-static/greenwood.config.js b/packages/cli/test/cases/build.config.optimization-static/greenwood.config.js new file mode 100644 index 000000000..040ce5504 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-static/greenwood.config.js @@ -0,0 +1,3 @@ +module.exports = { + optimization: 'static' +}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-static/src/components/header.js b/packages/cli/test/cases/build.config.optimization-static/src/components/header.js new file mode 100644 index 000000000..61ee46391 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-static/src/components/header.js @@ -0,0 +1,19 @@ +class HeaderComponent extends HTMLElement { + constructor() { + super(); + + this.root = this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.root.innerHTML = this.getTemplate(); + } + + getTemplate() { + return ` + <header>This is the header component.</header> + `; + } +} + +customElements.define('app-header', HeaderComponent); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-static/src/pages/index.html b/packages/cli/test/cases/build.config.optimization-static/src/pages/index.html new file mode 100644 index 000000000..44c18d455 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-static/src/pages/index.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/components/header.js"></script> + </head> + + <body> + + <div class="gwd-content-outlet"> + <div> + <app-header></app-header> + </div> + </div> + + </body> + +</html> \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.postcss/build.config.postcss.spec.js b/packages/cli/test/cases/build.config.postcss/build.config.postcss.spec.js deleted file mode 100644 index d705c8866..000000000 --- a/packages/cli/test/cases/build.config.postcss/build.config.postcss.spec.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Use Case - * Run Greenwood with a custom postcss config - * - * User Result - * Should generate a bare bones Greenwood build with a hello page containing a component with a - * @custom-media query - * - * User Command - * greenwood build - * - * User Workspace - * Greenwood default - * src/ - * pages/ - * hello.md - * index.md - */ -const { JSDOM } = require('jsdom'); -const path = require('path'); -const expect = require('chai').expect; -const runSmokeTest = require('../../../../../test/smoke-test'); -const TestBed = require('../../../../../test/test-bed'); - -describe('Build Greenwood With: ', function() { - const LABEL = 'Custom PostCSS configuration'; - 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('Hello page with working @custom-media queries', function() { - let dom; - - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './hello/index.html')); - }); - - it('should resolve the correct @custom-media queries for eve-container', function() { - // check @media (--screen-xs) resolves to @media (max-width:576px) via postcss preset-env: stage 1 - const expectedStyle = 'eve-container .container.eve-container,eve-container ' + - '.container-fluid.eve-container {\n margin-right:auto;margin-left:auto;padding-left:15px;' + - 'padding-right:15px\n}\n\n@media (max-width:576px) {\neve-container .container.eve-container ' + - '{\n width:calc(100% - 30px)\n}\n\n}\n\n@media (min-width:576px) {\neve-container ' + - '.container.eve-container {\n width:540px\n}\n\n}\n\n@media (min-width:768px) {\neve-container ' + - '.container.eve-container {\n width:720px\n}\n\n}\n\n@media (min-width:992px) {\neve-container ' + - '.container.eve-container {\n width:960px\n}\n\n}\n\n@media (min-width:1200px) {\neve-container ' + - '.container.eve-container {\n width:1140px\n}\n\n}'; - const containerStyle = dom.window.document.head.querySelector('style[scope="eve-container"]'); - expect(containerStyle.innerHTML).to.equal(expectedStyle); - }); - }); - }); - - after(function() { - setup.teardownTestBed(); - }); - -}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.postcss/postcss.config.js b/packages/cli/test/cases/build.config.postcss/postcss.config.js deleted file mode 100644 index dbe4fb4a1..000000000 --- a/packages/cli/test/cases/build.config.postcss/postcss.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - plugins: { - 'postcss-preset-env': { - stage: 1 - }, - 'postcss-nested': {}, - 'cssnano': {} - } -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.postcss/src/pages/hello.md b/packages/cli/test/cases/build.config.postcss/src/pages/hello.md deleted file mode 100644 index 3e4ce2541..000000000 --- a/packages/cli/test/cases/build.config.postcss/src/pages/hello.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -label: 'hello' -title: 'Hello Page' -imports: - Container: '@evergreen-wc/eve-container' ---- -### Hello World - -This is an example page built by Greenwood. Make your own in _src/pages_! - -<eve-container></eve-container> \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.postcss/src/pages/index.md b/packages/cli/test/cases/build.config.postcss/src/pages/index.md deleted file mode 100644 index 1c1a50fbb..000000000 --- a/packages/cli/test/cases/build.config.postcss/src/pages/index.md +++ /dev/null @@ -1,3 +0,0 @@ -### 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/packages/cli/test/cases/build.config.public-path/build.config.public-path.spec.js b/packages/cli/test/cases/build.config.public-path/build.config.public-path.spec.js deleted file mode 100644 index 26cf05849..000000000 --- a/packages/cli/test/cases/build.config.public-path/build.config.public-path.spec.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Use Case - * Run Greenwood with string publicPath in config and default workspace. - * - * User Result - * Should generate a bare bones Greenwood build. (same as build.default.spec.js) with custom publicPath - * from which assets will be served - * - * User Command - * greenwood build - * - * User Config - * { - * publicPath: '/assets/' - * } - */ -const { JSDOM } = require('jsdom'); -const path = require('path'); -const expect = require('chai').expect; -const runSmokeTest = require('../../../../../test/smoke-test'); -const TestBed = require('../../../../../test/test-bed'); - -describe('Build Greenwood With: ', function() { - const LABEL = 'Custom Public Path Configuration 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(['not-found', 'hello'], LABEL); - - describe('Custom Configuration with a custom public path', function() { - - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); - }); - - it('should serve assets from the configured publicPath', function() { - const publicPath = '/assets/'; - const scriptTags = dom.window.document.querySelectorAll('body script'); - const bundledScripts = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src; - - return src.indexOf('index.') >= 0 && src.indexOf('.bundle.js') >= 0; - }); - - expect(bundledScripts[0].src.indexOf(publicPath) >= 0).to.be.equal(true); - }); - }); - - }); - - after(function() { - setup.teardownTestBed(); - }); - -}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.public-path/greenwood.config.js b/packages/cli/test/cases/build.config.public-path/greenwood.config.js deleted file mode 100644 index 0ab84567b..000000000 --- a/packages/cli/test/cases/build.config.public-path/greenwood.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - publicPath: '/assets/' -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.theme/build.config.theme.spec.js b/packages/cli/test/cases/build.config.theme/build.config.theme.spec.js deleted file mode 100644 index c3d699f25..000000000 --- a/packages/cli/test/cases/build.config.theme/build.config.theme.spec.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Use Case - * Run Greenwood with a custom themeFile file in config and default workspace with a page template. - * - * User Result - * Should generate a bare bones Greenwood build. (same as build.default.spec.js) with custom theme styles - * - * User Command - * greenwood build - * - * User Config - * { - * title: 'My Custom Greenwood App' - * } - * - * User Workspace - * Greenwood default - * src/ - * templates/ - * page-template.js - * styles/ - * my-brand.css - */ -const fs = require('fs'); -const { JSDOM } = require('jsdom'); -const path = require('path'); -const expect = require('chai').expect; -const runSmokeTest = require('../../../../../test/smoke-test'); -const TestBed = require('../../../../../test/test-bed'); - -describe('Build Greenwood With: ', function() { - const LABEL = 'Custom Theme Configuration 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', 'not-found', 'index'], LABEL); - - describe('Theme Styled Page Template', function() { - let dom; - - before(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); - }); - - it('should output a single index.html file using our custom styled page template', function() { - expect(fs.existsSync(path.join(this.context.publicDir, './index.html'))).to.be.true; - }); - - it('should have the expected font import', function() { - const styles = '@import url(//fonts.googleapis.com/css?family=Roboto'; - const styleTags = dom.window.document.querySelectorAll('head style'); - let importCount = 0; - - styleTags.forEach((tag) => { - if (tag.textContent.indexOf(styles) >= 0) { - importCount += 1; - } - }); - - expect(importCount).to.equal(1); - }); - - it('should have the expected font family', function() { - const styles = 'body{font-family:Roboto,sans-serif}'; - const styleTags = dom.window.document.querySelectorAll('head style'); - let fontCount = 0; - - styleTags.forEach((tag) => { - if (tag.textContent.indexOf(styles) >= 0) { - fontCount += 1; - } - }); - - expect(fontCount).to.equal(1); - }); - - }); - - }); - - after(function() { - setup.teardownTestBed(); - }); - -}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.theme/greenwood.config.js b/packages/cli/test/cases/build.config.theme/greenwood.config.js deleted file mode 100644 index 815ada47f..000000000 --- a/packages/cli/test/cases/build.config.theme/greenwood.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - themeFile: 'my-brand.css' -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.theme/src/styles/my-brand.css b/packages/cli/test/cases/build.config.theme/src/styles/my-brand.css deleted file mode 100644 index b34a86dbc..000000000 --- a/packages/cli/test/cases/build.config.theme/src/styles/my-brand.css +++ /dev/null @@ -1,5 +0,0 @@ -@import url('//fonts.googleapis.com/css?family=Roboto'); - -body { - font-family: 'Roboto', sans-serif; -} \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.theme/src/templates/page-template.js b/packages/cli/test/cases/build.config.theme/src/templates/page-template.js deleted file mode 100644 index e2306013f..000000000 --- a/packages/cli/test/cases/build.config.theme/src/templates/page-template.js +++ /dev/null @@ -1,16 +0,0 @@ -import { html, LitElement } from 'lit-element'; -import '../styles/my-brand.css'; - -class PageTemplate extends LitElement { - render() { - return html` - <div class='wrapper'> - <div class='page-template content owen-test'> - <entry></entry> - </div> - </div> - `; - } -} - -customElements.define('page-template', PageTemplate); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.title/build.config.title.spec.js b/packages/cli/test/cases/build.config.title/build.config.title.spec.js index d5597d07a..9c2eb8b25 100644 --- a/packages/cli/test/cases/build.config.title/build.config.title.spec.js +++ b/packages/cli/test/cases/build.config.title/build.config.title.spec.js @@ -28,7 +28,6 @@ const runSmokeTest = require('../../../../../test/smoke-test'); const TestBed = require('../../../../../test/test-bed'); const configTitle = require('./greenwood.config').title; -const mainBundleScriptRegex = /index.*.bundle\.js/; describe('Build Greenwood With: ', function() { const LABEL = 'Custom Title Configuration and Default Workspace'; @@ -40,108 +39,43 @@ describe('Build Greenwood With: ', function() { }); describe(LABEL, function() { + before(async function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'not-found', 'hello'], LABEL); + + runSmokeTest(['public', 'index'], LABEL); - describe('Custom Title', function() { - const indexPageHeading = 'Greenwood'; - const indexPageBody = 'This is the home page built by Greenwood. Make your own pages in src/pages/index.js!'; + describe('Custom Title from Configuration', function() { let dom; - beforeEach(async function() { + before(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 <title> tag in the <head>', function() { + it('should have our custom config <title> tag in the <head>', function() { const title = dom.window.document.querySelector('head title').textContent; expect(title).to.be.equal(configTitle); }); - - it('should have one <script> tag in the <body> for the main bundle', function() { - const scriptTags = dom.window.document.querySelectorAll('body > script'); - const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src.replace('file:///', ''); - - return mainBundleScriptRegex.test(src); - }); - - expect(bundledScript.length).to.be.equal(1); - }); - - it('should have one <script> tag in the <body> for the main bundle loaded with async', function() { - const scriptTags = dom.window.document.querySelectorAll('body > script'); - const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src.replace('file:///', ''); - - return mainBundleScriptRegex.test(src); - }); - - expect(bundledScript[0].getAttribute('async')).to.be.equal(''); - }); - - it('should have one <script> tag for Apollo state', function() { - const scriptTags = dom.window.document.querySelectorAll('script'); - const bundleScripts = Array.prototype.slice.call(scriptTags).filter(script => { - return script.getAttribute('data-state') === 'apollo'; - }); - - expect(bundleScripts.length).to.be.equal(1); - }); - - it('should have only one <script> tag in the <head>', function() { - const scriptTags = dom.window.document.querySelectorAll('head > script'); - - expect(scriptTags.length).to.be.equal(1); - }); - - it('should have a router outlet tag in the <body>', function() { - const outlet = dom.window.document.querySelectorAll('body eve-app'); - - expect(outlet.length).to.be.equal(1); - }); - - it('should have the correct route tags in the <body>', function() { - const routes = dom.window.document.querySelectorAll('body lit-route'); - - expect(routes.length).to.be.equal(3); - }); - - it('should have the expected heading text within the index page in the public directory', function() { - const heading = dom.window.document.querySelector('h3').textContent; - - expect(heading).to.equal(indexPageHeading); - }); - - it('should have the expected paragraph text within the index page in the public directory', function() { - let paragraph = dom.window.document.querySelector('p').textContent; - - expect(paragraph).to.equal(indexPageBody); - }); }); describe('Custom Front-Matter Title', function() { - const helloPageTitle = 'Hello Page'; + const pageTitle = 'About Page'; let dom; - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'hello', './index.html')); + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'about', './index.html')); }); - it('should output an index.html file within the hello page directory', function() { - expect(fs.existsSync(path.join(this.context.publicDir, 'hello', './index.html'))).to.be.true; + it('should output an index.html file within the about page directory', function() { + expect(fs.existsSync(path.join(this.context.publicDir, 'about', './index.html'))).to.be.true; }); it('should have a overridden meta <title> tag in the <head> using markdown front-matter', function() { const title = dom.window.document.querySelector('head title').textContent; - expect(title).to.be.equal(`${configTitle} - ${helloPageTitle}`); + expect(title).to.be.equal(`${configTitle} - ${pageTitle}`); }); }); }); diff --git a/packages/cli/src/templates/hello.md b/packages/cli/test/cases/build.config.title/src/pages/about.md similarity index 64% rename from packages/cli/src/templates/hello.md rename to packages/cli/test/cases/build.config.title/src/pages/about.md index d0c315dca..afd78bcbc 100644 --- a/packages/cli/src/templates/hello.md +++ b/packages/cli/test/cases/build.config.title/src/pages/about.md @@ -1,6 +1,8 @@ --- -label: 'hello' +label: 'about' +title: 'About Page' --- -### Hello World + +### About This is an example page built by Greenwood. Make your own in _src/pages_! \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.title/src/pages/hello.md b/packages/cli/test/cases/build.config.title/src/pages/hello.md deleted file mode 100644 index fb8cf32d5..000000000 --- a/packages/cli/test/cases/build.config.title/src/pages/hello.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -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/packages/cli/test/cases/build.config.workspace-custom/build.config.workspace-custom.spec.js b/packages/cli/test/cases/build.config.workspace-custom/build.config.workspace-custom.spec.js index f7a07cf2d..7a54d264a 100644 --- a/packages/cli/test/cases/build.config.workspace-custom/build.config.workspace-custom.spec.js +++ b/packages/cli/test/cases/build.config.workspace-custom/build.config.workspace-custom.spec.js @@ -40,12 +40,12 @@ describe('Build Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'index', 'not-found'], LABEL); + runSmokeTest(['public', 'index'], LABEL); describe('Custom About page', function() { let dom; - beforeEach(async function() { + before(async function() { dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'about', './index.html')); }); diff --git a/packages/cli/test/cases/build.data.graph-custom-frontmatter/build.data.graph-custom-frontmatter.spec.js b/packages/cli/test/cases/build.data.graph-custom-frontmatter/build.data.graph-custom-frontmatter.spec.js deleted file mode 100644 index e1d3571ee..000000000 --- a/packages/cli/test/cases/build.data.graph-custom-frontmatter/build.data.graph-custom-frontmatter.spec.js +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Use Case - * Run Greenwood build command with GraphQL calls to get data about the projects graph. - * - * User Result - * Should generate a Greenwood build that specifically tests for custom frontmatter set by individual pages. - * - * User Command - * greenwood build - * - * Default Config - * - * Custom Workspace - * src/ - * pages/ - * blog/ - * first-post.md - * second-post.md - * index.md - * templates/ - * blog-template.js - */ -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'); - -const mainBundleScriptRegex = /index.*.bundle\.js/; - -describe('Build Greenwood With: ', function() { - const LABEL = 'Data from GraphQL and using Custom Frontmatter Data'; - const apolloStateRegex = /window.__APOLLO_STATE__ = true/; - 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', 'not-found'], LABEL); - - describe('Blog Page (Template) w/ custom date', function() { - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'blog', 'first-post', 'index.html')); - }); - - it('should output an index.html file (first post page)', function() { - expect(fs.existsSync(path.join(this.context.publicDir, 'blog', 'first-post', 'index.html'))).to.be.true; - }); - - it('should output a (partial) *-cache.json file, one per each query made', async function() { - expect(await glob.promise(path.join(this.context.publicDir, './*-cache.json'))).to.have.lengthOf(3); - }); - - it('should output a (partial) *-cache.json files, one per each query made, that are all defined', async function() { - const cacheFiles = await glob.promise(path.join(this.context.publicDir, './*-cache.json')); - - cacheFiles.forEach(file => { - const cache = require(file); - - expect(cache).to.not.be.undefined; - }); - }); - - it('should have one <script> tag in the <body> for the main bundle', function() { - const scriptTags = dom.window.document.querySelectorAll('body > script'); - const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src.replace('file:///', ''); - - return mainBundleScriptRegex.test(src); - }); - - expect(bundledScript.length).to.be.equal(1); - }); - - it('should have one <script> tag in the <body> for the main bundle loaded with async', function() { - const scriptTags = dom.window.document.querySelectorAll('body > script'); - const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src.replace('file:///', ''); - - return mainBundleScriptRegex.test(src); - }); - - expect(bundledScript[0].getAttribute('async')).to.be.equal(''); - }); - - it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', () => { - const scriptTags = dom.window.document.querySelectorAll('script'); - const apolloScriptTags = Array.prototype.slice.call(scriptTags).filter(script => { - return script.getAttribute('data-state') === 'apollo'; - }); - const innerHTML = apolloScriptTags[0].innerHTML; - - expect(apolloScriptTags.length).to.equal(1); - expect(innerHTML).to.match(apolloStateRegex); - }); - - // two webpack bundles, and apollo state - it('should have only 3 <script> tag in the <head>', function() { - const scriptTags = dom.window.document.querySelectorAll('head > script'); - - expect(scriptTags.length).to.be.equal(3); - }); - - it('should have expected blog posts links in the <body> tag when using ChildrenQuery', function() { - const listItems = dom.window.document.querySelectorAll('body div.posts ul li'); - const linkItems = dom.window.document.querySelectorAll('body div.posts ul li a'); - const spanItems = dom.window.document.querySelectorAll('body div.posts ul li span'); - - expect(listItems.length).to.be.equal(2); - expect(linkItems.length).to.be.equal(2); - expect(spanItems.length).to.be.equal(2); - - const link1 = linkItems[0]; - const link2 = linkItems[1]; - const span1 = spanItems[0]; - const span2 = spanItems[1]; - - expect(link1.href.replace('file://', '')).to.be.equal('/blog/first-post/'); - expect(link1.title).to.be.equal('Click to read my Blog blog post'); - expect(link1.innerHTML).to.contain('Blog posted: 2020/04/05'); - expect(span1.innerHTML).to.contain('Author: Lorum'); - - expect(link2.href.replace('file://', '')).to.be.equal('/blog/second-post/'); - expect(link2.title).to.be.equal('Click to read my Blog blog post'); - expect(link2.innerHTML).to.contain('Blog posted: 2020/04/06'); - expect(span2.innerHTML).to.contain('Author: Ipsum'); - }); - }); - - }); - - after(function() { - setup.teardownTestBed(); - }); - -}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/index.md b/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/index.md deleted file mode 100644 index b38fc3cfd..000000000 --- a/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/index.md +++ /dev/null @@ -1,7 +0,0 @@ -## Home Page - -This is the Getting Started home page! - -### My Posts -- [my-second-post](/blog/second-post/) -- [my-first-post](/blog/first-post/) diff --git a/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/templates/blog-template.js b/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/templates/blog-template.js deleted file mode 100644 index 87ff4086d..000000000 --- a/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/templates/blog-template.js +++ /dev/null @@ -1,78 +0,0 @@ -import { html, LitElement } from 'lit-element'; -import client from '@greenwood/cli/data/client'; -import gql from 'graphql-tag'; - -class BlogTemplate extends LitElement { - - static get properties() { - return { - posts: { - type: Array - } - }; - } - - constructor() { - super(); - this.posts = []; - } - - async connectedCallback() { - super.connectedCallback(); - const response = await client.query({ - query: gql`query($parent: String!) { - children(parent: $parent) { - id, - title, - link, - filePath, - fileName, - template, - data { - date, - author - } - } - }`, - variables: { - parent: 'blog' - } - }); - - this.posts = response.data.children; - } - - /* eslint-disable indent */ - render() { - const { posts } = this; - - return html` - - <div class='container'> - - <entry></entry> - - <div class="posts"> - <h1>More Posts</h1> - - <ul> - ${posts.map((post) => { - return html` - <li> - <a href="${post.link}/" title="Click to read my ${post.title} blog post"> - ${post.title} posted: ${post.data.date} - </a> - <span>Author: ${post.data.author}</span> - </li> - `; - })} - </ul> - </div> - - </div> - `; - } - /* eslint-enable */ -} - -customElements.define('page-template', BlogTemplate); \ No newline at end of file diff --git a/packages/cli/test/cases/build.data.graph/build.data.graph.spec.js b/packages/cli/test/cases/build.data.graph/build.data.graph.spec.js deleted file mode 100644 index 106ef502a..000000000 --- a/packages/cli/test/cases/build.data.graph/build.data.graph.spec.js +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Use Case - * Run Greenwood build command with GraphQL calls to get data about the projects graph. This will test various - * permutations of Children, Menu, and Graph calls, simulating a site of blog posts. - * - * User Result - * Should generate a Greenwood build that dynamically serializes data from the graph from the header and in the page-template. - * - * User Command - * greenwood build - * - * Default Config - * - * Custom Workspace - * src/ - * components/ - * header.js - * pages/ - * blog/ - * first-post/ - * index.md - * second-post/ - * index.md - * index.md - * templates/ - * app-template.js - * blog-template.js - * post-template.js - */ -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'); - -const mainBundleScriptRegex = /index.*.bundle\.js/; - -describe('Build Greenwood With: ', function() { - const LABEL = 'Data from GraphQL'; - const apolloStateRegex = /window.__APOLLO_STATE__ = true/; - 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', 'not-found'], LABEL); - - describe('Home (Page Template) w/ Navigation Query', function() { - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); - }); - - it('should create a public directory', function() { - expect(fs.existsSync(this.context.publicDir)).to.be.true; - }); - - it('should output an index.html file (home page)', function() { - expect(fs.existsSync(path.join(this.context.publicDir, './index.html'))).to.be.true; - }); - - it('should output a single 404.html file (not found page)', function() { - expect(fs.existsSync(path.join(this.context.publicDir, './404.html'))).to.be.true; - }); - - it('should output one JS bundle file', async function() { - expect(await glob.promise(path.join(this.context.publicDir, './index.*.bundle.js'))).to.have.lengthOf(1); - }); - - it('should output a (partial) *-cache.json file, one per each query made', async function() { - expect(await glob.promise(path.join(this.context.publicDir, './*-cache.json'))).to.have.lengthOf(5); - }); - - it('should output a (partial) *-cache.json files, one per each query made, that are all defined', async function() { - const cacheFiles = await glob.promise(path.join(this.context.publicDir, './*-cache.json')); - - cacheFiles.forEach(file => { - const cache = require(file); - - expect(cache).to.not.be.undefined; - }); - }); - - it('should have one <script> tag in the <body> for the main bundle', function() { - const scriptTags = dom.window.document.querySelectorAll('body > script'); - const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src.replace('file:///', ''); - - return mainBundleScriptRegex.test(src); - }); - - expect(bundledScript.length).to.be.equal(1); - }); - - it('should have one <script> tag in the <body> for the main bundle loaded with async', function() { - const scriptTags = dom.window.document.querySelectorAll('body > script'); - const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src.replace('file:///', ''); - - return mainBundleScriptRegex.test(src); - }); - - expect(bundledScript[0].getAttribute('async')).to.be.equal(''); - }); - - it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', () => { - const scriptTags = dom.window.document.querySelectorAll('script'); - const apolloScriptTags = Array.prototype.slice.call(scriptTags).filter(script => { - return script.getAttribute('data-state') === 'apollo'; - }); - const innerHTML = apolloScriptTags[0].innerHTML; - - expect(apolloScriptTags.length).to.equal(1); - expect(innerHTML).to.match(apolloStateRegex); - }); - - it('should have only one <script> tag in the <head>', function() { - const scriptTags = dom.window.document.querySelectorAll('head > script'); - - expect(scriptTags.length).to.be.equal(1); - }); - - it('should have a <header> tag in the <body>', function() { - const header = dom.window.document.querySelectorAll('body header'); - - expect(header.length).to.be.equal(1); - }); - - it('should have a expected NavigationQuery output in the <header> tag', function() { - const listItems = dom.window.document.querySelectorAll('body header ul li'); - const link1 = listItems[0].querySelector('a'); - const link2 = listItems[1].querySelector('a'); - - expect(listItems.length).to.be.equal(2); - - expect(link1.href.replace('file://', '')).to.be.equal('/blog/first-post/'); - expect(link1.title).to.be.equal('Click to visit the First blog post'); - expect(link1.innerHTML).to.contain('First'); - - expect(link2.href.replace('file://', '')).to.be.equal('/blog/second-post/'); - expect(link2.title).to.be.equal('Click to visit the Second blog post'); - expect(link2.innerHTML).to.contain('Second'); - }); - }); - - describe('Blog Page (Template) w/ Navigation and Children Query', function() { - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'blog', 'index.html')); - }); - - it('should output an index.html file (first post page)', function() { - expect(fs.existsSync(path.join(this.context.publicDir, 'blog', 'index.html'))).to.be.true; - }); - - it('should have one <script> tag in the <body> for the main bundle', function() { - const scriptTags = dom.window.document.querySelectorAll('body > script'); - const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src.replace('file:///', ''); - - return mainBundleScriptRegex.test(src); - }); - - expect(bundledScript.length).to.be.equal(1); - }); - - it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', () => { - const scriptTags = dom.window.document.querySelectorAll('script'); - const apolloScriptTags = Array.prototype.slice.call(scriptTags).filter(script => { - return script.getAttribute('data-state') === 'apollo'; - }); - const innerHTML = apolloScriptTags[0].innerHTML; - - expect(apolloScriptTags.length).to.equal(1); - expect(innerHTML).to.match(apolloStateRegex); - }); - - it('should have only one <script> tag in the <head>', function() { - const scriptTags = dom.window.document.querySelectorAll('head > script'); - - expect(scriptTags.length).to.be.equal(2); - }); - - it('should have a <header> tag in the <body>', function() { - const header = dom.window.document.querySelectorAll('body header'); - - expect(header.length).to.be.equal(1); - }); - - it('should have expected navigation links in the <header> tag tag when using NavigationQuery', function() { - const listItems = dom.window.document.querySelectorAll('body header ul li'); - const link1 = listItems[0].querySelector('a'); - const link2 = listItems[1].querySelector('a'); - - expect(listItems.length).to.be.equal(2); - - expect(link1.href.replace('file://', '')).to.be.equal('/blog/first-post/'); - expect(link1.title).to.be.equal('Click to visit the First blog post'); - expect(link1.innerHTML).to.contain('First'); - - expect(link2.href.replace('file://', '')).to.be.equal('/blog/second-post/'); - expect(link2.title).to.be.equal('Click to visit the Second blog post'); - expect(link2.innerHTML).to.contain('Second'); - }); - - it('should have expected blog posts links in the <body> tag when using ChildrenQuery', function() { - const listItems = dom.window.document.querySelectorAll('body div.posts ul li'); - const linkItems = dom.window.document.querySelectorAll('body div.posts ul li a'); - - expect(listItems.length).to.be.equal(2); - expect(linkItems.length).to.be.equal(2); - - const link1 = linkItems[0]; - const link2 = linkItems[1]; - - expect(link1.href.replace('file://', '')).to.be.equal('/blog/first-post/'); - expect(link1.title).to.be.equal('Click to read my First blog post'); - expect(link1.innerHTML).to.contain('First'); - - expect(link2.href.replace('file://', '')).to.be.equal('/blog/second-post/'); - expect(link2.title).to.be.equal('Click to read my Second blog post'); - expect(link2.innerHTML).to.contain('Second'); - }); - }); - - describe('Blog Post Pages (Post Template) w/ custom Graph Query', function() { - let dom2; - - // just use one post page for the generic tests here - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'blog', 'first-post', 'index.html')); - dom2 = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'blog', 'second-post', 'index.html')); - }); - - it('should output an index.html file for first blog post page', function() { - expect(fs.existsSync(path.join(this.context.publicDir, 'blog', 'first-post', 'index.html'))).to.be.true; - }); - - it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', () => { - const scriptTags = dom.window.document.querySelectorAll('script'); - const apolloScriptTags = Array.prototype.slice.call(scriptTags).filter(script => { - return script.getAttribute('data-state') === 'apollo'; - }); - const innerHTML = apolloScriptTags[0].innerHTML; - - expect(apolloScriptTags.length).to.equal(1); - expect(innerHTML).to.match(apolloStateRegex); - }); - - it('should have a <header> tag in the <body>', function() { - const header = dom.window.document.querySelectorAll('body header'); - - expect(header.length).to.be.equal(1); - }); - - it('should have expected navigation links in the <header> tag tag when using NavigationQuery', function() { - const listItems = dom.window.document.querySelectorAll('body header ul li'); - const link = listItems[0].querySelector('a'); - - expect(listItems.length).to.be.equal(2); - expect(link.href.replace('file://', '')).to.be.equal('/blog/first-post/'); - expect(link.title).to.be.equal('Click to visit the First blog post'); - expect(link.innerHTML).to.contain('First'); - }); - - it('should have expected date in the <body> tag when using custom GraphQuery', function() { - const date = dom.window.document.querySelectorAll('body p.date'); - - expect(date.length).to.be.equal(1); - expect(date[0].innerHTML).to.contain('Posted on 07.08.2020'); - }); - - it('should have expected date in the <body> tag when using custom GraphQuery', function() { - const date = dom2.window.document.querySelectorAll('body p.date'); - - expect(date.length).to.be.equal(1); - expect(date[0].innerHTML).to.contain('Posted on 07.09.2020'); - }); - }); - - }); - - after(function() { - setup.teardownTestBed(); - }); - -}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.data.graph/src/pages/blog/index.md b/packages/cli/test/cases/build.data.graph/src/pages/blog/index.md deleted file mode 100644 index 3d2da3d23..000000000 --- a/packages/cli/test/cases/build.data.graph/src/pages/blog/index.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -template: 'blog' ---- - -## My Posts \ No newline at end of file diff --git a/packages/cli/test/cases/build.data.graph/src/pages/index.md b/packages/cli/test/cases/build.data.graph/src/pages/index.md deleted file mode 100644 index b38fc3cfd..000000000 --- a/packages/cli/test/cases/build.data.graph/src/pages/index.md +++ /dev/null @@ -1,7 +0,0 @@ -## Home Page - -This is the Getting Started home page! - -### My Posts -- [my-second-post](/blog/second-post/) -- [my-first-post](/blog/first-post/) diff --git a/packages/cli/test/cases/build.data.graph/src/templates/blog-template.js b/packages/cli/test/cases/build.data.graph/src/templates/blog-template.js deleted file mode 100644 index 5a0e50b1f..000000000 --- a/packages/cli/test/cases/build.data.graph/src/templates/blog-template.js +++ /dev/null @@ -1,62 +0,0 @@ -import { html, LitElement } from 'lit-element'; -import client from '@greenwood/cli/data/client'; -import ChildrenQuery from '@greenwood/cli/data/queries/children'; -import '../components/header'; - -class BlogTemplate extends LitElement { - - static get properties() { - return { - posts: { - type: Array - } - }; - } - - constructor() { - super(); - this.posts = []; - } - - async connectedCallback() { - super.connectedCallback(); - const response = await client.query({ - query: ChildrenQuery, - variables: { - parent: 'blog' - } - }); - - this.posts = response.data.children; - } - - /* eslint-disable indent */ - render() { - const { posts } = this; - - return html` - - <div class='container'> - <app-header></app-header> - - <div class="posts"> - <entry></entry> - - <ul> - ${posts.map((post) => { - return html` - <li> - <a href="${post.link}" title="Click to read my ${post.title} blog post">${post.title}</a> - </li> - `; - })} - </ul> - </div> - - </div> - `; - } - /* eslint-enable */ -} - -customElements.define('page-template', BlogTemplate); \ No newline at end of file diff --git a/packages/cli/test/cases/build.data.graph/src/templates/page-template.js b/packages/cli/test/cases/build.data.graph/src/templates/page-template.js deleted file mode 100644 index 88022b8a1..000000000 --- a/packages/cli/test/cases/build.data.graph/src/templates/page-template.js +++ /dev/null @@ -1,21 +0,0 @@ -import { html, LitElement } from 'lit-element'; -import '../components/header'; - -class PageTemplate extends LitElement { - - constructor() { - super(); - } - - render() { - return html` - <div> - <app-header></app-header> - - <entry></entry> - </div> - `; - } -} - -customElements.define('page-template', PageTemplate); \ No newline at end of file diff --git a/packages/cli/test/cases/build.data.graph/src/templates/post-template.js b/packages/cli/test/cases/build.data.graph/src/templates/post-template.js deleted file mode 100644 index 6dc53b8d1..000000000 --- a/packages/cli/test/cases/build.data.graph/src/templates/post-template.js +++ /dev/null @@ -1,67 +0,0 @@ -import client from '@greenwood/cli/data/client'; -import gql from 'graphql-tag'; -import { html, LitElement } from 'lit-element'; -import '../components/header'; - -class PostTemplate extends LitElement { - - static get properties() { - return { - post: { - type: Object - } - }; - } - - constructor() { - super(); - - this.post = { - title: '', - link: '', - data: { - date: '' - } - }; - } - - async connectedCallback() { - super.connectedCallback(); - let route = window.location.pathname; - const response = await client.query({ - query: gql`query { - graph { - title, - link, - data { - date - } - } - }` - }); - - if (route.lastIndexOf('/') !== route.length - 1) { - route = `${route}/`; - } - - this.post = response.data.graph.filter((page) => { - return page.link.lastIndexOf(route) >= 0; - })[0]; - } - - render() { - const { date } = this.post.data; - - return html` - <div> - <app-header></app-header> - - <entry></entry> - - <p class="date">Posted on ${date}</p> - </div> - `; - } -} - -customElements.define('page-template', PostTemplate); \ 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 new file mode 100644 index 000000000..948ba8b8a --- /dev/null +++ b/packages/cli/test/cases/build.default.import-node-modules/build.default.import-node-modules.spec.js @@ -0,0 +1,268 @@ +/* + * Use Case + * Run Greenwood with and loading different references to node_module types to ensure proper support. + * + * Uaer Result + * Should generate a bare bones Greenwood build without erroring. + * + * User Command + * greenwood build + * + * User Config + * None + * + * User Workspace + * src/ + * pages/ + * index.html + * scripts/ + * main.js + */ +const expect = require('chai').expect; +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 = 'Importing packages from node modules'; + + let setup; + + before(async function() { + setup = new TestBed(); + + const litElementLibs = (await glob(`${process.cwd()}/node_modules/lit-element/lib/*.js`)).map((lib) => { + return { + dir: 'node_modules/lit-element/lib/', + name: path.basename(lib) + }; + }); + const litHtmlLibs = (await glob(`${process.cwd()}/node_modules/lit-html/lib/*.js`)).map((lib) => { + return { + dir: 'node_modules/lit-html/lib/', + name: path.basename(lib) + }; + }); + const lodashLibs = (await glob(`${process.cwd()}/node_modules/lodash-es/*.js`)).map((lib) => { + return { + dir: 'node_modules/lodash-es/', + name: path.basename(lib) + }; + }); + const pwaHelpersLibs = (await glob(`${process.cwd()}/node_modules/pwa-helpers/*.js`)).map((lib) => { + return { + dir: 'node_modules/pwa-helpers/', + name: path.basename(lib) + }; + }); + + this.context = await setup.setupTestBed(__dirname, [{ + // redux + dir: 'node_modules/redux/es', + name: 'redux.mjs' + }, { + dir: 'node_modules/redux/', + name: 'package.json' + }, { + dir: 'node_modules/loose-envify/', + name: 'index.js' + }, { + dir: 'node_modules/loose-envify/', + name: 'package.json' + }, { + dir: 'node_modules/js-tokens/', + name: 'index.js' + }, { + dir: 'node_modules/js-tokens/', + name: 'package.json' + }, { + dir: 'node_modules/symbol-observable/es/', + name: 'index.js' + }, { + dir: 'node_modules/symbol-observable/es/', + name: 'ponyfill.js' + }, { + dir: 'node_modules/symbol-observable/', + name: 'package.json' + }, { + + // lit-element (+ lit-html) + dir: 'node_modules/lit-element/', + name: 'lit-element.js' + }, { + dir: 'node_modules/lit-element/', + name: 'package.json' + }, + + ...litElementLibs, + + { + dir: 'node_modules/lit-html/', + name: 'lit-html.js' + }, { + dir: 'node_modules/lit-html/', + name: 'package.json' + }, + + ...litHtmlLibs, + + { + // lodash-es + dir: 'node_modules/lodash-es/', + name: 'lodash.js' + }, { + dir: 'node_modules/lodash-es/', + name: 'package.json' + }, + + ...lodashLibs, + + { + // pwa-helpers + dir: 'node_modules/pwa-helpers/', + name: 'package.json' + }, + + ...pwaHelpersLibs, + + { + // prism.css + dir: 'node_modules/prismjs/themes/', + name: 'prism-tomorrow.css' + }, + + { + // simple.css - included as a non-JavaScript package + // https://github.com/ProjectEvergreen/greenwood/issues/484 + dir: 'node_modules/simpledotcss/', + name: 'package.json' + }, + { + dir: 'node_modules/simpledotcss/', + name: 'simple.css' + }]); + }); + + describe(LABEL, function() { + let dom; + + before(async function() { + await setup.runGreenwoodCommand('build'); + + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + describe('<script src="..."> tag in the <head> tag', function() { + it('should have one <script src="..."> tag for main.js loaded in the <head> tag', function() { + const scriptTags = dom.window.document.querySelectorAll('head > script[src]'); + const mainScriptTags = Array.prototype.slice.call(scriptTags).filter(script => { + return (/main.*.js/).test(script.src); + }); + + expect(mainScriptTags.length).to.be.equal(1); + }); + + it('should have the total expected number of .js file in the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, '*.js'))).to.have.lengthOf(4); + }); + + it('should have the expected main.js file in the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, 'main.*.js'))).to.have.lengthOf(1); + }); + + it('should have the expected output from main.js for lit-element (ESM) in the page output', async function() { + const litOutput = dom.window.document.querySelectorAll('body > .output-lit'); + + expect(litOutput.length).to.be.equal(1); + expect(litOutput[0].textContent).to.be.equal('import from lit-element Y2xhc3MgTGl0RWxl'); + }); + + it('should have the expected output from main.js for lodash-es (ESM) in the page output', async function() { + const litOutput = dom.window.document.querySelectorAll('body > .output-lodash'); + + expect(litOutput.length).to.be.equal(1); + expect(litOutput[0].textContent).to.be.equal('import from lodash-es {"a":1,"b":2}'); + }); + + it('should have the expected output from main.js for pwa-helpers (ESM) in the page output', async function() { + const litOutput = dom.window.document.querySelectorAll('body > .output-pwa'); + + expect(litOutput.length).to.be.equal(1); + expect(litOutput[0].textContent).to.be.equal('import from pwa-helpers KGNvbWJpbmVSZWR1'); + }); + + it('should have the expected output from main.js for Redux (MJS) in the page output', async function() { + const reduxOutput = dom.window.document.querySelectorAll('body > .output-redux'); + + expect(reduxOutput.length).to.be.equal(1); + expect(reduxOutput[0].textContent).to.be.equal('import from redux ZnVuY3Rpb24gbyh0'); + }); + }); + + describe('<script> tag with inline code in the <head> tag', function() { + it('should have one <script> tag with inline code loaded in the <head> tag', function() { + const scriptTagsInline = dom.window.document.querySelectorAll('head > script:not([src])'); + + expect(scriptTagsInline.length).to.be.equal(1); + }); + + it('should have the expected lit-element.js file in the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, 'lit-element.*.js'))).to.have.lengthOf(1); + }); + + it('should have the expected lit-html.js files in the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, 'lit-html.cdc8faae.js'))).to.have.lengthOf(1); + }); + + it('should have the expected inline node_modules content in the inline script', async function() { + const inlineScriptTag = dom.window.document.querySelector('head > script:not([src])'); + + expect(inlineScriptTag.textContent).to + .contain('import"/lit-html.d69ea26f.js";import"/lit-element.ea26fec7.js";document.getElementsByClassName("output-script-inline")[0].innerHTML="script tag module inline"'); + }); + + it('should have the expected output from inline <script> tag in the page output', async function() { + const inlineScriptOutput = dom.window.document.querySelectorAll('body > .output-script-inline'); + + expect(inlineScriptOutput.length).to.be.equal(1); + expect(inlineScriptOutput[0].textContent).to.be.equal('script tag module inline'); + }); + }); + + describe('<script src="..."> with reference to node_modules/ path in the <head> tag', function() { + it('should have one <script src="..."> tag for lit-html loaded in the <head> tag', function() { + const scriptTagsInline = dom.window.document.querySelectorAll('head > script[src]'); + const litScriptTags = Array.prototype.slice.call(scriptTagsInline).filter(script => { + return (/lit.*.js/).test(script.src); + }); + + expect(litScriptTags.length).to.be.equal(1); + }); + + it('should have the expected lit-html.js files in the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, 'lit-html.d69ea26f.js'))).to.have.lengthOf(1); + }); + }); + + describe('<link rel="stylesheet" href="..."> with reference to node_modules/ path in the <head> tag', function() { + it('should have one <link href="..."> tag in the <head> tag', function() { + 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); + }); + + expect(prismLinkTag.length).to.be.equal(1); + }); + + it('should have the expected prism.css file in the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, 'prism-tomorrow.*.css'))).to.have.lengthOf(1); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.import-node-modules/package.json b/packages/cli/test/cases/build.default.import-node-modules/package.json new file mode 100644 index 000000000..2bff94f44 --- /dev/null +++ b/packages/cli/test/cases/build.default.import-node-modules/package.json @@ -0,0 +1,10 @@ +{ + "name": "test-import-node-modules", + "dependencies": { + "lit-element": "^2.4.0", + "lodash-es": "^4.17.20", + "pwa-helpers": "^0.9.1", + "redux": "^4.0.5", + "simpledotcss": "^1.0.0" + } +} \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.import-node-modules/src/pages/index.html b/packages/cli/test/cases/build.default.import-node-modules/src/pages/index.html new file mode 100644 index 000000000..a89645bb5 --- /dev/null +++ b/packages/cli/test/cases/build.default.import-node-modules/src/pages/index.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="../scripts/main.js"></script> + <script type="module"> + import { html } from 'lit-element'; + + document.getElementsByClassName('output-script-inline')[0].innerHTML = 'script tag module inline'; + </script> + <script type="module" src="/node_modules/lit-html/lit-html.js"></script> + <link rel="stylesheet" href="/node_modules/prismjs/themes/prism-tomorrow.css"></link> + + </head> + + <body> + <span class="output-lit"></span> + <span class="output-lodash"></span> + <span class="output-pwa"></span> + <span class="output-redux"></span> + <span class="output-script-inline"></span> + </body> + +</html> \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.import-node-modules/src/scripts/main.js b/packages/cli/test/cases/build.default.import-node-modules/src/scripts/main.js new file mode 100644 index 000000000..dd58e67f4 --- /dev/null +++ b/packages/cli/test/cases/build.default.import-node-modules/src/scripts/main.js @@ -0,0 +1,9 @@ +import { LitElement } from 'lit-element'; +import { defaults } from 'lodash-es'; +import { lazyReducerEnhancer } from 'pwa-helpers'; +import { createStore } from 'redux'; + +document.getElementsByClassName('output-lit')[0].innerHTML = `import from lit-element ${btoa(LitElement).slice(0, 16)}`; +document.getElementsByClassName('output-lodash')[0].innerHTML = `import from lodash-es ${JSON.stringify(defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }))}`; +document.getElementsByClassName('output-pwa')[0].innerHTML = `import from pwa-helpers ${btoa(lazyReducerEnhancer).slice(0, 16)}`; +document.getElementsByClassName('output-redux')[0].innerHTML = `import from redux ${btoa(createStore).slice(0, 16)}`; \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.markdown/build.default.markdown.spec.js b/packages/cli/test/cases/build.default.markdown/build.default.markdown.spec.js new file mode 100644 index 000000000..88b457597 --- /dev/null +++ b/packages/cli/test/cases/build.default.markdown/build.default.markdown.spec.js @@ -0,0 +1,84 @@ +/* + * Use Case + * Run Greenwood with custom markdown preset in greenwood config. + * + * User Result + * Should generate a bare bones Greenwood build. (same as build.default.spec.js) with custom markdown and rehype links + * + * User Command + * greenwood build + * + * User Config + * Default + * + * User Workspace + * Greenwood default + */ +const { JSDOM } = require('jsdom'); +const path = require('path'); +const expect = require('chai').expect; +const runSmokeTest = require('../../../../../test/smoke-test'); +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'Markdown'; + 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'], LABEL); + + describe('Markdown Rendering', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + it('should correctly rendering an <h3> tag', function() { + const heading = dom.window.document.querySelectorAll('body h3'); + + expect(heading.length).to.equal(1); + expect(heading[0].textContent).to.equal('Greenwood Markdown Test'); + }); + + it('should correctly render a <p> tag', function() { + const paragraph = dom.window.document.querySelectorAll('body p'); + + expect(paragraph.length).to.equal(1); + expect(paragraph[0].textContent).to.be.equal('This is some markdown being rendered by Greenwood.'); + }); + + it('should correctly render markdown with a <code> tag', function() { + const code = dom.window.document.querySelectorAll('body pre code'); + + expect(code.length).to.equal(1); + expect(code[0].textContent).to.contain('console.log(\'hello world\');'); + }); + + it('should correctly render markdown with an HTML <img> tag in it', function() { + const images = dom.window.document.querySelectorAll('body img'); + const myImage = images[0]; + + expect(images.length).to.equal(1); + expect(myImage.src).to.contain('my-image.png'); + expect(myImage.alt).to.equal('just passing by'); + }); + }); + + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file 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 new file mode 100644 index 000000000..66ce731f5 --- /dev/null +++ b/packages/cli/test/cases/build.default.markdown/src/pages/index.md @@ -0,0 +1,9 @@ +### Greenwood Markdown Test + +This is some markdown being rendered by Greenwood. + +``` +console.log('hello world'); +``` + +<img src="#my-image.png" alt="just passing by"/> \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.webpack/build.default.webpack.spec.js b/packages/cli/test/cases/build.default.webpack/build.default.webpack.spec.js deleted file mode 100644 index 93d1d8345..000000000 --- a/packages/cli/test/cases/build.default.webpack/build.default.webpack.spec.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Use Case - * Run Greenwood with a custom webpack config and default workspace. - * - * User Result - * Should generate a bare bones Greenwood build with custom webpack changes - * - * User Command - * greenwood build - * - * User Workspace - * Greenwood default - */ -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'); - -describe('Build Greenwood With: ', function() { - const mockBanner = `My Banner - v${version}`; - const LABEL = 'Custom Webpack Configuration'; - 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('using a custom webpack.config.common.js with 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.default.webpack/webpack.config.common.js b/packages/cli/test/cases/build.default.webpack/webpack.config.common.js deleted file mode 100644 index 7d51c1b57..000000000 --- a/packages/cli/test/cases/build.default.webpack/webpack.config.common.js +++ /dev/null @@ -1,173 +0,0 @@ -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const fs = require('fs'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const path = require('path'); -const webpack = require('webpack'); -const { version } = require('../../../package.json'); - -const getUserWorkspaceDirectories = (source) => { - return fs.readdirSync(source) - .map(name => path.join(source, name)) - .filter(path => fs.lstatSync(path).isDirectory()); -}; -const mapUserWorkspaceDirectories = (directoryPath, userWorkspaceDirectory) => { - const directoryName = directoryPath.replace(`${userWorkspaceDirectory}/`, ''); - const userWorkspaceDirectoryRoot = userWorkspaceDirectory.split('/').slice(-1); - - return new webpack.NormalModuleReplacementPlugin( - // https://github.com/ProjectEvergreen/greenwood/issues/132 - new RegExp(`\\.\\.\\/${directoryName}.+$(?<!\.js)|${userWorkspaceDirectoryRoot}\\/${directoryName}.+$(?<!\.js)`), - (resource) => { - - // workaround to ignore cli/templates default imports when rewriting - if (!new RegExp('\/cli\/templates').test(resource.content)) { - resource.request = resource.request.replace(new RegExp(`\\.\\.\\/${directoryName}`), directoryPath); - } - - // 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); - } - } - - ); -}; - -module.exports = ({ config, context }) => { - const { userWorkspace } = context; - - // dynamically map all the user's workspace directories for resolution by webpack - // this essentially helps us keep watch over changes from the user's workspace forgreenwood's build pipeline - const mappedUserDirectoriesForWebpack = getUserWorkspaceDirectories(userWorkspace) - .map((directory) => { - return mapUserWorkspaceDirectories(directory, userWorkspace); - }); - - // if user has an assets/ directory in their workspace, automatically copy it for them - const userAssetsDirectoryForWebpack = fs.existsSync(context.assetDir) ? [{ - from: context.assetDir, - to: path.join(context.publicDir, 'assets') - }] : []; - - const commonCssLoaders = [ - { loader: 'css-loader' }, - { - loader: 'postcss-loader', - options: { - config: { - path: path.join(__dirname, '../../..', './src/config', 'postcss.config.js') - } - } - } - ]; - - // gets Index Hooks to pass as options to HtmlWebpackPlugin - const customOptions = Object.assign({}, ...config.plugins - .filter((plugin) => plugin.type === 'index') - .map((plugin) => plugin.provider({ config, context })) - .filter((providerResult) => { - return Object.keys(providerResult).map((key) => { - if (key !== 'type') { - return providerResult[key]; - } - }); - })); - - // utilizes webpack plugins passed in directly by the user - const customWebpackPlugins = config.plugins - .filter((plugin) => plugin.type === 'webpack') - .map((plugin) => plugin.provider({ config, context })); - - return { - - resolve: { - extensions: ['.js', '.json', '.gql', '.graphql'], - alias: { - '@greenwood/cli/data': path.join(__dirname, '../../..', './src', './data'), - '@greenwood/cli/templates/app-template': path.join(context.scratchDir, 'app', 'app-template.js') - } - }, - - entry: { - index: path.join(context.scratchDir, 'app', 'app.js') - }, - - output: { - path: path.join(context.publicDir, '.', config.publicPath), - filename: '[name].[hash].bundle.js', - chunkFilename: '[name].[hash].bundle.js', - publicPath: config.publicPath - }, - - module: { - rules: [{ - test: /\.js$/, - loader: 'babel-loader', - options: { - configFile: path.join(__dirname, '../../..', './src/config', 'babel.config.js') - } - }, { - test: /\.md$/, - loader: 'wc-markdown-loader', - options: { - defaultStyle: false, - shadowRoot: false - } - }, { - test: /\.css$/, - exclude: new RegExp(`${config.themeFile}`), - loaders: [ - { loader: 'css-to-string-loader' }, - ...commonCssLoaders - ] - }, { - test: new RegExp(`${config.themeFile}`), - loaders: [ - { loader: 'style-loader' }, - ...commonCssLoaders - ] - }, { - test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, - loader: 'url-loader?limit=10000&mimetype=application/font-woff' - }, { - test: /\.(ttf|eot|svg|jpe?g|png|gif|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/, - loader: 'file-loader' - }, { - test: /\.(graphql|gql)$/, - loader: 'graphql-tag/loader' - }] - }, - - plugins: [ - new HtmlWebpackPlugin({ - filename: path.join(context.publicDir, context.indexPageTemplate), - template: path.join(context.scratchDir, context.indexPageTemplate), - chunksSortMode: 'dependency', - ...customOptions - }), - - new HtmlWebpackPlugin({ - filename: path.join(context.publicDir, context.notFoundPageTemplate), - template: path.join(context.scratchDir, context.notFoundPageTemplate), - chunksSortMode: 'dependency', - ...customOptions - }), - - ...mappedUserDirectoriesForWebpack, - - new CopyWebpackPlugin(userAssetsDirectoryForWebpack), - - new webpack.NormalModuleReplacementPlugin( - /\.md/, - (resource) => { - resource.request = resource.request.replace(/^\.\//, context.pagesDir); - } - ), - /// Custom plugin added to prove custom config is being used - new webpack.BannerPlugin(`My Banner - v${version}`), - ...customWebpackPlugins - ] - }; -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.webpack/webpack.config.develop.js b/packages/cli/test/cases/build.default.webpack/webpack.config.develop.js deleted file mode 100644 index ccf19c428..000000000 --- a/packages/cli/test/cases/build.default.webpack/webpack.config.develop.js +++ /dev/null @@ -1,93 +0,0 @@ -const path = require('path'); -const ManifestPlugin = require('webpack-manifest-plugin'); -const FilewatcherPlugin = require('filewatcher-webpack-plugin'); -const generateCompilation = require('../lifecycles/compile'); -const webpackMerge = require('webpack-merge'); -const commonConfig = require('./webpack.config.common.js'); - -let isRebuilding = false; - -const rebuild = async() => { - if (!isRebuilding) { - isRebuilding = true; - - // rebuild web components - await generateCompilation(); - - // debounce - setTimeout(() => { - isRebuilding = false; - }, 1000); - } -}; - -module.exports = ({ config, context, graph }) => { - config.publicPath = '/'; - - const configWithContext = commonConfig({ config, context, graph }); - const { devServer, publicPath } = config; - const { host, port } = devServer; - - // decorate HtmlWebpackPlugin instance with devServer specific SPA handling for index.html - configWithContext.plugins[0].options.hookGreenwoodSpaIndexFallback = ` - <script> - (function(){ - var redirect = sessionStorage.redirect; - - delete sessionStorage.redirect; - - if (redirect && redirect != location.href) { - history.replaceState(null, null, redirect); - } - })(); - </script> - `, - - // decorate HtmlWebpackPlugin instance with devServer specific SPA handling for 404.html - configWithContext.plugins[1].options.hookGreenwoodSpaIndexFallback = ` - <script> - sessionStorage.redirect = location.href; - </script> - - <meta http-equiv="refresh" content="0;URL='${publicPath}'"></meta> - `; - - return webpackMerge(configWithContext, { - - mode: 'development', - - entry: [ - `webpack-dev-server/client?http://${host}:${port}`, - path.join(context.scratchDir, 'app', 'app.js') - ], - - devServer: { - port, - host, - disableHostCheck: true, - historyApiFallback: true, - hot: false, - inline: true - }, - - plugins: [ - new FilewatcherPlugin({ - watchFileRegex: [`/${context.userWorkspace}/`], - onReadyCallback: () => { - console.log(`Now serving Development Server available at ${host}:${port}`); - }, - onChangeCallback: async () => { - rebuild(); - }, - usePolling: true, - atomic: true, - ignored: '/node_modules/' - }), - - new ManifestPlugin({ - fileName: 'manifest.json', - publicPath - }) - ] - }); -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.webpack/webpack.config.prod.js b/packages/cli/test/cases/build.default.webpack/webpack.config.prod.js deleted file mode 100644 index 62764d0d7..000000000 --- a/packages/cli/test/cases/build.default.webpack/webpack.config.prod.js +++ /dev/null @@ -1,17 +0,0 @@ -const webpackMerge = require('webpack-merge'); -const commonConfig = require('./webpack.config.common.js'); - -module.exports = ({ config, context, graph }) => { - const configWithContext = commonConfig({ config, context, graph }); - - return webpackMerge(configWithContext, { - - mode: 'production', - - performance: { - hints: 'warning' - } - - }); - -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-assets/build.default.workspace-assets.spec.js b/packages/cli/test/cases/build.default.workspace-assets/build.default.workspace-assets.spec.js index f6b479c2a..bd62e41eb 100644 --- a/packages/cli/test/cases/build.default.workspace-assets/build.default.workspace-assets.spec.js +++ b/packages/cli/test/cases/build.default.workspace-assets/build.default.workspace-assets.spec.js @@ -32,7 +32,7 @@ describe('Build Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'index', 'not-found'], LABEL); + runSmokeTest(['public', 'index'], LABEL); describe('Assets folder', function() { 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 c7d6f392c..4f6b8ab25 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 @@ -19,19 +19,22 @@ * footer.js * header.js * pages/ - * first-post.md - * second-post.md + * blog/ + * first-post.md + * second-post.md + * index.md * styles/ * theme.css * templates/ - * app-template.js - * blog-template.js + * app.html + * blog.html */ const expect = require('chai').expect; const fs = require('fs'); const glob = require('glob-promise'); const { JSDOM } = require('jsdom'); const path = require('path'); +const runSmokeTest = require('../../../../../test/smoke-test'); const TestBed = require('../../../../../test/test-bed'); describe('Build Greenwood With: ', function() { @@ -44,17 +47,18 @@ describe('Build Greenwood With: ', function() { }); describe(LABEL, function() { + before(async function() { await setup.runGreenwoodCommand('build'); }); + runSmokeTest(['public'], LABEL); + describe('Folder Structure and Home Page', function() { - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); - }); + let dom; - it('should create a public directory', function() { - expect(fs.existsSync(this.context.publicDir)).to.be.true; + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); }); it('should create a new assets directory', function() { @@ -65,88 +69,204 @@ describe('Build Greenwood With: ', function() { expect(fs.existsSync(path.join(this.context.publicDir, 'assets', './greenwood-logo.png'))).to.be.true; }); - it('should output an index.html file (home page)', function() { - expect(fs.existsSync(path.join(this.context.publicDir, './index.html'))).to.be.true; + it('should output two JS bundle files', async function() { + expect(await glob.promise(path.join(this.context.publicDir, './*.js'))).to.have.lengthOf(2); + }); + + it('should have two <script> tags in the <head>', async function() { + const scriptTags = dom.window.document.querySelectorAll('head script'); + + expect(scriptTags.length).to.be.equal(2); }); - it('should output a single 404.html file (not found page)', function() { - expect(fs.existsSync(path.join(this.context.publicDir, './404.html'))).to.be.true; + it('should output one CSS file', async function() { + expect(await glob.promise(`${path.join(this.context.publicDir, 'styles')}/theme.*.css`)).to.have.lengthOf(1); }); - it('should output one JS bundle file', async function() { - expect(await glob.promise(path.join(this.context.publicDir, './index.*.bundle.js'))).to.have.lengthOf(1); + it('should output two <style> tag in the <head> (one from puppeteer)', async function() { + const styleTags = dom.window.document.querySelectorAll('head style'); + + expect(styleTags.length).to.be.equal(2); + }); + + it('should output one <link> tag in the <head>', async function() { + const linkTags = dom.window.document.querySelectorAll('head link[rel="stylesheet"]'); + + expect(linkTags.length).to.be.equal(1); + }); + + it('should have content in the <body>', function() { + const h2 = dom.window.document.querySelector('body h2'); + const p = dom.window.document.querySelector('body p'); + const h3 = dom.window.document.querySelector('body h3'); + + expect(h2.textContent).to.be.equal('Home Page'); + expect(p.textContent).to.be.equal('This is the Getting Started home page!'); + expect(h3.textContent).to.be.equal('My Posts'); + }); + + it('should have an unordered list of blog posts in the <body>', function() { + const ul = dom.window.document.querySelectorAll('body ul'); + const li = dom.window.document.querySelectorAll('body ul li'); + const links = dom.window.document.querySelectorAll('body ul a'); + + expect(ul.length).to.be.equal(1); + expect(li.length).to.be.equal(2); + expect(links.length).to.be.equal(2); + + expect(links[0].href.replace('file://', '')).to.be.equal('/blog/second-post/'); + expect(links[0].textContent).to.be.equal('my-second-post'); + + expect(links[1].href.replace('file://', '')).to.be.equal('/blog/first-post/'); + expect(links[1].textContent).to.be.equal('my-first-post'); }); it('should have a <header> tag in the <body>', function() { const header = dom.window.document.querySelectorAll('body header'); expect(header.length).to.be.equal(1); + expect(header[0].textContent).to.be.equal('This is the header component.'); }); it('should have a <footer> tag in the <body>', function() { const footer = dom.window.document.querySelectorAll('body footer'); expect(footer.length).to.be.equal(1); + expect(footer[0].textContent).to.be.equal('This is the footer component.'); + }); + }); + + describe('First Blog Post', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'blog/first-post/index.html')); + }); + + it('should create a blog directory', function() { + expect(fs.existsSync(path.join(this.context.publicDir, 'blog'))).to.be.true; + }); + + it('should output an index.html file for first-post page', function() { + expect(fs.existsSync(path.join(this.context.publicDir, 'blog', 'first-post', './index.html'))).to.be.true; }); + + it('should have two <script> tags in the <head>', async function() { + const scriptTags = dom.window.document.querySelectorAll('head script'); - it('should have the expected font import', function() { - const styles = '@import url(//fonts.googleapis.com/css?family=Source+Sans+Pro&display=swap);'; + expect(scriptTags.length).to.be.equal(2); + }); + + it('should output one <style> tag in the <head> (one from puppeteer)', async function() { const styleTags = dom.window.document.querySelectorAll('head style'); - let importCount = 0; - styleTags.forEach((tag) => { - if (tag.textContent.indexOf(styles) >= 0) { - importCount += 1; - } - }); + expect(styleTags.length).to.be.equal(1); + }); - expect(importCount).to.equal(1); + it('should output one <link> tag in the <head>', async function() { + const linkTags = dom.window.document.querySelectorAll('head link[rel="stylesheet"]'); + + expect(linkTags.length).to.be.equal(1); + }); + + it('should have a <header> tag in the <body>', function() { + const header = dom.window.document.querySelectorAll('body header'); + + expect(header.length).to.be.equal(1); + expect(header[0].textContent).to.be.equal('This is the header component.'); + }); + + it('should have an the expected content in the <body>', 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'); + expect(p[1].textContent).to.be.equal('back'); + }); + + it('should have a <footer> tag in the <body>', function() { + const footer = dom.window.document.querySelectorAll('body footer'); + + expect(footer.length).to.be.equal(1); + expect(footer[0].textContent).to.be.equal('This is the footer component.'); + }); + + it('should have the expected content for the first blog post', function() { + const footer = dom.window.document.querySelectorAll('body footer'); + + expect(footer.length).to.be.equal(1); + expect(footer[0].textContent).to.be.equal('This is the footer component.'); }); }); - describe('Blog Posts', function() { + describe('Second Blog Post', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'blog/second-post/index.html')); + }); + it('should create a blog directory', function() { expect(fs.existsSync(path.join(this.context.publicDir, 'blog'))).to.be.true; }); - it('should output an index.html file (home page)', function() { - expect(fs.existsSync(path.join(this.context.publicDir, 'blog', 'first-post', './index.html'))).to.be.true; + it('should output an index.html file for first-post page', function() { + expect(fs.existsSync(path.join(this.context.publicDir, 'blog', 'second-post', './index.html'))).to.be.true; }); + + it('should have two <script> tags in the <head>', async function() { + const scriptTags = dom.window.document.querySelectorAll('head script'); - it('should output a single 404.html file (not found page)', function() { - expect(fs.existsSync(path.join(this.context.publicDir, 'blog', 'second-post', './index.html'))).to.be.true; + expect(scriptTags.length).to.be.equal(2); }); - it('should output one JS bundle file', async function() { - expect(await glob.promise(path.join(this.context.publicDir, './index.*.bundle.js'))).to.have.lengthOf(1); + it('should output one <style> tag in the <head> (one from puppeteer)', async function() { + const styleTags = dom.window.document.querySelectorAll('head style'); + + expect(styleTags.length).to.be.equal(1); + }); + + it('should output one <link> tag in the <head>', async function() { + const linkTags = dom.window.document.querySelectorAll('head link[rel="stylesheet"]'); + + expect(linkTags.length).to.be.equal(1); }); - it('should have a <header> tag in the <body> in first-post.md', async function() { - const dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'blog', 'first-post', 'index.html')); + it('should have a <header> tag in the <body>', function() { const header = dom.window.document.querySelectorAll('body header'); expect(header.length).to.be.equal(1); + expect(header[0].textContent).to.be.equal('This is the header component.'); }); - it('should have a <footer> tag in the <body>', async function() { - const dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'blog', 'first-post', 'index.html')); + it('should have an the expected content in the <body>', 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'); + expect(p[1].textContent).to.be.equal('back'); + }); + + it('should have a <footer> tag in the <body>', function() { const footer = dom.window.document.querySelectorAll('body footer'); expect(footer.length).to.be.equal(1); + expect(footer[0].textContent).to.be.equal('This is the footer component.'); }); - it('should have the expected font import', function() { - const styles = '@import url(//fonts.googleapis.com/css?family=Source+Sans+Pro&display=swap);'; - const styleTags = dom.window.document.querySelectorAll('head style'); - let importCount = 0; - - styleTags.forEach((tag) => { - if (tag.textContent.indexOf(styles) >= 0) { - importCount += 1; - } - }); + it('should have the expected content for the first blog post', function() { + const footer = dom.window.document.querySelectorAll('body footer'); - expect(importCount).to.equal(1); + expect(footer.length).to.be.equal(1); + expect(footer[0].textContent).to.be.equal('This is the footer component.'); }); }); diff --git a/packages/cli/test/cases/build.default.workspace-getting-started/src/components/footer.js b/packages/cli/test/cases/build.default.workspace-getting-started/src/components/footer.js index 083ebf66a..7db5e62f2 100644 --- a/packages/cli/test/cases/build.default.workspace-getting-started/src/components/footer.js +++ b/packages/cli/test/cases/build.default.workspace-getting-started/src/components/footer.js @@ -21,4 +21,4 @@ class FooterComponent extends HTMLElement { } } -customElements.define('app-footer', FooterComponent); +customElements.define('app-footer', FooterComponent); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-getting-started/src/components/header.js b/packages/cli/test/cases/build.default.workspace-getting-started/src/components/header.js index 1db57c902..c7f82989e 100644 --- a/packages/cli/test/cases/build.default.workspace-getting-started/src/components/header.js +++ b/packages/cli/test/cases/build.default.workspace-getting-started/src/components/header.js @@ -21,4 +21,4 @@ class HeaderComponent extends HTMLElement { } } -customElements.define('app-header', HeaderComponent); +customElements.define('app-header', HeaderComponent); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-getting-started/src/templates/blog-template.js b/packages/cli/test/cases/build.default.workspace-getting-started/src/templates/blog-template.js deleted file mode 100644 index d21cfd5eb..000000000 --- a/packages/cli/test/cases/build.default.workspace-getting-started/src/templates/blog-template.js +++ /dev/null @@ -1,26 +0,0 @@ -import { html, LitElement } from 'lit-element'; -import '../components/footer'; -import '../components/header'; - -class BlogTemplate extends LitElement { - - constructor() { - super(); - } - - render() { - return html` - <style> - - </style> - - <div class='container'> - <app-header></app-header> - <entry></entry> - <app-footer></app-footer> - </div> - `; - } -} - -customElements.define('page-template', BlogTemplate); \ No newline at end of file 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 new file mode 100644 index 000000000..ea436fe72 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-getting-started/src/templates/blog.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/components/footer.js"></script> + <script type="module" src="/components/header.js"></script> + + <link rel="stylesheet" href="/styles/theme.css"></link> + </head> + + <body> + + <div class="gwd-content-outlet"> + <div class='container'> + <app-header></app-header> + <h1>A Blog Post Page</h1> + <content-outlet></content-outlet> + <app-footer></app-footer> + </div> + </div> + + </body> + +</html> \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-getting-started/src/templates/page-template.js b/packages/cli/test/cases/build.default.workspace-getting-started/src/templates/page-template.js deleted file mode 100644 index 3bb9c1b8c..000000000 --- a/packages/cli/test/cases/build.default.workspace-getting-started/src/templates/page-template.js +++ /dev/null @@ -1,32 +0,0 @@ -import { html, LitElement } from 'lit-element'; -import '../components/footer'; -import '../components/header'; -import '../styles/theme.css'; - -class PageTemplate extends LitElement { - - constructor() { - super(); - } - - render() { - return html` - - <style> - section { - margin: 0 auto; - width: 70%; - } - </style> - - - <div> - <app-header></app-header> - <entry></entry> - <app-footer></app-footer> - </div> - `; - } -} - -customElements.define('page-template', PageTemplate); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-getting-started/src/templates/page.html b/packages/cli/test/cases/build.default.workspace-getting-started/src/templates/page.html new file mode 100644 index 000000000..a12cef0fc --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-getting-started/src/templates/page.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/components/footer.js"></script> + <script type="module" src="/components/header.js"></script> + + <link rel="stylesheet" href="/styles/theme.css"></link> + + <style> + section { + margin: 0 auto; + width: 70%; + } + </style> + + </head> + + <body> + + <div class="gwd-content-outlet"> + <div> + <app-header></app-header> + <content-outlet></content-outlet> + <app-footer></app-footer> + </div> + </div> + + </body> + +</html> \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-javascript-css-remote/build.default.workspace-javascript-css-remote.spec.js b/packages/cli/test/cases/build.default.workspace-javascript-css-remote/build.default.workspace-javascript-css-remote.spec.js new file mode 100644 index 000000000..b25d67d26 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-javascript-css-remote/build.default.workspace-javascript-css-remote.spec.js @@ -0,0 +1,107 @@ +/* + * Use Case + * Run Greenwood with various usages of JavaScript (<script>) and CSS (<style> / <link>) tags with remove links. + * + * Uaer Result + * Should generate a bare bones Greenwood build without erroring. + * + * User Command + * greenwood build + * + * User Config + * None + * + * User Workspace + * src/ + * pages/ + * index.html + * + */ +const expect = require('chai').expect; +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 = 'Loading remote JavaScript and CSS using <script> and <link> tags'; + + let setup; + + before(async function() { + setup = new TestBed(); + + this.context = await setup.setupTestBed(__dirname); + }); + + describe(LABEL, function() { + let dom; + + before(async function() { + await setup.runGreenwoodCommand('build'); + + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + describe('it should have the expected files in the output directory', function() { + it('should output no javascript files', async function() { + expect(await glob.promise(path.join(this.context.publicDir, '*.js'))).to.have.lengthOf(0); + }); + + it('should output no css files', async function() { + expect(await glob.promise(path.join(this.context.publicDir, '*.css'))).to.have.lengthOf(0); + }); + }); + + describe('two <script></script> tags with remote URLs in the <head>', function() { + it('should have two <script src="..."> tags in the <head>', function() { + const scriptTags = dom.window.document.querySelectorAll('head > script'); + const mainScriptTags = Array.prototype.slice.call(scriptTags).filter(script => { + return (/http/).test(script.src); + }); + + expect(mainScriptTags.length).to.be.equal(2); + }); + + it('should have one CDN <script src="..."> tag for jquery in the <head> using http', function() { + const scriptTags = dom.window.document.querySelectorAll('head > script'); + const jqueryScriptTag = Array.prototype.slice.call(scriptTags).filter(script => { + return (/http:\/\/code.jquery.com\//).test(script.src); + }); + + expect(jqueryScriptTag.length).to.be.equal(1); + }); + + it('should have one UNPKG <script src="..." type="module"> tag for LitElements in the <head> using https', function() { + const scriptTags = dom.window.document.querySelectorAll('head > script'); + const unpkgScriptTag = Array.prototype.slice.call(scriptTags).filter(script => { + return (/https:\/\/unpkg.com\//).test(script.src); + }); + + expect(unpkgScriptTag.length).to.be.equal(1); + }); + }); + + describe('<link rel="stylesheet" href="..."/> tag in the <head>', function() { + it('should have one <link> tag in the <head> with no protocol specified', function() { + const linkTags = dom.window.document.querySelectorAll('head > link'); + + expect(linkTags.length).to.be.equal(1); + }); + + it('should have one <link> tag for google fonts in the <head> with no protocol specified', async function() { + const linkTags = dom.window.document.querySelectorAll('head > link'); + const fontsLinkTag = Array.prototype.slice.call(linkTags).filter(link => { + return (/\/\/fonts.googleapis.com\//).test(link.href); + }); + + expect(fontsLinkTag.length).to.be.equal(1); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-javascript-css-remote/src/pages/index.html b/packages/cli/test/cases/build.default.workspace-javascript-css-remote/src/pages/index.html new file mode 100644 index 000000000..5e33361e6 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-javascript-css-remote/src/pages/index.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto&display=swap"></link> + <script src="http://code.jquery.com/jquery-3.6.0.min.js"></script> + <script type="module" src="https://unpkg.com/lit-element@2.4.0/lit-element.js"></script> + </head> + + <body> + <h1>Hello!</h1> + </body> + +</html> \ No newline at end of file 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 new file mode 100644 index 000000000..43686ee9a --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-javascript-css/build.default.workspace-javascript-css.spec.js @@ -0,0 +1,151 @@ +/* + * Use Case + * Run Greenwood with various usages of JavaScript (<script>) and CSS (<style> / <link>) tags, with inlined or file based content. + * + * Uaer Result + * Should generate a bare bones Greenwood build without erroring. + * + * User Command + * greenwood build + * + * User Config + * None + * + * User Workspace + * src/ + * pages/ + * index.html + * scripts/ + * main.js + * styles/ + * main.css + * + */ +const expect = require('chai').expect; +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 = 'Importing JavaScript and CSS using <script>, <style>, and <link> tags'; + + let setup; + + before(async function() { + setup = new TestBed(); + + this.context = await setup.setupTestBed(__dirname); + }); + + describe(LABEL, function() { + let dom; + + before(async function() { + await setup.runGreenwoodCommand('build'); + + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + describe('<script type="module" src="..."></script> tag in the <head>', function() { + it('should have one <script> tag for main.js loaded in the <head>', function() { + const scriptTags = dom.window.document.querySelectorAll('head > script[src]'); + const mainScriptTags = Array.prototype.slice.call(scriptTags).filter(script => { + return (/main.*.js/).test(script.src); + }); + + expect(mainScriptTags.length).to.be.equal(1); + }); + + it('should have one <script> tag for other.js loaded in the <head>', function() { + const scriptTags = dom.window.document.querySelectorAll('head > script[src]'); + const mainScriptTags = Array.prototype.slice.call(scriptTags).filter(script => { + return (/other.*.js/).test(script.src); + }); + + expect(mainScriptTags.length).to.be.equal(1); + }); + + it('should have the expected number of bundled .js files in the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, '*.js'))).to.have.lengthOf(2); + }); + + it('should have the expected main.js file in the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, 'main.*.js'))).to.have.lengthOf(1); + }); + + it('should have the expected other.js file in the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, 'other.*.js'))).to.have.lengthOf(1); + }); + + it('should have the expected output from main.js file in index.html', async function() { + const scriptTagSrc = dom.window.document.querySelector('body > .output-script-src'); + + expect(scriptTagSrc.textContent).to.be.equal('script tag module with src'); + }); + }); + + describe('<script>...</script> tag in the <head>', function() { + it('should have one <script> tag with inline script in the <head>', function() { + const scriptTagInline = dom.window.document.querySelectorAll('head > script:not([src])'); + + expect(scriptTagInline.length).to.be.equal(1); + }); + + it('should have the expected output from inline <script> tag in index.html', async function() { + const scriptTagSrc = dom.window.document.querySelector('body > .output-script-inline'); + + expect(scriptTagSrc.textContent).to.be.equal('script tag module inline'); + }); + }); + + describe('<style>...</style> tag in the <head>', function() { + it('should have one <style> tag in the <head>', function() { + const styleTags = dom.window.document.querySelectorAll('head > style'); + + // first <style> tag comes from puppeteer output + expect(styleTags.length).to.be.equal(2); + }); + + it('should have the expected output from main.js file in index.html', async function() { + const styleTags = dom.window.document.querySelectorAll('head > style'); + + // first <style> tag comes from puppeteer output + expect(styleTags[1].textContent.replace(/\n/g, '').trim().replace(' ', '')).to.be.contain('p.output-style{ color: green; }'); + }); + + it('should have the color style for the output element', function() { + const output = dom.window.document.querySelector('body > p.output-style'); + const computedStyle = dom.window.getComputedStyle(output); + + expect(computedStyle.color).to.equal('green'); + }); + }); + + describe('<link rel="stylesheet" href="..."/> tag in the <head>', function() { + it('should have one <link> tag in the <head>', function() { + const linkTags = dom.window.document.querySelectorAll('head > link[rel="stylesheet"]'); + + expect(linkTags.length).to.be.equal(1); + }); + + it('should have the expected main.css file in the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, 'styles', 'main.*.css'))).to.have.lengthOf(1); + }); + + // JSDOM may not support this case of computing styles when using a <link> tag? + // https://github.com/jsdom/jsdom/issues/2986 + xit('should have the color style for the output element', function() { + const output = dom.window.document.querySelector('body > p.output-link'); + const computedStyle = dom.window.getComputedStyle(output); + + expect(computedStyle.color).to.equal('blue'); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-javascript-css/src/pages/index.html b/packages/cli/test/cases/build.default.workspace-javascript-css/src/pages/index.html new file mode 100644 index 000000000..8f838962b --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-javascript-css/src/pages/index.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="../scripts/main.js"></script> + <script type="module" src="../scripts/other.js"></script> + + <script type="module"> + document.getElementsByClassName('output-script-inline')[0].innerHTML = 'script tag module inline'; + </script> + + <style> + p.output-style { + color: green; + } + </style> + + <link rel="stylesheet" href="../styles/main.css"></link> + </head> + + <body> + <p class="output-script-src"></p> + <p class="output-script-inline"></p> + <p class="output-style"></p> + <p class="output-link"></p> + </body> + +</html> \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-javascript-css/src/scripts/main.js b/packages/cli/test/cases/build.default.workspace-javascript-css/src/scripts/main.js new file mode 100644 index 000000000..c977bde13 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-javascript-css/src/scripts/main.js @@ -0,0 +1 @@ +document.getElementsByClassName('output-script-src')[0].innerHTML = 'script tag module with src'; \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-javascript-css/src/scripts/other.js b/packages/cli/test/cases/build.default.workspace-javascript-css/src/scripts/other.js new file mode 100644 index 000000000..4033200d0 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-javascript-css/src/scripts/other.js @@ -0,0 +1 @@ +console.debug('hello world'); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-javascript-css/src/styles/main.css b/packages/cli/test/cases/build.default.workspace-javascript-css/src/styles/main.css new file mode 100644 index 000000000..304570a59 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-javascript-css/src/styles/main.css @@ -0,0 +1,3 @@ +.output-link { + color: blue; +} \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/build.default.workspace-nested.spec.js b/packages/cli/test/cases/build.default.workspace-nested/build.default.workspace-nested.spec.js index c7ff9b846..9c31be611 100644 --- a/packages/cli/test/cases/build.default.workspace-nested/build.default.workspace-nested.spec.js +++ b/packages/cli/test/cases/build.default.workspace-nested/build.default.workspace-nested.spec.js @@ -1,9 +1,10 @@ /* * Use Case - * Run Greenwood with default config and nested directories in workspace. + * Run Greenwood with default config and nested directories in workspace with lots of nested pages. * * Result - * Test for correctly nested generated output. + * Test for correctly ordered graph.json and pages output, which by default should mimic + * the filesystem order by default. * * Command * greenwood build @@ -15,12 +16,33 @@ * src/ * pages/ * blog/ + * 2017/ + * 03/26/index.md + * 03/30/index.md + * 04/10/index.md + * 04/22/index.md + * 05/05/index.md + * 06/07/index.md + * 09/10/index.md + * 10/15/index.md + * 2018/ + * 01/24/index.md + * 05/16/index.md + * 06/06/index.md + * 09/26/index.md + * 10/28/index.md + * 11/19/index.md * 2019/ - * index.md + * 11/11/index.md + * 2020/ + * 04/07/index.md + * 08/15/index.md + * 10/28/index.md + * index.md + * index.md */ const expect = require('chai').expect; const fs = require('fs'); -const { JSDOM } = require('jsdom'); const path = require('path'); const TestBed = require('../../../../../test/test-bed'); @@ -38,33 +60,94 @@ describe('Build Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'not-found', 'index'], LABEL); + runSmokeTest(['public', 'index'], LABEL); - it('should create a default blog page directory', function() { - expect(fs.existsSync(path.join(this.context.publicDir, './blog'))).to.be.true; - }); + describe('Blog Pages Directory', function() { + let graph; - describe('Custom blog page directory', function() { - let dom; + before(async function() { + graph = require(path.join(this.context.publicDir, 'graph.json')); + }); - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'blog', '2019', './index.html')); + it('should have the expected ordering of pages in graph.json', function() { + expect(graph.length).to.equal(20); + expect(graph[0].path).to.be.equal('src/pages/blog/2017/03/26/index.md'); + expect(graph[1].path).to.be.equal('src/pages/blog/2017/03/30/index.md'); + expect(graph[2].path).to.be.equal('src/pages/blog/2017/04/10/index.md'); + expect(graph[3].path).to.be.equal('src/pages/blog/2017/04/22/index.md'); + expect(graph[4].path).to.be.equal('src/pages/blog/2017/05/05/index.md'); + expect(graph[5].path).to.be.equal('src/pages/blog/2017/06/07/index.md'); + expect(graph[6].path).to.be.equal('src/pages/blog/2017/09/10/index.md'); + expect(graph[7].path).to.be.equal('src/pages/blog/2017/10/15/index.md'); + expect(graph[8].path).to.be.equal('src/pages/blog/2018/01/24/index.md'); + expect(graph[9].path).to.be.equal('src/pages/blog/2018/05/16/index.md'); + expect(graph[10].path).to.be.equal('src/pages/blog/2018/06/06/index.md'); + expect(graph[11].path).to.be.equal('src/pages/blog/2018/09/26/index.md'); + expect(graph[12].path).to.be.equal('src/pages/blog/2018/10/28/index.md'); + expect(graph[13].path).to.be.equal('src/pages/blog/2018/11/19/index.md'); + expect(graph[14].path).to.be.equal('src/pages/blog/2019/11/11/index.md'); + expect(graph[15].path).to.be.equal('src/pages/blog/2020/04/07/index.md'); + expect(graph[16].path).to.be.equal('src/pages/blog/2020/08/15/index.md'); + expect(graph[17].path).to.be.equal('src/pages/blog/2020/10/28/index.md'); + expect(graph[18].path).to.be.equal('src/pages/blog/index.md'); + expect(graph[19].path).to.be.equal('src/pages/index.html'); }); - it('should output an index.html file within the default hello page directory', function() { - expect(fs.existsSync(path.join(this.context.publicDir, 'blog', '2019', './index.html'))).to.be.true; + it('should create a top level blog pages directory', function() { + expect(fs.existsSync(path.join(this.context.publicDir, './blog'))).to.be.true; + }); + + it('should create a directory for each year of blog pages', function() { + expect(fs.existsSync(path.join(this.context.publicDir, 'blog/2017'))).to.be.true; + expect(fs.existsSync(path.join(this.context.publicDir, 'blog/2018'))).to.be.true; + expect(fs.existsSync(path.join(this.context.publicDir, 'blog/2019'))).to.be.true; + expect(fs.existsSync(path.join(this.context.publicDir, 'blog/2020'))).to.be.true; }); - it('should have the expected heading text within the hello example page in the hello directory', function() { - const heading = dom.window.document.querySelector('h3').textContent; + it('should have the expected pages for 2017 blog pages', function() { + graph.filter((page) => { + return page.route.indexOf('2017') > 0; + }).forEach((page) => { + const outputpath = path.join(this.context.publicDir, page.route, 'index.html'); + expect(fs.existsSync(outputpath)).to.be.true; + }); + }); - expect(heading).to.equal('Blog Page'); + it('should have the expected pages for 2018 blog pages', function() { + graph.filter((page) => { + return page.route.indexOf('2018') > 0; + }).forEach((page) => { + const outputpath = path.join(this.context.publicDir, page.route, 'index.html'); + expect(fs.existsSync(outputpath)).to.be.true; + }); }); - it('should have the expected paragraph text within the hello example page in the hello directory', function() { - let paragraph = dom.window.document.querySelector('p').textContent; + it('should have the expected pages for 2019 blog pages', function() { + graph.filter((page) => { + return page.route.indexOf('2019') > 0; + }).forEach((page) => { + const outputpath = path.join(this.context.publicDir, page.route, 'index.html'); + expect(fs.existsSync(outputpath)).to.be.true; + }); + }); + + it('should have the expected pages for 2020 blog pages', function() { + graph.filter((page) => { + return page.route.indexOf('2020') > 0; + }).forEach((page) => { + const outputpath = path.join(this.context.publicDir, page.route, 'index.html'); + expect(fs.existsSync(outputpath)).to.be.true; + }); + }); - expect(paragraph).to.equal('This is the test blog page built by Greenwood.'); + it('should have the expected content for each blog page', function() { + graph.filter((page) => { + return page.route.indexOf(/\/blog\/[0-9]{4}/) > 0; + }).forEach((page) => { + const contents = fs.readFileSync(path.join(this.context.publicDir, page.route, 'index.html'), 'utf-8'); + + expect(contents).to.contain(`<p>This is the post for page ${page.data.date}.</p>`); + }); }); }); }); diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/03/26/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/03/26/index.md new file mode 100644 index 000000000..8026a4409 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/03/26/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '03.26.2017' +--- + +This is the post for page 03.26.2017. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/03/30/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/03/30/index.md new file mode 100644 index 000000000..97a10eb0f --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/03/30/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '03.30.2017' +--- + +This is the post for page 03.30.2017. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/04/10/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/04/10/index.md new file mode 100644 index 000000000..4948409a7 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/04/10/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '04.10.2017' +--- + +This is the post for page 04.10.2017. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/04/22/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/04/22/index.md new file mode 100644 index 000000000..898a217a1 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/04/22/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '04.22.2017' +--- + +This is the post for page 04.22.2017. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/05/05/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/05/05/index.md new file mode 100644 index 000000000..c002d13ea --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/05/05/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '05.05.2017' +--- + +This is the post for page 05.05.2017. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/06/07/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/06/07/index.md new file mode 100644 index 000000000..455f7e394 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/06/07/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '06.07.2017' +--- + +This is the post for page 06.07.2017. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/09/10/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/09/10/index.md new file mode 100644 index 000000000..db4f9a932 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/09/10/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '09.10.2017' +--- + +This is the post for page 09.10.2017. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/10/15/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/10/15/index.md new file mode 100644 index 000000000..28339f7e5 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2017/10/15/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '10.15.2017' +--- + +This is the post for page 10.15.2017. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/01/24/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/01/24/index.md new file mode 100644 index 000000000..1925e4ede --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/01/24/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '01.24.2018' +--- + +This is the post for page 01.24.2018. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/05/16/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/05/16/index.md new file mode 100644 index 000000000..0dfd92489 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/05/16/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '05.16.2018' +--- + +This is the post for page 05.16.2018. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/06/06/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/06/06/index.md new file mode 100644 index 000000000..6a4143047 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/06/06/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '06.06.2018' +--- + +This is the post for page 06.06.2018. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/09/26/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/09/26/index.md new file mode 100644 index 000000000..ae1d7e72a --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/09/26/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '09.26.2018' +--- + +This is the post for page 09.26.2018. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/10/28/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/10/28/index.md new file mode 100644 index 000000000..fa52eb033 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/10/28/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '10.28.2018' +--- + +This is the post for page 10.28.2018. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/11/19/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/11/19/index.md new file mode 100644 index 000000000..d13baf713 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2018/11/19/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '11.19.2018' +--- + +This is the post for page 11.19.2018. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2019/11/11/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2019/11/11/index.md new file mode 100644 index 000000000..4b273d6eb --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2019/11/11/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '11.11.2019' +--- + +This is the post for page 11.11.2019. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2019/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2019/index.md deleted file mode 100644 index 5ce82295d..000000000 --- a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2019/index.md +++ /dev/null @@ -1,8 +0,0 @@ -### Blog Page - -This is the test blog page built by Greenwood. - -```html -<style>${CSS}</style> -<eve-header></eve-header> -``` \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2020/04/07/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2020/04/07/index.md new file mode 100644 index 000000000..4dd9ad265 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2020/04/07/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '04.07.2020' +--- + +This is the post for page 04.07.2020. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2020/08/15/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2020/08/15/index.md new file mode 100644 index 000000000..9f1a40193 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2020/08/15/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '08.15.2020' +--- + +This is the post for page 08.15.2020. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2020/10/28/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2020/10/28/index.md new file mode 100644 index 000000000..4b273d6eb --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/2020/10/28/index.md @@ -0,0 +1,6 @@ +--- +title: 'Lorum Ipsum' +date: '11.11.2019' +--- + +This is the post for page 11.11.2019. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/index.md new file mode 100644 index 000000000..e124eb3df --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/blog/index.md @@ -0,0 +1,3 @@ +# Blog Posts + +See our blog posts! \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/index.html b/packages/cli/test/cases/build.default.workspace-nested/src/pages/index.html new file mode 100644 index 000000000..90598b163 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-nested/src/pages/index.html @@ -0,0 +1,8 @@ +<html> + <head> + <meta-outlet></meta-outlet> + </head> + <body> + <p>This is the home page built by Greenwood. Make your own pages in src/pages/index.js!</p> + </body> +</html> \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-nested/src/pages/index.md b/packages/cli/test/cases/build.default.workspace-nested/src/pages/index.md deleted file mode 100644 index 1c1a50fbb..000000000 --- a/packages/cli/test/cases/build.default.workspace-nested/src/pages/index.md +++ /dev/null @@ -1,3 +0,0 @@ -### 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/packages/cli/test/cases/build.default.workspace-template-app/build.default.workspace-template-app.spec.js b/packages/cli/test/cases/build.default.workspace-template-app/build.default.workspace-template-app.spec.js index 0050f2b45..80ce26758 100644 --- a/packages/cli/test/cases/build.default.workspace-template-app/build.default.workspace-template-app.spec.js +++ b/packages/cli/test/cases/build.default.workspace-template-app/build.default.workspace-template-app.spec.js @@ -14,7 +14,7 @@ * User Workspace * src/ * templates/ - * app-template.js + * app.html */ const expect = require('chai').expect; const fs = require('fs'); @@ -22,8 +22,6 @@ const { JSDOM } = require('jsdom'); const path = require('path'); const TestBed = require('../../../../../test/test-bed'); -const mainBundleScriptRegex = /index.*.bundle\.js/; - describe('Build Greenwood With: ', function() { const LABEL = 'Default Greenwood Configuration and Workspace w/Custom App Template'; let setup; @@ -40,85 +38,7 @@ describe('Build Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'not-found', 'hello'], LABEL); - - describe('Custom Index (Home) page', function() { - 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 have the default config title in the <title> tag in the <head>', function() { - const title = dom.window.document.querySelector('head title').textContent; - - expect(title).to.be.equal('My App'); - }); - - it('should have one <script> tag in the <body> for the main bundle', function() { - const scriptTags = dom.window.document.querySelectorAll('body > script'); - const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src.replace('file:///', ''); - - return mainBundleScriptRegex.test(src); - }); - - expect(bundledScript.length).to.be.equal(1); - }); - - it('should have one <script> tag in the <body> for the main bundle loaded with async', function() { - const scriptTags = dom.window.document.querySelectorAll('body > script'); - const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src.replace('file:///', ''); - - return mainBundleScriptRegex.test(src); - }); - - expect(bundledScript[0].getAttribute('async')).to.be.equal(''); - }); - - it('should have one <script> tag for Apollo state', function() { - const scriptTags = dom.window.document.querySelectorAll('script'); - const bundleScripts = Array.prototype.slice.call(scriptTags).filter(script => { - return script.getAttribute('data-state') === 'apollo'; - }); - - expect(bundleScripts.length).to.be.equal(1); - }); - - it('should have only one <script> tag in the <head>', function() { - const scriptTags = dom.window.document.querySelectorAll('head > script'); - - expect(scriptTags.length).to.be.equal(1); - }); - - it('should have a router outlet tag in the <body>', function() { - const outlet = dom.window.document.querySelectorAll('body app-root'); - - expect(outlet.length).to.be.equal(1); - }); - - // no 404 route in our custom app-template.js, like greenwood does - it('should have the correct route tags in the <body>', function() { - const routes = dom.window.document.querySelectorAll('body lit-route'); - - expect(routes.length).to.be.equal(2); - }); - - it('should have the expected heading text within the index page in the public directory', function() { - const heading = dom.window.document.querySelector('h3').textContent; - - expect(heading).to.equal(indexPageHeading); - }); - - it('should have the expected paragraph text within the index page in the public directory', function() { - let paragraph = dom.window.document.querySelector('p').textContent; - - expect(paragraph).to.equal(indexPageBody); - }); - }); + runSmokeTest(['public', 'index'], LABEL); describe('Custom App Template', function() { before(async function() { @@ -130,7 +50,7 @@ describe('Build Greenwood With: ', function() { }); it('should have the specific element we added as part of our custom app template', function() { - const customParagraph = dom.window.document.querySelector('p#custom-app-template').textContent; + const customParagraph = dom.window.document.querySelector('body p').textContent; expect(customParagraph).to.equal('My Custom App Template'); }); diff --git a/packages/cli/test/cases/build.default.workspace-template-app/src/templates/app-template.js b/packages/cli/test/cases/build.default.workspace-template-app/src/templates/app-template.js deleted file mode 100644 index d04a46b99..000000000 --- a/packages/cli/test/cases/build.default.workspace-template-app/src/templates/app-template.js +++ /dev/null @@ -1,13 +0,0 @@ -import { html, LitElement } from 'lit-element'; - -class AppComponent extends LitElement { - - render() { - return html` - <routes></routes> - <p id="custom-app-template">My Custom App Template</p> - `; - } -} - -customElements.define('eve-app', AppComponent); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-app/src/templates/app.html b/packages/cli/test/cases/build.default.workspace-template-app/src/templates/app.html new file mode 100644 index 000000000..fbf92892f --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-template-app/src/templates/app.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <meta charset='utf-8'> + <meta name='viewport' content='width=device-width, initial-scale=1'/> + </head> + + <body> + + <div class='gwd-wrapper'> + + <p>My Custom App Template</p> + + <page-outlet></page-outlet> + + </div> + + </body> + +</html> \ No newline at end of file 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 new file mode 100644 index 000000000..a480fcd1b --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-template-page-and-app/build.default.workspace-template-page-and-app.spec.js @@ -0,0 +1,125 @@ +/* + * Use Case + * Run Greenwood build command with no config and custom page (and app) template. + * + * User Result + * Should generate a bare bones Greenwood build with custom page template. + * + * User Command + * greenwood build + * + * User Config + * None (Greenwood Default) + * + * User Workspace + * src/ + * scripts/ + * app-template-one.js + * app-template-two.js + * page-template-one.js + * page-template-two.js + * styles/ + * app-template-one.css + * app-template-two.css + * page-template-one.css + * page-template-two.css + * templates/ + * app.html + * page.html + */ +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'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'Default Greenwood Configuration and Workspace w/Custom App and Page Templates'; + 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'], LABEL); + + describe('Custom App and Page Templates', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + it('should have the specific element we added as part of our custom page template', function() { + const customElement = dom.window.document.querySelectorAll('div.owen-test'); + + expect(customElement.length).to.equal(1); + }); + + describe('merge order for app and page template <head> tags', function() { + let scriptTags; + let linkTags; + let styleTags; + + before(function() { + scriptTags = dom.window.document.querySelectorAll('head > script'); + linkTags = dom.window.document.querySelectorAll('head > link[rel="stylesheet"'); + styleTags = dom.window.document.querySelectorAll('head > style'); + }); + + it('should have 4 <script> tags in the <head>', function() { + expect(scriptTags.length).to.equal(4); + }); + + it('should have 4 <link> tags in the <head>', function() { + expect(linkTags.length).to.equal(4); + }); + + it('should have 5 <style> tags in the <head> (4 + one from Puppeteer)', function() { + expect(styleTags.length).to.equal(5); + }); + + it('should merge page template <script> tags after app template <script> tags', function() { + expect(scriptTags[0].src).to.match(/app-template-one.*.js/); + expect(scriptTags[1].src).to.match(/app-template-two.*.js/); + expect(scriptTags[2].src).to.match(/page-template-one.*.js/); + expect(scriptTags[3].src).to.match(/page-template-two.*.js/); + + scriptTags.forEach((scriptTag) => { + expect(scriptTag.type).to.equal('module'); + }); + }); + + it('should merge page template <link> tags after app template <link> tags', function() { + expect(linkTags[0].href).to.match(/app-template-one.*.css/); + expect(linkTags[1].href).to.match(/app-template-two.*.css/); + expect(linkTags[2].href).to.match(/page-template-one.*.css/); + expect(linkTags[3].href).to.match(/page-template-two.*.css/); + + linkTags.forEach((linkTag) => { + expect(linkTag.rel).to.equal('stylesheet'); + }); + }); + + it('should merge page template <style> tags after app template <style> tags', function() { + // offset index by one since first <style> tag is from Puppeteer + expect(styleTags[1].textContent).to.contain('app-template-one-style'); + expect(styleTags[2].textContent).to.contain('app-template-two-style'); + expect(styleTags[3].textContent).to.contain('page-template-one-style'); + expect(styleTags[4].textContent).to.contain('page-template-two-style'); + }); + }); + }); + + }); + + after(function() { + setup.teardownTestBed(); + }); +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/app-template-one.js b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/app-template-one.js new file mode 100644 index 000000000..9c4338f1e --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/app-template-one.js @@ -0,0 +1 @@ +export default 'app template one'; \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/app-template-two.js b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/app-template-two.js new file mode 100644 index 000000000..391ce18a5 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/app-template-two.js @@ -0,0 +1 @@ +export default 'app template two'; \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/page-template-one.js b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/page-template-one.js new file mode 100644 index 000000000..f6067cc55 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/page-template-one.js @@ -0,0 +1 @@ +export default 'page template one'; \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/page-template-two.js b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/page-template-two.js new file mode 100644 index 000000000..ee96331e8 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/page-template-two.js @@ -0,0 +1 @@ +export default 'page template two'; \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/styles/app-template-one.css b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/styles/app-template-one.css new file mode 100644 index 000000000..75180b40b --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/styles/app-template-one.css @@ -0,0 +1,3 @@ +p { + color: 'royal-blue' +} \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/styles/app-template-two.css b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/styles/app-template-two.css new file mode 100644 index 000000000..0c7419542 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/styles/app-template-two.css @@ -0,0 +1,3 @@ +body { + font-family: 'Comic Sans' +} \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/styles/page-template-one.css b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/styles/page-template-one.css new file mode 100644 index 000000000..57bca52f2 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/styles/page-template-one.css @@ -0,0 +1,3 @@ +a { + cursor: pointer; +} \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/styles/page-template-two.css b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/styles/page-template-two.css new file mode 100644 index 000000000..6d2685111 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/styles/page-template-two.css @@ -0,0 +1,3 @@ +h1 { + font-size: 16rem; +} \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/templates/app.html b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/templates/app.html new file mode 100644 index 000000000..c4ba53b81 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/templates/app.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/scripts/app-template-one.js"></script> + <script type="module" src="/scripts/app-template-two.js"></script> + + <link rel="stylesheet" href="/styles/app-template-one.css"> + <link rel="stylesheet" href="/styles/app-template-two.css"></link> + + <style> + /* app-template-one-style */ + span { + text-align: center; + } + </style> + <style> + /* app-template-two-style */ + p { + margin: 0 auto; + } + </style> + </head> + + <body> + <page-outlet></page-outlet> + </body> + +</html> \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/templates/page.html b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/templates/page.html new file mode 100644 index 000000000..7e7c124d9 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-template-page-and-app/src/templates/page.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="../scripts/page-template-one.js"></script> + <script type="module" src="../scripts/page-template-two.js"></script> + + <link rel="stylesheet" href="/styles/page-template-one.css"></link> + <link rel="stylesheet" href="/styles/page-template-two.css"/> + + <style> + /* page-template-one-style */ + ol { + list-style: none; + } + </style> + <style> + /* page-template-two-style */ + h3 { + text-decoration: underline; + } + </style> + </head> + + <body> + + <div class='wrapper'> + + <div class='page-template blog-content content owen-test'> + <content-outlet></content-outlet> + </div> + + </div> + + </body> + +</html> \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-style/build.default.workspace-template-page-style.spec.js b/packages/cli/test/cases/build.default.workspace-template-page-style/build.default.workspace-template-page-style.spec.js deleted file mode 100644 index ce8a302ef..000000000 --- a/packages/cli/test/cases/build.default.workspace-template-page-style/build.default.workspace-template-page-style.spec.js +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Use Case - * Run Greenwood build command with no config and custom styled page template. - * - * User Result - * Should generate a bare bones Greenwood build with custom styled page template. - * - * User Command - * greenwood build - * - * User Config - * None (Greenwood Default) - * - * User Workspace - * src/ - * styles/ - * style.css - * theme.css - * templates/ - * page-template.js - */ -const expect = require('chai').expect; -const fs = require('fs'); -const path = require('path'); -const { JSDOM } = require('jsdom'); -const TestBed = require('../../../../../test/test-bed'); - -describe('Build Greenwood With: ', function() { - const LABEL = 'Default Greenwood Configuration and Workspace w/Custom Style and Theme Page Template'; - 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 Styled Page Template', function() { - let dom; - - before(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); - }); - - it('should output a single index.html file using our custom styled page template', function() { - expect(fs.existsSync(path.join(this.context.publicDir, './index.html'))).to.be.true; - }); - - it('should have the color style for the .owen-test element in the page template that we added as part of our custom style', function() { - - const customElement = dom.window.document.querySelector('.owen-test'); - const computedStyle = dom.window.getComputedStyle(customElement); - - expect(computedStyle.color).to.equal('rgb(0, 0, 255)'); - }); - - it('should have the color styles for the h3 element that we defined as part of our custom style', function() { - - const customHeader = dom.window.document.querySelector('h3'); - const computedStyle = dom.window.getComputedStyle(customHeader); - - expect(computedStyle.color).to.equal('green'); - }); - }); - - describe('Theme Styled Page Template', function() { - let dom; - - before(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); - }); - - it('should have the expected font import', async function() { - const styles = '@import url(//fonts.googleapis.com/css?family=Source+Sans+Pro&display=swap);'; - const styleTags = dom.window.document.querySelectorAll('head style'); - let importCount = 0; - - styleTags.forEach((tag) => { - if (tag.textContent.indexOf(styles) >= 0) { - importCount += 1; - } - }); - - expect(importCount).to.equal(1); - }); - - it('should have the expected font family', function() { - const styles = 'body{font-family:Source Sans Pro,sans-serif}'; - const styleTags = dom.window.document.querySelectorAll('head style'); - let fontCount = 0; - - styleTags.forEach((tag) => { - if (tag.textContent.indexOf(styles) >= 0) { - fontCount += 1; - } - }); - - expect(fontCount).to.equal(1); - }); - - }); - - }); - - after(function() { - setup.teardownTestBed(); - }); - -}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-style/src/styles/style.css b/packages/cli/test/cases/build.default.workspace-template-page-style/src/styles/style.css deleted file mode 100644 index dafcc1f42..000000000 --- a/packages/cli/test/cases/build.default.workspace-template-page-style/src/styles/style.css +++ /dev/null @@ -1,7 +0,0 @@ -.owen-test { - color: blue; -} - -h3 { - color: green; -} \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-style/src/templates/page-template.js b/packages/cli/test/cases/build.default.workspace-template-page-style/src/templates/page-template.js deleted file mode 100644 index 00c45ce62..000000000 --- a/packages/cli/test/cases/build.default.workspace-template-page-style/src/templates/page-template.js +++ /dev/null @@ -1,20 +0,0 @@ -import { html, LitElement } from 'lit-element'; -import '../styles/theme.css'; -import css from '../styles/style.css'; - -class PageTemplate extends LitElement { - render() { - return html` - <style> - ${css} - </style> - <div class='wrapper'> - <div class='page-template content owen-test'> - <entry></entry> - </div> - </div> - `; - } -} - -customElements.define('page-template', PageTemplate); \ No newline at end of file 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 457acc0b2..ad542818e 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 @@ -13,13 +13,17 @@ * * User Workspace * src/ + * scripts + * main.js + * styles/ + * theme.css * templates/ - * page-template.js + * page.html */ 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'); describe('Build Greenwood With: ', function() { @@ -36,7 +40,7 @@ describe('Build Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'index', 'not-found', 'hello'], LABEL); + runSmokeTest(['public', 'index'], LABEL); describe('Custom Page Template', function() { let dom; @@ -45,14 +49,71 @@ describe('Build Greenwood With: ', function() { dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); }); - it('should output a single index.html file using our custom page template', function() { - expect(fs.existsSync(path.join(this.context.publicDir, './index.html'))).to.be.true; - }); + describe('correct merge order for default app and custom page template <head> tags', function() { + let scriptTags; + let linkTags; + let styleTags; + + before(function() { + scriptTags = dom.window.document.querySelectorAll('head > script'); + linkTags = dom.window.document.querySelectorAll('head > link[rel="stylesheet"]'); + styleTags = dom.window.document.querySelectorAll('head > style'); + }); + + it('should have 1 <script> tags in the <head>', function() { + expect(scriptTags.length).to.equal(1); + }); + + it('should have 1 <link> tags in the <head>', function() { + expect(linkTags.length).to.equal(1); + }); - it('should have the specific element we added as part of our custom page template', function() { - const customElement = dom.window.document.querySelectorAll('div.owen-test'); + it('should have 2 <style> tags in the <head> (1 + one from Puppeteer)', function() { + expect(styleTags.length).to.equal(2); + }); + + it('should add one page template <script> tag', function() { + expect(scriptTags[0].type).to.equal('module'); + expect(scriptTags[0].src).to.match(/main.*.js/); + }); + + it('should add one page template <link> tag', function() { + expect(linkTags[0].rel).to.equal('stylesheet'); + expect(linkTags[0].href).to.match(/styles\/theme.[a-z0-9]{8}.css/); + }); + + it('should add one page template <style> tag', function() { + // offset index by one since first <style> tag is from Puppeteer + expect(styleTags[1].textContent).to.contain('.owen-test'); + }); + }); + + describe('custom inline <style> tag in the <head> of a page template', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); - expect(customElement.length).to.equal(1); + it('should have the specific element we added as part of our custom page template', function() { + const customElement = dom.window.document.querySelectorAll('div.owen-test'); + + expect(customElement.length).to.equal(1); + }); + + it('should have the color style for the .owen-test element in the page template that we added as part of our custom style', function() { + const customElement = dom.window.document.querySelector('.owen-test'); + const computedStyle = dom.window.getComputedStyle(customElement); + + expect(computedStyle.color).to.equal('blue'); + }); + + it('should have the color styles for the h3 element that we defined as part of our custom style', function() { + const customHeader = dom.window.document.querySelector('h3'); + const computedStyle = dom.window.getComputedStyle(customHeader); + + expect(computedStyle.color).to.equal('green'); + }); }); }); diff --git a/packages/cli/test/cases/build.default.workspace-template-page/src/scripts/main.js b/packages/cli/test/cases/build.default.workspace-template-page/src/scripts/main.js new file mode 100644 index 000000000..54df14889 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-template-page/src/scripts/main.js @@ -0,0 +1 @@ +export default 'hello world'; \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-style/src/styles/theme.css b/packages/cli/test/cases/build.default.workspace-template-page/src/styles/theme.css similarity index 100% rename from packages/cli/test/cases/build.default.workspace-template-page-style/src/styles/theme.css rename to packages/cli/test/cases/build.default.workspace-template-page/src/styles/theme.css diff --git a/packages/cli/test/cases/build.default.workspace-template-page/src/templates/page-template.js b/packages/cli/test/cases/build.default.workspace-template-page/src/templates/page-template.js deleted file mode 100644 index 1e0d17ae8..000000000 --- a/packages/cli/test/cases/build.default.workspace-template-page/src/templates/page-template.js +++ /dev/null @@ -1,15 +0,0 @@ -import { html, LitElement } from 'lit-element'; - -class PageTemplate extends LitElement { - render() { - return html` - <div class='wrapper'> - <div class='page-template blog-content content owen-test'> - <entry></entry> - </div> - </div> - `; - } -} - -customElements.define('page-template', PageTemplate); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page/src/templates/page.html b/packages/cli/test/cases/build.default.workspace-template-page/src/templates/page.html new file mode 100644 index 000000000..3021703a4 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-template-page/src/templates/page.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <head> + <script type="module" src="/scripts/main.js"></script> + <link rel="stylesheet" href="/styles/theme.css"></link> + </head> + + <style> + .owen-test { + color: blue; + } + + h3 { + color: green; + } + </style> + </head> + + <body> + + <div class='wrapper'> + <div class='page-template content owen-test'> + <h3>Hello</h3> + <content-outlet></content-outlet> + </div> + </div> + + </body> + +</html> \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-top-level-pages/build.default.workspace-top-level-pages.spec.js b/packages/cli/test/cases/build.default.workspace-top-level-pages/build.default.workspace-top-level-pages.spec.js new file mode 100644 index 000000000..d85e74799 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-top-level-pages/build.default.workspace-top-level-pages.spec.js @@ -0,0 +1,116 @@ +/* + * Use Case + * Run Greenwood with default config and mixed HTML and markdown top level pages. + * + * Result + * Test for correct page output and layout. + * + * Command + * greenwood build + * + * User Config + * None (Greenwood default) + * + * User Workspace + * src/ + * pages/ + * about.html + * contact.md + * index.html + */ +const expect = require('chai').expect; +const fs = require('fs'); +const { JSDOM } = require('jsdom'); +const path = require('path'); +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'Default Greenwood Configuration and Default Workspace w/ Top Level Pages'; + 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'], LABEL); + + describe('Home (index) Page', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + xit('should have the correct <title> for the home page', function() { + const titleTags = dom.window.document.querySelectorAll('title'); + + expect(titleTags.length).to.equal(1); + expect(titleTags[0].textContent).to.equal('Top Level Test'); + }); + + it('should have the correct content for the home page', function() { + const h1Tags = dom.window.document.querySelectorAll('h1'); + + expect(h1Tags.length).to.equal(1); + expect(h1Tags[0].textContent).to.equal('Hello from the home page!!!!'); + }); + }); + + describe('About Page', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'about', 'index.html')); + }); + + it('should create a top level about page with a directory and index.html', function() { + expect(fs.existsSync(path.join(this.context.publicDir, 'about', 'index.html'))).to.be.true; + }); + + it('should have the correct content for the about page', function() { + const h1Tags = dom.window.document.querySelectorAll('h1'); + const pTags = dom.window.document.querySelectorAll('p'); + + expect(h1Tags.length).to.equal(1); + expect(h1Tags[0].textContent).to.equal('Hello from about.html'); + + expect(pTags.length).to.equal(1); + expect(pTags[0].textContent).to.equal('Lorum Ipsum'); + }); + }); + + describe('Contact Page', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'contact', 'index.html')); + }); + + it('should create a top level contact page with a directory and index.html', function() { + expect(fs.existsSync(path.join(this.context.publicDir, 'contact', 'index.html'))).to.be.true; + }); + + it('should have the correct content for the contact page', function() { + const h3Tags = dom.window.document.querySelectorAll('h3'); + const pTags = dom.window.document.querySelectorAll('p'); + + expect(h3Tags.length).to.equal(1); + expect(h3Tags[0].textContent).to.equal('Contact Page'); + + expect(pTags.length).to.equal(1); + expect(pTags[0].textContent).to.equal('Thanks for contacting us.'); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-top-level-pages/src/pages/about.html b/packages/cli/test/cases/build.default.workspace-top-level-pages/src/pages/about.html new file mode 100644 index 000000000..c3e165d74 --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-top-level-pages/src/pages/about.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <body> + <h1>Hello from about.html</h1> + <p>Lorum Ipsum</p> + </body> + +</html> \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-top-level-pages/src/pages/contact.md b/packages/cli/test/cases/build.default.workspace-top-level-pages/src/pages/contact.md new file mode 100644 index 000000000..f6d45df3c --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-top-level-pages/src/pages/contact.md @@ -0,0 +1,3 @@ +### Contact Page + +Thanks for contacting us. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-top-level-pages/src/pages/index.html b/packages/cli/test/cases/build.default.workspace-top-level-pages/src/pages/index.html new file mode 100644 index 000000000..b10aabaca --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-top-level-pages/src/pages/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <title>Top Level Test + + + +

Hello from the home page!!!!

+ + + \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-user-directory-mapping/build.default.workspace-user-directory-mapping.spec.js b/packages/cli/test/cases/build.default.workspace-user-directory-mapping/build.default.workspace-user-directory-mapping.spec.js index 53bb0fc50..a04a27ee3 100644 --- a/packages/cli/test/cases/build.default.workspace-user-directory-mapping/build.default.workspace-user-directory-mapping.spec.js +++ b/packages/cli/test/cases/build.default.workspace-user-directory-mapping/build.default.workspace-user-directory-mapping.spec.js @@ -18,15 +18,24 @@ * header/ * header.js * pages/ - * about.md + * plugins/ + * index-hooks.md + * index.md * index.md + * pages.md * services/ - * components.js * pages/ * pages.js + * components.js * templates/ - * page-template.js + * page.html */ +const expect = require('chai').expect; +const fs = require('fs'); +const glob = require('glob-promise'); +const { JSDOM } = require('jsdom'); +const path = require('path'); +const runSmokeTest = require('../../../../../test/smoke-test'); const TestBed = require('../../../../../test/test-bed'); describe('Build Greenwood With: ', function() { @@ -43,7 +52,120 @@ describe('Build Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'index', 'not-found'], LABEL); + runSmokeTest(['public', 'index'], LABEL); + + describe('Output Folder Structure and Home Page', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + it('should create a public directory', function() { + expect(fs.existsSync(this.context.publicDir)).to.be.true; + }); + + it('should output an index.html file (home page)', function() { + expect(fs.existsSync(path.join(this.context.publicDir, './index.html'))).to.be.true; + }); + + it('should output one JS bundle files', async function() { + expect(await glob.promise(path.join(this.context.publicDir, './*.js'))).to.have.lengthOf(1); + }); + + it('should have one + + + + + + +
+ + + +
+ + + + \ No newline at end of file diff --git a/packages/cli/test/cases/build.default/build.default.spec.js b/packages/cli/test/cases/build.default/build.default.spec.js index c5405abde..d4ad64ba8 100644 --- a/packages/cli/test/cases/build.default/build.default.spec.js +++ b/packages/cli/test/cases/build.default/build.default.spec.js @@ -14,6 +14,9 @@ * 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'); @@ -31,7 +34,85 @@ describe('Build Greenwood With: ', function() { before(async function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'index', 'not-found', 'hello'], LABEL); + + runSmokeTest(['public', 'index'], LABEL); + + describe('Default output for index.html', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); + }); + + describe('head section tags', function() { + let metaTags; + + before(function() { + metaTags = dom.window.document.querySelectorAll('head > meta'); + }); + + it('should have a tag in the <head>', function() { + const title = dom.window.document.querySelector('head title').textContent; + + expect(title).to.be.equal('My App'); + }); + + it('should have five default <meta> tags in the <head>', function() { + expect(metaTags.length).to.be.equal(5); + }); + + it('should have default charset <meta> tag', function() { + expect(metaTags[0].getAttribute('charset')).to.be.equal('utf-8'); + }); + + it('should have default viewport <meta> tag', function() { + const viewportMeta = metaTags[1]; + + expect(viewportMeta.getAttribute('name')).to.be.equal('viewport'); + expect(viewportMeta.getAttribute('content')).to.be.equal('width=device-width, initial-scale=1'); + }); + + it('should have default mobile-web-app-capable <meta> tag', function() { + const mwacMeta = metaTags[2]; + + expect(mwacMeta.getAttribute('name')).to.be.equal('mobile-web-app-capable'); + expect(mwacMeta.getAttribute('content')).to.be.equal('yes'); + }); + + it('should have default apple-mobile-web-app-capable <meta> tag', function() { + const amwacMeta = metaTags[3]; + + expect(amwacMeta.getAttribute('name')).to.be.equal('apple-mobile-web-app-capable'); + expect(amwacMeta.getAttribute('content')).to.be.equal('yes'); + }); + + it('should have default apple-mobile-web-app-status-bar-style <meta> tag', function() { + const amwasbsMeta = metaTags[4]; + + expect(amwasbsMeta.getAttribute('name')).to.be.equal('apple-mobile-web-app-status-bar-style'); + expect(amwasbsMeta.getAttribute('content')).to.be.equal('black'); + }); + }); + + it('should have expected h1 tag in the <body>', function() { + const title = dom.window.document.querySelector('body h1').textContent; + + expect(title).to.be.equal('Welcome to my website!'); + }); + + it('should have expected <content-outlet> tag in the <body>', function() { + const contentOutlet = dom.window.document.querySelectorAll('body content-outlet'); + + expect(contentOutlet.length).to.be.equal(1); + expect(contentOutlet[0]).to.not.be.undefined; + }); + + it('should have the expected heading text within the index page in the public directory', function() { + const heading = dom.window.document.querySelector('body h1').textContent; + + expect(heading).to.equal('Welcome to my website!'); + }); + }); }); after(function() { 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 0852125b6..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: ` - * <!-- some analytics code --> - * ` - * }; - * } - * }, { - * type: 'index', - * provider: function() { - * return { - * hookGreenwoodPolyfills: ` - * <!-- some polyfills code --> - * ` - * }; - * } - * }] - * - * } - * - * 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'); - -describe('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: ` - <div class="hook-analytics"> - <!-- analytics code goes here --> - </div> - ` - }; - } - }, { - type: 'index', - provider: function() { - return { - hookGreenwoodPolyfills: ` - <!-- - this covers custom overriding since polyfills are on by default already - so for this test, we actully need to load something that works with puppeteer + JSDOM - --> - <div class="hook-polyfills"> - <script src="//cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/2.2.7/webcomponents-bundle.js"></script> - </div> - ` - }; - } - }] - -}; \ 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 7e077610b..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'); - -describe('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.config.error-public-path/build.config.error-public-path.spec.js b/packages/cli/test/cases/build.plugins.error-name/build.plugins.error-name.spec.js similarity index 57% rename from packages/cli/test/cases/build.config.error-public-path/build.config.error-public-path.spec.js rename to packages/cli/test/cases/build.plugins.error-name/build.plugins.error-name.spec.js index 1ab35ad82..88ee9352d 100644 --- a/packages/cli/test/cases/build.config.error-public-path/build.config.error-public-path.spec.js +++ b/packages/cli/test/cases/build.plugins.error-name/build.plugins.error-name.spec.js @@ -1,6 +1,6 @@ /* * Use Case - * Run Greenwood build command with a bad value for workspace directory in a custom config. + * Run Greenwood build command with a bad value for the type of a plugin. * * User Result * Should throw an error. @@ -10,12 +10,17 @@ * * User Config * { - * publicPath: 123 + * plugins: [{ + * type: 'resource', + * plugin: function () { } + * }] * } * * User Workspace - * Greenwood default + * Greenwood default (src/) + * */ + const expect = require('chai').expect; const TestBed = require('../../../../../test/test-bed'); @@ -27,15 +32,14 @@ describe('Build Greenwood With: ', function() { await setup.setupTestBed(__dirname); }); - describe('Custom Configuration with a bad value for Public Path', function() { - it('should throw an error that publicPath must be a string', async function() { + 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('greenwood.config.js publicPath must be a string'); + expect(err).to.contain('Error: greenwood.config.js plugins must have a name. got undefined instead.'); } }); - }); after(function() { 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 83% 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 3fc3c99c4..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: {} * }] * } @@ -33,11 +34,11 @@ describe('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 74% 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 196b9753d..588d11aca 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 @@ -33,12 +33,12 @@ describe('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, rollup, server". 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 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..7379c59cf --- /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; + + before(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..c33b646a6 --- /dev/null +++ b/packages/cli/test/cases/build.plugins.resource/src/pages/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="../foo-files/my-custom-file.foo"></script> + </head> + + <body> + <h1>My Foo Website</h1> + </body> + +</html> \ No newline at end of file diff --git a/packages/cli/test/cases/eject.default/eject.default.spec.js b/packages/cli/test/cases/eject.default/eject.default.spec.js index 40fe7b614..506829591 100644 --- a/packages/cli/test/cases/eject.default/eject.default.spec.js +++ b/packages/cli/test/cases/eject.default/eject.default.spec.js @@ -8,116 +8,41 @@ * User Command * greenwood eject */ -const fs = require('fs-extra'); +const fs = require('fs'); const path = require('path'); const expect = require('chai').expect; const TestBed = require('../../../../../test/test-bed'); -describe('Eject Greenwood With: ', function() { +describe('Eject Greenwood', function() { let setup; + let configFiles; before(async function() { setup = new TestBed(); this.context = await setup.setupTestBed(__dirname); }); - describe('Default Eject Option', function() { + describe('Default Eject', function() { before(async function() { await setup.runGreenwoodCommand('eject'); - }); - - it('should output webpack config files to project working directory', function() { - let configFiles = fs.readdirSync(__dirname); - - configFiles = configFiles.filter((file) => file !== 'node_modules' && file !== 'eject.default.spec.js'); - - /* - * 'webpack.config.common.js', - * 'webpack.config.develop.js', - * 'webpack.config.prod.js' - */ - expect(configFiles.length).to.equal(3); - }); - - it('should output webpack common config', function() { - expect(fs.existsSync(path.join(__dirname, 'webpack.config.common.js'))).to.be.true; - }); - - it('should output webpack develop config', function() { - expect(fs.existsSync(path.join(__dirname, 'webpack.config.develop.js'))).to.be.true; - }); - - it('should output webpack prod config', function() { - expect(fs.existsSync(path.join(__dirname, 'webpack.config.prod.js'))).to.be.true; - }); - - after(function() { - // remove files - const configFiles = fs.readdirSync(__dirname); - - configFiles.forEach(file => { - if (file !== 'eject.default.spec.js' && file !== 'node_modules') { - fs.remove(path.join(__dirname, file)); - } - }); - }); - }); - - describe('Eject All Option', function() { - - before(async function() { - await setup.runGreenwoodCommand('eject --all'); - }); - - it('should output webpack, postcss, babel, browserlistrc config files to working directory', function() { - let configFiles = fs.readdirSync(__dirname); - - configFiles = configFiles.filter((file) => file !== 'node_modules' && file !== 'eject.default.spec.js'); - - /* - * '.browserslistrc', - * 'babel.config.js', - * 'postcss.config.js', - * 'webpack.config.common.js', - * 'webpack.config.develop.js', - * 'webpack.config.prod.js' - */ - expect(configFiles.length).to.equal(6); - }); - - it('should output webpack common config', function() { - expect(fs.existsSync(path.join(__dirname, 'webpack.config.common.js'))).to.be.true; - }); - - it('should output webpack develop config', function() { - expect(fs.existsSync(path.join(__dirname, 'webpack.config.develop.js'))).to.be.true; - }); - - it('should output webpack prod config', function() { - expect(fs.existsSync(path.join(__dirname, 'webpack.config.prod.js'))).to.be.true; - }); - it('should output babel config', function() { - expect(fs.existsSync(path.join(__dirname, 'babel.config.js'))).to.be.true; + configFiles = fs.readdirSync(__dirname) + .filter((file) => path.extname(file) === '.js' && file.indexOf('spec.js') < 0); }); - it('should output postcss config', function() { - expect(fs.existsSync(path.join(__dirname, 'postcss.config.js'))).to.be.true; + it('should output one config files to users project working directory', function() { + expect(configFiles.length).to.equal(1); }); - it('should output browserslist', function() { - expect(fs.existsSync(path.join(__dirname, '.browserslistrc'))).to.be.true; + it('should output rollup config file', function() { + expect(fs.existsSync(path.join(__dirname, 'rollup.config.js'))).to.be.true; }); after(function() { - // remove files - const configFiles = fs.readdirSync(__dirname); - + // remove test files configFiles.forEach(file => { - if (file !== 'eject.default.spec.js' && file !== 'node_modules') { - fs.remove(path.join(__dirname, file)); - } + fs.unlinkSync(path.join(__dirname, file)); }); }); }); @@ -129,19 +54,15 @@ describe('Eject Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'index', 'not-found', 'hello'], 'Eject and Build Ejected Config'); + runSmokeTest(['public', 'index'], 'Eject and Build Ejected Config'); }); after(function() { - setup.teardownTestBed(); - - // remove ejected files - const configFiles = fs.readdirSync(__dirname); - + // remove test files configFiles.forEach(file => { - if (file !== 'eject.default.spec.js') { - fs.remove(path.join(__dirname, file)); - } + fs.unlinkSync(path.join(__dirname, file)); }); + + setup.teardownTestBed(); }); }); \ No newline at end of file diff --git a/packages/cli/test/unit/data/mocks/graph.js b/packages/cli/test/unit/data/mocks/graph.js deleted file mode 100644 index 27f5d457b..000000000 --- a/packages/cli/test/unit/data/mocks/graph.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - // eslint-disable-next-line - graph: [{"data":{"menu":"","index":"","linkheadings":0,"tableOfContents":[]},"mdFile":"./index.md","label":"be5eeae398e34ba","route":"/","template":"home","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/index.md","fileName":"index","relativeExpectedPath":"'../index/index.js'","title":"","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":3,"linkheadings":0,"tableOfContents":[]},"mdFile":"./about/community.md","label":"community","route":"/about/community","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/about/community.md","fileName":"community","relativeExpectedPath":"'../about/community/community.js'","title":"Community","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":2,"linkheadings":0,"tableOfContents":[]},"mdFile":"./about/features.md","label":"features","route":"/about/features","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/about/features.md","fileName":"features","relativeExpectedPath":"'../about/features/features.js'","title":"Features","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":"","linkheadings":0,"tableOfContents":[]},"mdFile":"./about/goals.md","label":"goals","route":"/about/goals","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/about/goals.md","fileName":"goals","relativeExpectedPath":"'../about/goals/goals.js'","title":"Goals","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":1,"linkheadings":3,"tableOfContents":[{"content":"CLI","slug":"cli","lvl":3,"i":1,"seen":0},{"content":"Evergreen Build","slug":"evergreen-build","lvl":3,"i":2,"seen":0},{"content":"Browser Support","slug":"browser-support","lvl":3,"i":3,"seen":0},{"content":"Polyfills","slug":"polyfills","lvl":3,"i":4,"seen":0}]},"mdFile":"./about/how-it-works.md","label":"how-it-works","route":"/about/how-it-works","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/about/how-it-works.md","fileName":"how-it-works","relativeExpectedPath":"'../about/how-it-works/how-it-works.js'","title":"How It Works","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"navigation","index":"","linkheadings":0,"tableOfContents":[]},"mdFile":"./about/index.md","label":"about","route":"/about/","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/about/index.md","fileName":"index","relativeExpectedPath":"'../about/index/index.js'","title":"About","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":1,"linkheadings":0,"tableOfContents":[]},"mdFile":"./docs/component-model.md","label":"component-model","route":"/docs/component-model","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/component-model.md","fileName":"component-model","relativeExpectedPath":"'../docs/component-model/component-model.js'","title":"Component Model","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":2,"linkheadings":3,"tableOfContents":[{"content":"Dev Server","slug":"dev-server","lvl":3,"i":1,"seen":0},{"content":"Example","slug":"example","lvl":4,"i":2,"seen":0},{"content":"Meta","slug":"meta","lvl":3,"i":3,"seen":0},{"content":"Example","slug":"example-1","lvl":4,"i":4,"seen":1},{"content":"Public Path","slug":"public-path","lvl":3,"i":5,"seen":0},{"content":"Example","slug":"example-2","lvl":4,"i":6,"seen":2},{"content":"Title","slug":"title","lvl":3,"i":7,"seen":0},{"content":"Example","slug":"example-3","lvl":4,"i":8,"seen":3},{"content":"Workspace","slug":"workspace","lvl":3,"i":9,"seen":0},{"content":"Example","slug":"example-4","lvl":4,"i":10,"seen":4}]},"mdFile":"./docs/configuration.md","label":"configuration","route":"/docs/configuration","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/configuration.md","fileName":"configuration","relativeExpectedPath":"'../docs/configuration/configuration.js'","title":"Configuration","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":5,"linkheadings":3,"tableOfContents":[{"content":"Theming","slug":"theming","lvl":3,"i":1,"seen":0},{"content":"Example","slug":"example","lvl":4,"i":2,"seen":0},{"content":"Shadow DOM","slug":"shadow-dom","lvl":3,"i":3,"seen":0},{"content":"Example","slug":"example-1","lvl":4,"i":4,"seen":1},{"content":"Assets and Images","slug":"assets-and-images","lvl":3,"i":5,"seen":0},{"content":"Example","slug":"example-2","lvl":4,"i":6,"seen":2}]},"mdFile":"./docs/css-and-images.md","label":"styles-assets","route":"/docs/css-and-images","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/css-and-images.md","fileName":"css-and-images","relativeExpectedPath":"'../docs/css-and-images/css-and-images.js'","title":"Styles and Assets","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":7,"linkheadings":3,"tableOfContents":[{"content":"Internal Sources","slug":"internal-sources","lvl":3,"i":1,"seen":0},{"content":"Schema","slug":"schema","lvl":4,"i":2,"seen":0},{"content":"Queries","slug":"queries","lvl":4,"i":3,"seen":0},{"content":"Graph","slug":"graph","lvl":5,"i":4,"seen":0},{"content":"Definition","slug":"definition","lvl":6,"i":5,"seen":0},{"content":"Usage","slug":"usage","lvl":6,"i":6,"seen":0},{"content":"Response","slug":"response","lvl":6,"i":7,"seen":0},{"content":"Menu Query","slug":"menu-query","lvl":5,"i":8,"seen":0},{"content":"Children","slug":"children","lvl":5,"i":9,"seen":0},{"content":"Definition","slug":"definition-1","lvl":6,"i":10,"seen":1},{"content":"Usage","slug":"usage-1","lvl":6,"i":11,"seen":1},{"content":"Response","slug":"response-1","lvl":6,"i":12,"seen":1},{"content":"Config","slug":"config","lvl":5,"i":13,"seen":0},{"content":"Definition","slug":"definition-2","lvl":6,"i":14,"seen":2},{"content":"Usage","slug":"usage-2","lvl":6,"i":15,"seen":2},{"content":"Response","slug":"response-2","lvl":6,"i":16,"seen":2},{"content":"Custom","slug":"custom","lvl":5,"i":17,"seen":0},{"content":"example:","slug":"example","lvl":6,"i":18,"seen":0},{"content":"Complete Example","slug":"complete-example","lvl":5,"i":19,"seen":0},{"content":"External Sources","slug":"external-sources","lvl":4,"i":20,"seen":0}]},"mdFile":"./docs/data.md","label":"data-sources","route":"/docs/data","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/data.md","fileName":"data","relativeExpectedPath":"'../docs/data/data.js'","title":"Data Sources","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":3,"linkheadings":3,"tableOfContents":[{"content":"Element Label","slug":"element-label","lvl":3,"i":1,"seen":0},{"content":"Example","slug":"example","lvl":4,"i":2,"seen":0},{"content":"Imports","slug":"imports","lvl":3,"i":3,"seen":0},{"content":"Example","slug":"example-1","lvl":4,"i":4,"seen":1},{"content":"Template","slug":"template","lvl":3,"i":5,"seen":0},{"content":"Example","slug":"example-2","lvl":4,"i":6,"seen":2},{"content":"Title","slug":"title","lvl":3,"i":7,"seen":0},{"content":"Example","slug":"example-3","lvl":4,"i":8,"seen":3},{"content":"Custom Data","slug":"custom-data","lvl":3,"i":9,"seen":0},{"content":"Example","slug":"example-4","lvl":4,"i":10,"seen":4}]},"mdFile":"./docs/front-matter.md","label":"front-matter","route":"/docs/front-matter","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/front-matter.md","fileName":"front-matter","relativeExpectedPath":"'../docs/front-matter/front-matter.js'","title":"Front Matter","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"navigation","index":"","linkheadings":0,"tableOfContents":[]},"mdFile":"./docs/index.md","label":"docs","route":"/docs/","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/index.md","fileName":"index","relativeExpectedPath":"'../docs/index/index.js'","title":"Docs","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":6,"linkheadings":3,"tableOfContents":[{"content":"Page Template","slug":"page-template","lvl":3,"i":1,"seen":0},{"content":"Template Hooks","slug":"template-hooks","lvl":4,"i":2,"seen":0},{"content":"App Template","slug":"app-template","lvl":3,"i":3,"seen":0},{"content":"Pages","slug":"pages","lvl":3,"i":4,"seen":0}]},"mdFile":"./docs/layouts.md","label":"templates","route":"/docs/layouts","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/layouts.md","fileName":"layouts","relativeExpectedPath":"'../docs/layouts/layouts.js'","title":"Templates and Pages","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":4,"linkheadings":3,"tableOfContents":[{"content":"Syntax Highlighting","slug":"syntax-highlighting","lvl":3,"i":1,"seen":0},{"content":"Imports","slug":"imports","lvl":3,"i":2,"seen":0},{"content":"Example","slug":"example","lvl":4,"i":3,"seen":0}]},"mdFile":"./docs/markdown.md","label":"markdown","route":"/docs/markdown","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/markdown.md","fileName":"markdown","relativeExpectedPath":"'../docs/markdown/markdown.js'","title":"Markdown","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":5,"linkheadings":3,"tableOfContents":[{"content":"Declare Menu","slug":"declare-menu","lvl":3,"i":1,"seen":0},{"content":"Retrieve Menu","slug":"retrieve-menu","lvl":3,"i":2,"seen":0},{"content":"Sorting","slug":"sorting","lvl":3,"i":3,"seen":0},{"content":"Filtering By Path","slug":"filtering-by-path","lvl":3,"i":4,"seen":0}]},"mdFile":"./docs/menus.md","label":"menus","route":"/docs/menus","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/menus.md","fileName":"menus","relativeExpectedPath":"'../docs/menus/menus.js'","title":"Menus","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":8,"linkheadings":3,"tableOfContents":[{"content":"NodeJS","slug":"nodejs","lvl":3,"i":1,"seen":0},{"content":"Web Components","slug":"web-components","lvl":3,"i":2,"seen":0},{"content":"Webpack","slug":"webpack","lvl":3,"i":3,"seen":0},{"content":"Development","slug":"development","lvl":3,"i":4,"seen":0}]},"mdFile":"./docs/tech-stack.md","label":"stack","route":"/docs/tech-stack","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/tech-stack.md","fileName":"tech-stack","relativeExpectedPath":"'../docs/tech-stack/tech-stack.js'","title":"Tech Stack","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":5,"linkheadings":3,"tableOfContents":[{"content":"Web Components","slug":"web-components","lvl":3,"i":1,"seen":0},{"content":"CSS","slug":"css","lvl":3,"i":2,"seen":0}]},"mdFile":"./getting-started/branding.md","label":"css-components","route":"/getting-started/branding","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/branding.md","fileName":"branding","relativeExpectedPath":"'../getting-started/branding/branding.js'","title":"Styles and Web Components","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":6,"linkheadings":0,"tableOfContents":[]},"mdFile":"./getting-started/build-and-deploy.md","label":"deploy","route":"/getting-started/build-and-deploy","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/build-and-deploy.md","fileName":"build-and-deploy","relativeExpectedPath":"'../getting-started/build-and-deploy/build-and-deploy.js'","title":"Build and Deploy","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":4,"linkheadings":3,"tableOfContents":[{"content":"Objectives","slug":"objectives","lvl":3,"i":1,"seen":0},{"content":"Home Page Template","slug":"home-page-template","lvl":3,"i":2,"seen":0},{"content":"Blog Posts Template","slug":"blog-posts-template","lvl":3,"i":3,"seen":0},{"content":"Creating Pages","slug":"creating-pages","lvl":3,"i":4,"seen":0},{"content":"Development Server","slug":"development-server","lvl":3,"i":5,"seen":0}]},"mdFile":"./getting-started/creating-content.md","label":"create-content","route":"/getting-started/creating-content","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/creating-content.md","fileName":"creating-content","relativeExpectedPath":"'../getting-started/creating-content/creating-content.js'","title":"Creating Content","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"navigation","index":"","linkheadings":0,"tableOfContents":[]},"mdFile":"./getting-started/index.md","label":"getting-started","route":"/getting-started/","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/index.md","fileName":"index","relativeExpectedPath":"'../getting-started/index/index.js'","title":"Getting Started","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":2,"linkheadings":3,"tableOfContents":[{"content":"Workspace","slug":"workspace","lvl":3,"i":1,"seen":0},{"content":"Templates","slug":"templates","lvl":3,"i":2,"seen":0},{"content":"Pages","slug":"pages","lvl":3,"i":3,"seen":0}]},"mdFile":"./getting-started/key-concepts.md","label":"concepts","route":"/getting-started/key-concepts","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/key-concepts.md","fileName":"key-concepts","relativeExpectedPath":"'../getting-started/key-concepts/key-concepts.js'","title":"Key Concepts","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":7,"linkheadings":0,"tableOfContents":[]},"mdFile":"./getting-started/next-steps.md","label":"next","route":"/getting-started/next-steps","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/next-steps.md","fileName":"next-steps","relativeExpectedPath":"'../getting-started/next-steps/next-steps.js'","title":"Next Steps","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":2,"linkheadings":3,"tableOfContents":[{"content":"Installing Greenwood","slug":"installing-greenwood","lvl":3,"i":1,"seen":0},{"content":"Configuring Workflows","slug":"configuring-workflows","lvl":3,"i":2,"seen":0},{"content":"Project Structure","slug":"project-structure","lvl":3,"i":3,"seen":0}]},"mdFile":"./getting-started/project-setup.md","label":"project-setup","route":"/getting-started/project-setup","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/project-setup.md","fileName":"project-setup","relativeExpectedPath":"'../getting-started/project-setup/project-setup.js'","title":"Project Setup","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":1,"linkheadings":0,"tableOfContents":[]},"mdFile":"./getting-started/quick-start.md","label":"start","route":"/getting-started/quick-start","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/quick-start.md","fileName":"quick-start","relativeExpectedPath":"'../getting-started/quick-start/quick-start.js'","title":"Quick Start","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":1,"linkheadings":0,"tableOfContents":[]},"mdFile":"./plugins/composite-plugins.md","label":"composite-plugins","route":"/plugins/composite-plugins","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/plugins/composite-plugins.md","fileName":"composite-plugins","relativeExpectedPath":"'../plugins/composite-plugins/composite-plugins.js'","title":"Composite Plugins","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":2,"linkheadings":0,"tableOfContents":[]},"mdFile":"./plugins/index-hooks.md","label":"index-hooks","route":"/plugins/index-hooks","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/plugins/index-hooks.md","fileName":"index-hooks","relativeExpectedPath":"'../plugins/index-hooks/index-hooks.js'","title":"Index Hooks","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"navigation","index":"","linkheadings":0,"tableOfContents":[]},"mdFile":"./plugins/index.md","label":"plugins","route":"/plugins/","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/plugins/index.md","fileName":"index","relativeExpectedPath":"'../plugins/index/index.js'","title":"Plugins","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]},{"data":{"menu":"side","index":3,"linkheadings":0,"tableOfContents":[]},"mdFile":"./plugins/webpack.md","label":"webpack","route":"/plugins/webpack","template":"page","filePath":"/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/plugins/webpack.md","fileName":"webpack","relativeExpectedPath":"'../plugins/webpack/webpack.js'","title":"Webpack Plugins","meta":[{"name":"description","content":"A modern and performant static site generator supporting Web Component based development"},{"name":"twitter:site","content":"@PrjEvergreen"},{"property":"og:title","content":"Greenwood"},{"property":"og:type","content":"website"},{"property":"og:url","content":"https://www.greenwoodjs.io"},{"property":"og:image","content":"https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png"},{"property":"og:description","content":"A modern and performant static site generator supporting Web Component based development"},{"rel":"shortcut icon","href":"/assets/favicon.ico"},{"rel":"icon","href":"/assets/favicon.ico"},{"name":"google-site-verification","content":"4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0"}]}] -}; \ No newline at end of file diff --git a/packages/plugin-babel/README.md b/packages/plugin-babel/README.md new file mode 100644 index 000000000..5b9a5427b --- /dev/null +++ b/packages/plugin-babel/README.md @@ -0,0 +1,77 @@ +# @greenwood/plugin-babel + +## Overview +A Greenwood plugin for using [**Babel**](https://babeljs.io/) and applying it to your JavaScript. + +> This package assumes you already have `@greenwood/cli` installed. + +## Installation +You can use your favorite JavaScript package manager to install this package. + +_examples:_ +```bash +# npm +npm -i @greenwood/plugin-babel --save-dev + +# yarn +yarn add @greenwood/plugin-babel --dev +``` + +## Usage +Add this plugin to your _greenwood.config.js_. + +```javascript +const pluginBabel = require('@greenwood/plugin-babel'); + +module.exports = { + ... + + plugins: [ + ...pluginBabel() // notice the spread ... ! + ] +} +``` + +Create a _babel.config.js_ in the root of your project with your own custom plugins / settings that you've installed and want to use. + +```javascript +module.exports = { + plugins: [ + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-private-methods' + ] +}; +``` + +This will then process your JavaScript with Babel with the configurated plugins / settings you provide. + +## Options +This plugin provides a default _babel.config.js_ that includes support for [**@babel/preset-env**](https://babeljs.io/docs/en/babel-preset-env) using [**browserslist**](https://github.com/browserslist/browserslist) with reasonable [default configs](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-babel/src/) for each. + +If you would like to use it, either standalone or with your own custom _babel.config.js_, you will need to take the following extra steps: + +1. Install `@babel/runtime` and `regenerator-runtime` as direct dependencies of your project + ```bash + # npm + npm -i @babel/runtime regenerator-runtime + + # yarn + yarn add @babel/runtime regenerator-runtime + ``` +1. When adding `pluginBabel` to your _greenwood.config.js_, enable the `extendConfig` option + ```js + const pluginBabel = require('@greenwood/plugin-babel'); + + module.exports = { + ... + + plugins: [ + // notice the spread ... ! + ...pluginBabel({ + extendConfig: true + }) + ] + } + ``` + +If you have a custom _babel.config.js_, this option will merge its own `presets` and `plugins` in the array ahead of your own (if you have them). \ No newline at end of file diff --git a/packages/plugin-babel/package.json b/packages/plugin-babel/package.json new file mode 100644 index 000000000..937a05b78 --- /dev/null +++ b/packages/plugin-babel/package.json @@ -0,0 +1,39 @@ +{ + "name": "@greenwood/plugin-babel", + "version": "0.10.0-alpha.10", + "description": "A Greenwood plugin for using Babel and applying it to your JavaScript.", + "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-babel", + "author": "Owen Buckley <owen@thegreenhouse.io>", + "license": "MIT", + "keywords": [ + "Greenwood", + "CommonJS", + "Static Site Generator", + "NodeJS", + "Babel" + ], + "main": "src/index.js", + "files": [ + "src/" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@babel/core": "^7.10.4", + "@greenwood/cli": "^0.4.0" + }, + "dependencies": { + "@babel/core": "^7.10.4", + "@babel/plugin-transform-runtime": "^7.10.4", + "@babel/preset-env": "^7.10.4", + "@rollup/plugin-babel": "^5.3.0", + "core-js": "^3.4.1" + }, + "devDependencies": { + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/plugin-proposal-private-methods": "^7.10.4", + "@babel/runtime": "^7.10.4", + "@greenwood/cli": "^0.10.0-alpha.10" + } +} diff --git a/packages/plugin-babel/src/.browserslistrc b/packages/plugin-babel/src/.browserslistrc new file mode 100644 index 000000000..b84c67c70 --- /dev/null +++ b/packages/plugin-babel/src/.browserslistrc @@ -0,0 +1,2 @@ +> 1% +not dead \ No newline at end of file diff --git a/packages/cli/src/config/babel.config.js b/packages/plugin-babel/src/babel.config.js similarity index 69% rename from packages/cli/src/config/babel.config.js rename to packages/plugin-babel/src/babel.config.js index 9d1d7e102..c5b65ee95 100644 --- a/packages/cli/src/config/babel.config.js +++ b/packages/plugin-babel/src/babel.config.js @@ -1,10 +1,4 @@ module.exports = { - - // https://github.com/babel/babel/issues/9937#issuecomment-489352549 - sourceType: 'unambiguous', - - // https://github.com/babel/babel/issues/8731#issuecomment-426522500 - ignore: [/[\/\\]core-js/, /@babel[\/\\]runtime/], // https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md#babelpreset-env presets: [ @@ -12,10 +6,14 @@ module.exports = { // https://babeljs.io/docs/en/babel-preset-env '@babel/preset-env', { - + // https://babeljs.io/docs/en/babel-preset-env#usebuiltins useBuiltIns: 'entry', - + + // https://babeljs.io/docs/en/babel-preset-env#modules + // preserves ES Modules + modules: false, + // https://babeljs.io/docs/en/babel-preset-env#corejs corejs: { version: 3, @@ -31,10 +29,11 @@ module.exports = { // https://github.com/babel/babel/issues/8829#issuecomment-456524916 plugins: [ [ + // https://babeljs.io/docs/en/babel-plugin-transform-runtime '@babel/plugin-transform-runtime', { - regenerator: true - }, - '@babel/plugin-syntax-dynamic-import' + regenerator: true, + useESModules: true + } ] ] diff --git a/packages/plugin-babel/src/index.js b/packages/plugin-babel/src/index.js new file mode 100644 index 000000000..364cfaabd --- /dev/null +++ b/packages/plugin-babel/src/index.js @@ -0,0 +1,78 @@ +/* + * + * Enable using Babel for processing JavaScript files. + * + */ +const babel = require('@babel/core'); +const fs = require('fs'); +const path = require('path'); +const { ResourceInterface } = require('@greenwood/cli/src/lib/resource-interface'); +const rollupBabelPlugin = require('@rollup/plugin-babel').default; + +function getConfig (compilation, extendConfig = false) { + const { projectDirectory } = compilation.context; + const configFile = 'babel.config'; + const defaultConfig = require(path.join(__dirname, configFile)); + const userConfig = fs.existsSync(path.join(projectDirectory, `${configFile}.js`)) + ? require(`${projectDirectory}/${configFile}`) + : {}; + let finalConfig = Object.assign({}, userConfig); + + if (extendConfig) { + finalConfig.presets = Array.isArray(userConfig.presets) + ? [...defaultConfig.presets, ...userConfig.presets] + : [...defaultConfig.presets]; + + finalConfig.plugins = Array.isArray(userConfig.plugins) + ? [...defaultConfig.plugins, ...userConfig.plugins] + : [...defaultConfig.plugins]; + } + + return finalConfig; +} + +class BabelResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + this.extensions = ['.js']; + this.contentType = ['text/javascript']; + } + + async shouldIntercept(url) { + return Promise.resolve(path.extname(url) === this.extensions[0] && url.indexOf('node_modules/') < 0); + } + + async intercept(url, body) { + return new Promise(async(resolve, reject) => { + try { + const config = getConfig(this.compilation, this.options.extendConfig); + const result = await babel.transform(body, config); + + resolve({ + body: result.code + }); + } catch (e) { + reject(e); + } + }); + } +} + +module.exports = (options = {}) => { + return [{ + type: 'resource', + name: 'plugin-babel:resource', + provider: (compilation) => new BabelResource(compilation, options) + }, { + type: 'rollup', + name: 'plugin-babel:rollup', + provider: (compilation) => [ + rollupBabelPlugin({ + // https://github.com/rollup/plugins/tree/master/packages/babel#babelhelpers + babelHelpers: options.extendConfig ? 'runtime' : 'bundled', + + ...getConfig(compilation, options.extendConfig) + }) + ] + }]; +}; \ No newline at end of file diff --git a/packages/plugin-babel/test/cases/default/babel.config.js b/packages/plugin-babel/test/cases/default/babel.config.js new file mode 100644 index 000000000..b76997412 --- /dev/null +++ b/packages/plugin-babel/test/cases/default/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: [ + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-private-methods' + ] +}; \ No newline at end of file diff --git a/packages/plugin-babel/test/cases/default/default.spec.js b/packages/plugin-babel/test/cases/default/default.spec.js new file mode 100644 index 000000000..7963ee9cf --- /dev/null +++ b/packages/plugin-babel/test/cases/default/default.spec.js @@ -0,0 +1,76 @@ +/* + * Use Case + * Run Greenwood with Babel processing. + * + * User Result + * Should generate a bare bones Greenwood build with the user's JavaScript files processed + * based on their own babel.config.js file. + * + * User Command + * greenwood build + * + * User Config + * const pluginBabel = require('@greenwod/plugin-babel'); + * + * { + * plugins: [ + * ...pluginBabel() + * ] + * } + * + * User Workspace + * src/ + * pages/ + * index.html + * scripts/ + * main.js + * + * User babel.config.js + * module.exports = { + * plugins: [ + * '@babel/plugin-proposal-class-properties', + * '@babel/plugin-proposal-private-methods' + * ] + * }; + */ +const fs = require('fs'); +const glob = require('glob-promise'); +const path = require('path'); +const expect = require('chai').expect; +const runSmokeTest = require('../../../../../test/smoke-test'); +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'Custom Babel configuration'; + 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'], LABEL); + + describe('Babel should process JavaScript that reference private class members / methods', function() { + it('should output correctly processed JavaScript without private members', function() { + const expectedJavaScript = '#x'; + const jsFiles = glob.sync(path.join(this.context.publicDir, '*.js')); + const javascript = fs.readFileSync(jsFiles[0], 'utf-8'); + + expect(jsFiles.length).to.equal(1); + expect(javascript).to.not.contain(expectedJavaScript); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-babel/test/cases/default/greenwood.config.js b/packages/plugin-babel/test/cases/default/greenwood.config.js new file mode 100644 index 000000000..0adf056d5 --- /dev/null +++ b/packages/plugin-babel/test/cases/default/greenwood.config.js @@ -0,0 +1,7 @@ +const pluginBabel = require('../../../src/index'); + +module.exports = { + plugins: [ + ...pluginBabel() + ] +}; \ No newline at end of file diff --git a/packages/plugin-babel/test/cases/default/src/pages/index.html b/packages/plugin-babel/test/cases/default/src/pages/index.html new file mode 100644 index 000000000..9eb301f72 --- /dev/null +++ b/packages/plugin-babel/test/cases/default/src/pages/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/scripts/main.js"></script> + </head> + + <body> + <h1>Hello World!</h1> + </body> + +</html> \ No newline at end of file diff --git a/packages/plugin-babel/test/cases/default/src/scripts/main.js b/packages/plugin-babel/test/cases/default/src/scripts/main.js new file mode 100644 index 000000000..d4231e479 --- /dev/null +++ b/packages/plugin-babel/test/cases/default/src/scripts/main.js @@ -0,0 +1,18 @@ +class Counter { + #x = 0; + + #logClicked() { + console.debug('private variable "x" was clicked. is now =>', this.#x) + } + + clicked() { + this.#x++; + this.#logClicked(); + } +} + +const counter = new Counter(); + +setInterval(() => { + counter.clicked(); +}, 1000); \ No newline at end of file diff --git a/packages/plugin-babel/test/cases/options.extend-config/babel.config.js b/packages/plugin-babel/test/cases/options.extend-config/babel.config.js new file mode 100644 index 000000000..b76997412 --- /dev/null +++ b/packages/plugin-babel/test/cases/options.extend-config/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: [ + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-private-methods' + ] +}; \ No newline at end of file diff --git a/packages/plugin-babel/test/cases/options.extend-config/greenwood.config.js b/packages/plugin-babel/test/cases/options.extend-config/greenwood.config.js new file mode 100644 index 000000000..208349dff --- /dev/null +++ b/packages/plugin-babel/test/cases/options.extend-config/greenwood.config.js @@ -0,0 +1,9 @@ +const pluginBabel = require('../../../src/index'); + +module.exports = { + plugins: [ + ...pluginBabel({ + extendConfig: true + }) + ] +}; \ No newline at end of file diff --git a/packages/plugin-babel/test/cases/options.extend-config/options.extend-config.spec.js b/packages/plugin-babel/test/cases/options.extend-config/options.extend-config.spec.js new file mode 100644 index 000000000..ff393595d --- /dev/null +++ b/packages/plugin-babel/test/cases/options.extend-config/options.extend-config.spec.js @@ -0,0 +1,93 @@ +/* + * Use Case + * Run Greenwood with Babel processing merging user and default babel.config.js files. + * + * User Result + * Should generate a bare bones Greenwood build with the user's JavaScript files processed + * based on their own babel.config.js file merged with plugin default babel.config.js file. + * + * User Command + * greenwood build + * + * User Config + * const pluginBabel = require('@greenwod/plugin-babel'); + * + * { + * plugins: [ + * ...pluginBabel({ + * extendConfig: true + * }) + * ] + * } + * + * User Workspace + * src/ + * pages/ + * index.html + * scripts/ + * main.js + * + * User babel.config.js + * module.exports = { + * plugins: [ + * '@babel/plugin-proposal-class-properties', + * '@babel/plugin-proposal-private-methods' + * ] + * }; + */ +const fs = require('fs'); +const glob = require('glob-promise'); +const path = require('path'); +const expect = require('chai').expect; +const runSmokeTest = require('../../../../../test/smoke-test'); +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'Custom Babel Options for extending Default Configuration'; + let setup; + + before(async function() { + setup = new TestBed(); + this.context = await setup.setupTestBed(__dirname); + }); + + describe(LABEL, function() { + let jsFiles; + + before(async function() { + await setup.runGreenwoodCommand('build'); + + jsFiles = glob.sync(path.join(this.context.publicDir, '*.js')); + }); + + runSmokeTest(['public', 'index'], LABEL); + + it('should output one JavaScript file', function() { + expect(jsFiles.length).to.equal(1); + }); + + describe('Babel should process JavaScript that reference private class members / methods', function() { + it('should output correctly processed JavaScript without private members', function() { + const notExpectedJavaScript = '#x;'; + const javascript = fs.readFileSync(jsFiles[0], 'utf-8'); + + expect(javascript).to.not.contain(notExpectedJavaScript); + }); + }); + + // find a better way to test for preset-env specifically? + describe('Babel should handle processing of JavaScript per usage of @babel/preset-env', function() { + xit('should output correctly processed JavaScript...', function() { + const expectedJavaScript = 'return e&&e.__esModule'; + const javascript = fs.readFileSync(jsFiles[0], 'utf-8'); + + expect(javascript).to.contain(expectedJavaScript); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-babel/test/cases/options.extend-config/src/pages/index.html b/packages/plugin-babel/test/cases/options.extend-config/src/pages/index.html new file mode 100644 index 000000000..9eb301f72 --- /dev/null +++ b/packages/plugin-babel/test/cases/options.extend-config/src/pages/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/scripts/main.js"></script> + </head> + + <body> + <h1>Hello World!</h1> + </body> + +</html> \ No newline at end of file diff --git a/packages/plugin-babel/test/cases/options.extend-config/src/scripts/main.js b/packages/plugin-babel/test/cases/options.extend-config/src/scripts/main.js new file mode 100644 index 000000000..338027c59 --- /dev/null +++ b/packages/plugin-babel/test/cases/options.extend-config/src/scripts/main.js @@ -0,0 +1,18 @@ +class Counter { + #x = 0; + + #logClicked() { + console.debug('private #x was clicked. is now =>', this.#x) + } + + clicked() { + this.#x++; + this.#logClicked(); + } +} + +const counter = new Counter(); + +setInterval(() => { + counter.clicked(); +}, 1000); \ No newline at end of file diff --git a/packages/plugin-google-analytics/README.md b/packages/plugin-google-analytics/README.md index 4b3f9f9cc..49cbff77d 100644 --- a/packages/plugin-google-analytics/README.md +++ b/packages/plugin-google-analytics/README.md @@ -1,12 +1,13 @@ # @greenwood/plugin-google-analytics ## Overview -A composite plugin for Greenwood for adding support for [Google Analytics](https://developers.google.com/analytics/) JavaScript tracker. For more information and complete docs about Greenwood, please visit the [Greenwood website](https://www.greenwoodjs.io/docs). +A Greenwood plugin adding support for [Google Analytics](https://developers.google.com/analytics/) JavaScript tracker. + +> _For more information and complete docs about Greenwood, please visit the [Greenwood website](https://www.greenwoodjs.io/)._ -> This package assumes you already have `@greenwood/cli` installed. ## Installation -You can use your favorite JavaScript package manager to install this package. +You can use your favorite JavaScript package manager to install this package. This package assumes you already have `@greenwood/cli` installed. _examples:_ ```bash @@ -18,9 +19,7 @@ yarn add @greenwood/plugin-google-analytics --dev ``` ## Usage -Use this plugin in your _greenwood.config.js_ and simply pass in your Google Analytics ID, e.g. `UA-XXXXX`. - -> As this is a composite plugin, you will need to spread the result. +Use this plugin in your _greenwood.config.js_ and pass in your Google Analytics ID, e.g. `UA-XXXXX`. ```javascript const googleAnalyticsPlugin = require('@greenwood/plugin-google-analytics'); @@ -29,7 +28,7 @@ module.exports = { ... plugins: [ - ...googleAnalyticsPlugin({ + googleAnalyticsPlugin({ analyticsId: 'UA-XXXXXX' }) ] @@ -38,9 +37,10 @@ module.exports = { This will then add the Google Analytics [JavaScript tracker snippet](https://developers.google.com/analytics/devguides/collection/analyticsjs/) to your project's _index.html_. -### Options + +## Options - `analyticsId` (required) - Your Google Analytics ID -- `anonymous` (optional) - If tracking of IPs should be done anonymously. Defaults to `true` +- `anonymous` (optional) - Sets if tracking of IPs should be done anonymously. Default is `true` ### Outbound Links For links that go outside of your domain, the global function [`getOutboundLink`](https://support.google.com/analytics/answer/7478520) is available for you to use. @@ -49,7 +49,7 @@ Example: ```html <a target="_blank" - rel="noopener" + rel="noopener" onclick="getOutboundLink('www.mylink.com');" href="www.mylink.com">My Link </a> diff --git a/packages/plugin-google-analytics/package.json b/packages/plugin-google-analytics/package.json index 60d5f4c2c..1f82e1ec4 100644 --- a/packages/plugin-google-analytics/package.json +++ b/packages/plugin-google-analytics/package.json @@ -1,16 +1,14 @@ { "name": "@greenwood/plugin-google-analytics", - "version": "0.9.0", - "description": "A composite plugin for Greenwood for adding support for Google Analytics.", + "version": "0.10.0-alpha.10", + "description": "A Greenwood plugin adding support for Google Analytics JavaScript tracker.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-google-analytics", "author": "Owen Buckley <owen@thegreenhouse.io>", "license": "MIT", "keywords": [ "Greenwood", - "Web Components", - "Lit Element", - "Lit Html", "Static Site Generator", + "Web Components", "Google Analytics" ], "main": "src/index.js", @@ -24,6 +22,6 @@ "@greenwood/cli": "^0.4.0" }, "devDependencies": { - "@greenwood/cli": "^0.9.0" + "@greenwood/cli": "^0.10.0-alpha.10" } } diff --git a/packages/plugin-google-analytics/src/index.js b/packages/plugin-google-analytics/src/index.js index 99613f59b..ec923577a 100644 --- a/packages/plugin-google-analytics/src/index.js +++ b/packages/plugin-google-analytics/src/index.js @@ -1,22 +1,30 @@ -module.exports = (options = {}) => { - const { analyticsId, anonymous } = options; +const path = require('path'); +const { ResourceInterface } = require('@greenwood/cli/src/lib/resource-interface'); + +class GoogleAnalyticsResource extends ResourceInterface { + constructor(compilation, options = {}) { + super(compilation, options); - const validId = analyticsId && typeof analyticsId === 'string'; - const trackAnon = typeof anonymous === 'boolean' ? anonymous : true; + const { analyticsId } = options; - if (!validId) { - throw new Error(`Error: analyticsId should be of type string. get "${typeof analyticsId}" instead.`); + if (!analyticsId || typeof analyticsId !== 'string') { + throw new Error(`Error: analyticsId should be of type string. got "${typeof analyticsId}" instead.`); + } } - return [{ - type: 'index', - provider: () => { - return { - hookGreenwoodAnalytics: ` - <link rel="preconnect" href="https://www.google-analytics.com/"> + async shouldOptimize(url) { + return Promise.resolve(path.extname(url) === '.html'); + } - <script async src="https://www.googletagmanager.com/gtag/js?id=${analyticsId}"></script> + async optimize(url, body) { + const { analyticsId, anonymous } = this.options; + const trackAnon = typeof anonymous === 'boolean' ? anonymous : true; + return new Promise((resolve, reject) => { + try { + const newHtml = body.replace('</head>', ` + <link rel="preconnect" href="https://www.google-analytics.com/"> + <script async src="https://www.googletagmanager.com/gtag/js?id=${analyticsId}"></script> <script> var getOutboundLink = function(url) { gtag('event', 'click', { @@ -25,16 +33,27 @@ module.exports = (options = {}) => { 'transport_type': 'beacon' }); } - window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); - gtag('config', '${analyticsId}', { 'anonymize_ip': ${trackAnon} }); gtag('config', '${analyticsId}'); </script> - ` - }; - } - }]; + </head> + `); + + 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 69514f7e1..38e7ed48b 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' * }) * }] @@ -45,13 +45,13 @@ describe('Build Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'index', 'not-found', 'hello'], LABEL); + runSmokeTest(['public', 'index'], LABEL); describe('Initialization script', function() { let inlineScript = []; let scriptSrcTags = []; - beforeEach(async function() { + before(async function() { const dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); const scriptTags = dom.window.document.querySelectorAll('head script'); @@ -78,11 +78,9 @@ describe('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}'); `; @@ -98,7 +96,7 @@ describe('Build Greenwood With: ', function() { describe('Link Preconnect', function() { let linkTag; - beforeEach(async function() { + before(async function() { const dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); const linkTags = dom.window.document.querySelectorAll('head link'); @@ -119,7 +117,7 @@ describe('Build Greenwood With: ', function() { describe('Tracking script', function() { let trackingScript; - beforeEach(async function() { + before(async function() { const dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); const scriptTags = dom.window.document.querySelectorAll('head script'); 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 d6670b62d..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() * }] * * } @@ -37,7 +37,7 @@ describe('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 186b671d5..965a3b943 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 * }) @@ -46,12 +46,12 @@ describe('Build Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'index', 'not-found', 'hello'], LABEL); + runSmokeTest(['public', 'index'], LABEL); describe('Initialization script', function() { let inlineScript; - beforeEach(async function() { + before(async function() { const dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); const scriptTags = dom.window.document.querySelectorAll('head script'); @@ -70,7 +70,6 @@ describe('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}'); `; @@ -82,7 +81,7 @@ describe('Build Greenwood With: ', function() { describe('Tracking script', function() { let trackingScript; - beforeEach(async function() { + before(async function() { const dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); const scriptTags = dom.window.document.querySelectorAll('head script'); diff --git a/packages/plugin-graphql/README.md b/packages/plugin-graphql/README.md new file mode 100644 index 000000000..4c245d3e1 --- /dev/null +++ b/packages/plugin-graphql/README.md @@ -0,0 +1,84 @@ +# @greenwood/plugin-graphl + +## Overview +A plugin for Greenwood to support using [GraphQL](https://graphql.org/) to query your content graph. It runs [**apollo-server**](https://www.apollographql.com/docs/apollo-server/) on the backend and provides an [**@apollo/client** like](https://www.apollographql.com/docs/react/api/core/ApolloClient/#ApolloClient.readQuery) interface for the frontend. + +> This package assumes you already have `@greenwood/cli` installed. + +## Installation +You can use your favorite JavaScript package manager to install this package. + +_examples:_ +```bash +# npm +npm -i @greenwood/plugin-graphql --save-dev + +# yarn +yarn add @greenwood/plugin-graphql --dev +``` + +## Usage +Add this plugin to your _greenwood.config.js_ and spread the `export`. + +```javascript +const pluginGraphQL = require('@greenwood/plugin-graphql'); + +module.exports = { + ... + + plugins: [ + ...pluginGraphQL() // notice the spread ... ! + ] +} +``` + +## Example +This will then allow you to use GraphQL to query your content. + +```js +import client from '@greenwood/plugin-graphql/core/client'; +import MenuQuery from '@greenwood/plugin-graphql/queries/menu'; + +class HeaderComponent extends HTMLElement { + constructor() { + super(); + + this.root = this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + const response = await client.query({ + query: MenuQuery, + variables: { + name: 'navigation', + order: 'index_asc' + } + }); + + this.navigation = response.data.menu.children.map(item => item.item); + this.root.innerHTML = this.getTemplate(navigation); + } + + getTemplate(navigation) { + const navigationList = navigation.map((menuItem) => { + return ` + <li> + <a href="${menuItem.route}" title="Click to visit the ${menuItem.label} page">${menuItem.label}</a> + </li> + `; + }).join(); + + return ` + <header> + <nav> + <ul> + ${navigationList} + </ul> + </nav> + <header> + `; + } +} +``` + +> _For more information on using GraphQL with Greenwood, [please review our docs](https://www.greenwoodjs.io/docs/data)._ \ No newline at end of file diff --git a/packages/plugin-graphql/package.json b/packages/plugin-graphql/package.json new file mode 100644 index 000000000..f703271e0 --- /dev/null +++ b/packages/plugin-graphql/package.json @@ -0,0 +1,35 @@ +{ + "name": "@greenwood/plugin-graphql", + "version": "0.10.0-alpha.10", + "description": "A plugin for using GraphQL for querying your content.", + "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-graphql", + "author": "Owen Buckley <owen@thegreenhouse.io>", + "license": "MIT", + "keywords": [ + "Greenwood", + "GraphQL", + "Static Site Generator", + "NodeJS", + "Apollo" + ], + "main": "src/index.js", + "files": [ + "src/" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@greenwood/cli": "^0.4.0" + }, + "dependencies": { + "@apollo/client": "^3.3.11", + "@rollup/plugin-alias": "^3.1.2", + "apollo-server": "^2.21.0", + "graphql": "^15.5.0", + "graphql-tag": "^2.10.1" + }, + "devDependencies": { + "@greenwood/cli": "^0.10.0-alpha.10" + } +} diff --git a/packages/cli/src/data/cache.js b/packages/plugin-graphql/src/core/cache.js similarity index 54% rename from packages/cli/src/data/cache.js rename to packages/plugin-graphql/src/core/cache.js index 6690b2f3b..029a99c74 100644 --- a/packages/cli/src/data/cache.js +++ b/packages/plugin-graphql/src/core/cache.js @@ -1,11 +1,8 @@ -const { ApolloClient } = require('apollo-client'); -const createHttpLink = require('apollo-link-http').createHttpLink; +const { ApolloClient, InMemoryCache, HttpLink } = require('@apollo/client/core'); const fetch = require('node-fetch'); -const fs = require('fs-extra'); +const fs = require('fs'); const { gql } = require('apollo-server'); -const InMemoryCache = require('apollo-cache-inmemory').InMemoryCache; -const path = require('path'); -const { getQueryHash } = require('./common'); +const { getQueryHash } = require('./common.server'); /* Extract cache server-side */ module.exports = async (req, context) => { @@ -13,7 +10,7 @@ module.exports = async (req, context) => { return new Promise(async(resolve, reject) => { try { const client = await new ApolloClient({ - link: createHttpLink({ + link: new HttpLink({ uri: 'http://localhost:4000?q=internal', /* internal flag to prevent looping cache on request */ fetch }), @@ -22,27 +19,27 @@ module.exports = async (req, context) => { /* Take the same query from request, and repeat the query for our server side cache */ const { query, variables } = req.body; - const queryObj = gql`${query}`; - const { data } = await client.query({ - query: queryObj, + query: gql`${query}`, variables }); if (data) { - const cache = JSON.stringify(client.extract()); - const queryHash = getQueryHash(queryObj, variables); + const { outputDir } = context; + const cache = JSON.stringify(data); + const queryHash = getQueryHash(query, variables); const hashFilename = `${queryHash}-cache.json`; - const cachePath = `${context.publicDir}/${queryHash}-cache.json`; - - if (!fs.existsSync(context.publicDir)) { - fs.mkdirSync(context.publicDir); + const cachePath = `${outputDir}/${hashFilename}`; + + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); } if (!fs.existsSync(cachePath)) { - fs.writeFileSync(path.join(context.publicDir, hashFilename), cache, 'utf8'); + fs.writeFileSync(cachePath, cache, 'utf8'); } } + resolve(); } catch (err) { console.error('create cache error', err); diff --git a/packages/cli/src/data/client.js b/packages/plugin-graphql/src/core/client.js similarity index 55% rename from packages/cli/src/data/client.js rename to packages/plugin-graphql/src/core/client.js index f562b3399..b968ce87d 100644 --- a/packages/cli/src/data/client.js +++ b/packages/plugin-graphql/src/core/client.js @@ -1,15 +1,24 @@ -import { ApolloClient } from 'apollo-client'; -import { InMemoryCache } from 'apollo-cache-inmemory'; -import { HttpLink } from 'apollo-link-http'; -import { getQueryHash } from '@greenwood/cli/data/common'; +import { getQueryHash } from '@greenwood/plugin-graphql/core/common'; + +const client = { + query: (params) => { + const { query, variables = {} } = params; + + return fetch('http://localhost:4000/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + query, + variables + }) + }).then((response) => response.json()); + } +}; const APOLLO_STATE = window.__APOLLO_STATE__; // eslint-disable-line no-underscore-dangle -const client = new ApolloClient({ - cache: new InMemoryCache(), - link: new HttpLink({ - uri: 'http://localhost:4000' - }) -}); const backupQuery = client.query; client.query = (params) => { @@ -21,9 +30,8 @@ client.query = (params) => { return fetch(cachePath) .then(response => response.json()) .then((response) => { - // mock client.query response return { - data: new InMemoryCache().restore(response).readQuery(params) + data: response }; }); } else { diff --git a/packages/plugin-graphql/src/core/common.client.js b/packages/plugin-graphql/src/core/common.client.js new file mode 100644 index 000000000..aa46cb316 --- /dev/null +++ b/packages/plugin-graphql/src/core/common.client.js @@ -0,0 +1,23 @@ +// https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0#gistcomment-2775538 +function hashString(queryKeysString) { + let h = 0; + + for (let i = 0; i < queryKeysString.length; i += 1) { + h = Math.imul(31, h) + queryKeysString.charCodeAt(i) | 0; // eslint-disable-line no-bitwise + } + + return Math.abs(h).toString(); +} + +function getQueryHash(query, variables = {}) { + const queryKeys = query; + const variableValues = Object.keys(variables).length > 0 + ? `_${Object.values(variables).join('').replace(/\//g, '')}` // handle / which will translate to filepaths + : ''; + + return hashString(`${queryKeys}${variableValues}`); +} + +export { + getQueryHash +}; \ No newline at end of file diff --git a/packages/plugin-graphql/src/core/common.server.js b/packages/plugin-graphql/src/core/common.server.js new file mode 100644 index 000000000..85026762e --- /dev/null +++ b/packages/plugin-graphql/src/core/common.server.js @@ -0,0 +1,23 @@ +// https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0#gistcomment-2775538 +function hashString(queryKeysString) { + let h = 0; + + for (let i = 0; i < queryKeysString.length; i += 1) { + h = Math.imul(31, h) + queryKeysString.charCodeAt(i) | 0; // eslint-disable-line no-bitwise + } + + return Math.abs(h).toString(); +} + +function getQueryHash(query, variables = {}) { + const queryKeys = query; + const variableValues = Object.keys(variables).length > 0 + ? `_${Object.values(variables).join('').replace(/\//g, '')}` // handle / which will translate to filepaths + : ''; + + return hashString(`${queryKeys}${variableValues}`); +} + +module.exports = { + getQueryHash +}; \ No newline at end of file diff --git a/packages/cli/src/data/server.js b/packages/plugin-graphql/src/core/server.js similarity index 90% rename from packages/cli/src/data/server.js rename to packages/plugin-graphql/src/core/server.js index 584a947e6..5a9db7452 100644 --- a/packages/cli/src/data/server.js +++ b/packages/plugin-graphql/src/core/server.js @@ -2,7 +2,7 @@ const { ApolloServer } = require('apollo-server'); module.exports = (compilation) => { const { config, graph, context } = compilation; - const schema = require('./schema/schema')(graph); + const schema = require('../schema/schema')(graph); const createCache = require('./cache'); const server = new ApolloServer({ @@ -15,7 +15,7 @@ module.exports = (compilation) => { }, context: async (integrationContext) => { const { req } = integrationContext; - + if (req.query.q !== 'internal') { await createCache(req, context); } diff --git a/packages/plugin-graphql/src/index.js b/packages/plugin-graphql/src/index.js new file mode 100644 index 000000000..6b9923a9b --- /dev/null +++ b/packages/plugin-graphql/src/index.js @@ -0,0 +1,120 @@ +const fs = require('fs'); +const graphqlServer = require('./core/server'); +const path = require('path'); +const { ResourceInterface } = require('@greenwood/cli/src/lib/resource-interface'); +const { ServerInterface } = require('@greenwood/cli/src/lib/server-interface'); +const rollupPluginAlias = require('@rollup/plugin-alias'); + +class GraphQLResource extends ResourceInterface { + constructor(compilation, options = {}) { + super(compilation, options); + this.extensions = ['.gql']; + this.contentType = ['text/javascript']; + } + + async serve(url) { + return new Promise(async (resolve, reject) => { + try { + const js = await fs.promises.readFile(url, 'utf-8'); + const body = ` + export default \`${js}\`; + `; + + resolve({ + body, + contentType: this.contentType + }); + } catch (e) { + reject(e); + } + }); + } + + async shouldIntercept(url, body, headers) { + return Promise.resolve(headers.request.accept && headers.request.accept.indexOf('text/html') >= 0); + } + + async intercept(url, body) { + return new Promise(async (resolve, reject) => { + try { + // es-modules-shims breaks on dangling commas in an importMap :/ + const danglingComma = body.indexOf('"imports": {}') > 0 + ? '' + : ','; + const shimmedBody = body.replace('"imports": {', ` + "imports": { + "@greenwood/plugin-graphql/core/client": "/node_modules/@greenwood/plugin-graphql/src/core/client.js", + "@greenwood/plugin-graphql/core/common": "/node_modules/@greenwood/plugin-graphql/src/core/common.client.js", + "@greenwood/plugin-graphql/queries/children": "/node_modules/@greenwood/plugin-graphql/src/queries/children.gql", + "@greenwood/plugin-graphql/queries/config": "/node_modules/@greenwood/plugin-graphql/src/queries/config.gql", + "@greenwood/plugin-graphql/queries/graph": "/node_modules/@greenwood/plugin-graphql/src/queries/graph.gql", + "@greenwood/plugin-graphql/queries/menu": "/node_modules/@greenwood/plugin-graphql/src/queries/menu.gql"${danglingComma} + `); + + resolve({ body: shimmedBody }); + } catch (e) { + reject(e); + } + }); + } + + async shouldOptimize(url) { + return Promise.resolve(path.extname(url) === '.html'); + } + + async optimize(url, body) { + return new Promise((resolve, reject) => { + try { + body = body.replace('<head>', ` + <script data-state="apollo"> + window.__APOLLO_STATE__ = true; + </script> + <head> + `); + + resolve(body); + } catch (e) { + reject(e); + } + }); + } +} + +class GraphQLServer extends ServerInterface { + constructor(compilation, options = {}) { + super(compilation, options); + } + + async start() { + return graphqlServer(this.compilation).listen().then((server) => { + console.log(`GraphQLServer started at ${server.url}`); + }); + } +} + +module.exports = (options = {}) => { + return [{ + type: 'server', + name: 'plugin-graphql:server', + provider: (compilation) => new GraphQLServer(compilation, options) + }, { + type: 'resource', + name: 'plugin-graphql:resource', + provider: (compilation) => new GraphQLResource(compilation, options) + }, { + type: 'rollup', + name: 'plugin-graphql:rollup', + provider: () => [ + rollupPluginAlias({ + entries: [ + { find: '@greenwood/plugin-graphql/core/client', replacement: '@greenwood/plugin-graphql/src/core/client.js' }, + { find: '@greenwood/plugin-graphql/core/common', replacement: '@greenwood/plugin-graphql/src/core/common.client.js' }, + { find: '@greenwood/plugin-graphql/queries/menu', replacement: '@greenwood/plugin-graphql/src/queries/menu.gql' }, + { find: '@greenwood/plugin-graphql/queries/config', replacement: '@greenwood/plugin-graphql/src/queries/config.gql' }, + { find: '@greenwood/plugin-graphql/queries/children', replacement: '@greenwood/plugin-graphql/src/queries/children.gql' }, + { find: '@greenwood/plugin-graphql/queries/graph', replacement: '@greenwood/plugin-graphql/src/queries/graph.gql' } + ] + }) + ] + }]; +}; \ No newline at end of file diff --git a/packages/plugin-graphql/src/queries/children.gql b/packages/plugin-graphql/src/queries/children.gql new file mode 100644 index 000000000..295a70a64 --- /dev/null +++ b/packages/plugin-graphql/src/queries/children.gql @@ -0,0 +1,11 @@ +query($parent: String!) { + children(parent: $parent) { + id, + filename, + label, + path, + route, + template, + title + } +} \ No newline at end of file diff --git a/packages/cli/src/data/queries/config.gql b/packages/plugin-graphql/src/queries/config.gql similarity index 82% rename from packages/cli/src/data/queries/config.gql rename to packages/plugin-graphql/src/queries/config.gql index 24d57c5fd..11d119399 100644 --- a/packages/cli/src/data/queries/config.gql +++ b/packages/plugin-graphql/src/queries/config.gql @@ -1,8 +1,7 @@ query { config { devServer { - port, - host + port }, meta { name, @@ -12,8 +11,8 @@ query { value, href }, + mode, optimization, - publicPath, title, workspace } diff --git a/packages/plugin-graphql/src/queries/graph.gql b/packages/plugin-graphql/src/queries/graph.gql new file mode 100644 index 000000000..99201dbc6 --- /dev/null +++ b/packages/plugin-graphql/src/queries/graph.gql @@ -0,0 +1,11 @@ +query { + graph { + id, + filename, + label, + path, + route, + template, + title + } +} \ No newline at end of file diff --git a/packages/cli/src/data/queries/menu.gql b/packages/plugin-graphql/src/queries/menu.gql similarity index 86% rename from packages/cli/src/data/queries/menu.gql rename to packages/plugin-graphql/src/queries/menu.gql index 101c77ece..ad205cec3 100644 --- a/packages/cli/src/data/queries/menu.gql +++ b/packages/plugin-graphql/src/queries/menu.gql @@ -2,17 +2,17 @@ query($name: String, $route: String, $order: MenuOrderBy) { menu(name: $name, pathname: $route, orderBy: $order) { item { label, - link + route } children { item { label, - link + route }, children { item { label, - link + route } } } diff --git a/packages/cli/src/data/schema/config.js b/packages/plugin-graphql/src/schema/config.js similarity index 92% rename from packages/cli/src/data/schema/config.js rename to packages/plugin-graphql/src/schema/config.js index 7c715ba9f..65cfaa2e8 100644 --- a/packages/cli/src/data/schema/config.js +++ b/packages/plugin-graphql/src/schema/config.js @@ -7,8 +7,7 @@ const getConfiguration = async (root, query, context) => { // https://www.greenwoodjs.io/docs/configuration const configTypeDefs = gql` type DevServer { - port: Int, - host: String + port: Int } type Meta { @@ -23,8 +22,8 @@ const configTypeDefs = gql` type Config { devServer: DevServer, meta: [Meta], + mode: String, optimization: String, - publicPath: String, title: String, workspace: String } diff --git a/packages/cli/src/data/schema/graph.js b/packages/plugin-graphql/src/schema/graph.js similarity index 62% rename from packages/cli/src/data/schema/graph.js rename to packages/plugin-graphql/src/schema/graph.js index f5a74b73d..3c7f2a4f8 100644 --- a/packages/cli/src/data/schema/graph.js +++ b/packages/plugin-graphql/src/schema/graph.js @@ -6,7 +6,7 @@ const getMenuFromGraph = async (root, { name, pathname, orderBy }, context) => { graph .forEach((page) => { - const { route, data, title } = page; + const { route, data, label } = page; const { menu, index, tableOfContents, linkheadings } = data; let children = getParsedHeadingsFromPage(tableOfContents, linkheadings); @@ -20,17 +20,19 @@ const getMenuFromGraph = async (root, { name, pathname, orderBy }, context) => { } if (route.includes(baseRoute)) { - items.push({ item: { link: route, label: title, index }, children }); + items.push({ item: { route, label, index }, children }); } } else { - items.push({ item: { link: route, label: title, index }, children }); + items.push({ item: { route, label, index }, children }); } } }); + if (orderBy !== '') { items = sortMenuItems(items, orderBy); } - return { item: { label: name, link: 'na' }, children: items }; + + return Promise.resolve({ item: { label: name }, children: items }); }; const sortMenuItems = (menuItems, order) => { @@ -70,52 +72,15 @@ const getParsedHeadingsFromPage = (tableOfContents, headingLevel) => { tableOfContents.forEach(({ content, slug, lvl }) => { // make sure we only add heading elements of the same level (h1, h2, h3) if (lvl === headingLevel) { - children.push({ item: { label: content, link: '#' + slug }, children: [] }); + children.push({ item: { label: content, route: '#' + slug }, children: [] }); } }); } return children; }; -const getDeriveMetaFromRoute = (route) => { - const root = route.split('/')[1] || ''; - const label = root - .replace('/', '') - .replace('-', ' ') - .split(' ') - .map((word) => `${word.charAt(0).toUpperCase()}${word.substring(1)}`) - .join(' '); - - return { - label, - root - }; -}; - const getPagesFromGraph = async (root, query, context) => { - const pages = []; - const { graph } = context; - - graph - .forEach((page) => { - const { route, mdFile, fileName, template, title, data } = page; - const { label } = getDeriveMetaFromRoute(route); - const id = page.label; - - pages.push({ - id, - filePath: mdFile, - fileName, - template, - title: title !== '' ? title : label, - link: route, - data: { - ...data - } - }); - }); - - return pages; + return Promise.resolve(context.graph); }; const getChildrenFromParentRoute = async (root, query, context) => { @@ -125,44 +90,32 @@ const getChildrenFromParentRoute = async (root, query, context) => { graph .forEach((page) => { - const { route, mdFile, fileName, template, title, data } = page; - const { label } = getDeriveMetaFromRoute(route); + const { route, path } = page; const root = route.split('/')[1]; - if (root.indexOf(parent) >= 0 && mdFile !== `./${parent}/index.md`) { - const id = page.label; - - pages.push({ - id, - filePath: mdFile, - fileName, - template, - title: title !== '' ? title : label, - link: route, - data: { - ...data - } - }); + if (root === parent && path.indexOf(`${parent}/index.md`) < 0) { + pages.push(page); } }); - return pages; + return Promise.resolve(pages); }; const graphTypeDefs = gql` type Page { data: Data, + filename: String, id: String, - filePath: String, - fileName: String, + label: String, + path: String, + route: String, template: String, - link: String, title: String } type Link { label: String, - link: String + route: String } type Menu { diff --git a/packages/cli/src/data/schema/schema.js b/packages/plugin-graphql/src/schema/schema.js similarity index 100% rename from packages/cli/src/data/schema/schema.js rename to packages/plugin-graphql/src/schema/schema.js diff --git a/packages/plugin-graphql/test/cases/query-children/greenwood.config.js b/packages/plugin-graphql/test/cases/query-children/greenwood.config.js new file mode 100644 index 000000000..edb1b78c3 --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-children/greenwood.config.js @@ -0,0 +1,10 @@ +const pluginGraphQL = require('../../../src/index'); + +module.exports = { + title: 'GraphQL ConfigQuery Spec', + + plugins: [ + ...pluginGraphQL() + ] + +}; \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-children/package.json b/packages/plugin-graphql/test/cases/query-children/package.json new file mode 100644 index 000000000..75d12a9b7 --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-children/package.json @@ -0,0 +1,6 @@ +{ + "name": "plugin-graphql-test-children-query", + "dependencies": { + "lit-element": "^2.4.0" + } +} \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-children/query-children.spec.js b/packages/plugin-graphql/test/cases/query-children/query-children.spec.js new file mode 100644 index 000000000..af227ef2c --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-children/query-children.spec.js @@ -0,0 +1,151 @@ +/* + * Use Case + * Run Greenwood build command with GraphQL calls to get data about the projects graph using ChildrenQuaery. + * + * User Result + * Should generate a Greenwood build that tests basic output from the ChildrenQuery. + * + * User Command + * greenwood build + * + * Default Config + * + * Custom Workspace + * src/ + * components/ + * posts-list.js + * pages/ + * blog/ + * first-post/ + * index.md + * second-post/ + * index.md + * index.html + */ +const expect = require('chai').expect; +const glob = require('glob-promise'); +const { JSDOM } = require('jsdom'); +const path = require('path'); +const runSmokeTest = require('../../../../../test/smoke-test'); +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'Children from GraphQL'; + const apolloStateRegex = /window.__APOLLO_STATE__ = true/; + let setup; + + before(async function() { + setup = new TestBed(); + + const greenwoodGraphqlCoreLibs = (await glob(`${process.cwd()}/packages/plugin-graphql/src/core/*.js`)).map((lib) => { + return { + dir: 'node_modules/@greenwood/plugin-graphql/src/core/', + name: path.basename(lib) + }; + }); + const greenwoodGraphqlQueryLibs = (await glob(`${process.cwd()}/packages/plugin-graphql/src/queries/*.gql`)).map((lib) => { + return { + dir: 'node_modules/@greenwood/plugin-graphql/src/queries/', + name: path.basename(lib) + }; + }); + const litElementLibs = (await glob(`${process.cwd()}/node_modules/lit-element/lib/*.js`)).map((lib) => { + return { + dir: 'node_modules/lit-element/lib/', + name: path.basename(lib) + }; + }); + const litHtmlLibs = (await glob(`${process.cwd()}/node_modules/lit-html/lib/*.js`)).map((lib) => { + return { + dir: 'node_modules/lit-html/lib/', + name: path.basename(lib) + }; + }); + + this.context = await setup.setupTestBed(__dirname, [ + ...greenwoodGraphqlCoreLibs, + ...greenwoodGraphqlQueryLibs, + { + // lit-element (+ lit-html) + dir: 'node_modules/lit-element/', + name: 'lit-element.js' + }, { + dir: 'node_modules/lit-element/', + name: 'package.json' + }, + + ...litElementLibs, + + { + dir: 'node_modules/lit-html/', + name: 'lit-html.js' + }, { + dir: 'node_modules/lit-html/', + name: 'package.json' + }, + + ...litHtmlLibs + ]); + }); + + describe(LABEL, function() { + + before(async function() { + await setup.runGreenwoodCommand('build'); + }); + + runSmokeTest(['public', 'index'], LABEL); + + describe('Home Page output w/ ChildrenQuery', function() { + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', function() { + const scriptTags = dom.window.document.querySelectorAll('script'); + const apolloScriptTags = Array.prototype.slice.call(scriptTags).filter(script => { + return script.getAttribute('data-state') === 'apollo'; + }); + const innerHTML = apolloScriptTags[0].innerHTML; + + expect(apolloScriptTags.length).to.equal(1); + expect(innerHTML).to.match(apolloStateRegex); + }); + + it('should output a single (partial) *-cache.json file, one per each query made', async function() { + expect(await glob.promise(path.join(this.context.publicDir, './*-cache.json'))).to.have.lengthOf(1); + }); + + it('should output a (partial) *-cache.json files, one per each query made, that are all defined', async function() { + const cacheFiles = await glob.promise(path.join(this.context.publicDir, './*-cache.json')); + + cacheFiles.forEach(file => { + const cache = require(file); + + expect(cache).to.not.be.undefined; + }); + }); + + it('should have a <ul> in the <body>', function() { + const lists = dom.window.document.querySelectorAll('body ul'); + + expect(lists.length).to.be.equal(1); + }); + + it('should have a expected navigation output in the <header> based on pages with menu: navigation frontmatter', function() { + const listItems = dom.window.document.querySelectorAll('body ul li'); + + expect(listItems.length).to.be.equal(2); + + expect(listItems[0].innerHTML).to.be.contain('First Post'); + expect(listItems[1].innerHTML).to.be.contain('Second Post'); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-children/src/components/posts-list.js b/packages/plugin-graphql/test/cases/query-children/src/components/posts-list.js new file mode 100644 index 000000000..24dab85ba --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-children/src/components/posts-list.js @@ -0,0 +1,55 @@ +import { html, LitElement } from 'lit-element'; +import client from '@greenwood/plugin-graphql/core/client'; +import ChildrenQuery from '@greenwood/plugin-graphql/queries/children'; + +class PostsListTemplate extends LitElement { + + static get properties() { + return { + posts: { + type: Array + } + }; + } + + constructor() { + super(); + this.posts = []; + } + + async connectedCallback() { + super.connectedCallback(); + const response = await client.query({ + query: ChildrenQuery, + variables: { + parent: 'blog' + } + }); + + this.posts = response.data.children; + } + + /* eslint-disable indent */ + render() { + const { posts } = this; + + return html` + <h1>My Posts</h1> + + <div class="posts"> + <ul> + ${posts.map((post) => { + return html` + <li> + <a href="${post.route}" title="Click to read my ${post.title} blog post">${post.title} Post</a> + </li> + `; + })} + </ul> + </div> + `; + } + /* eslint-enable */ +} + +customElements.define('posts-list', PostsListTemplate); \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-children/src/pages/blog/first-post/index.md b/packages/plugin-graphql/test/cases/query-children/src/pages/blog/first-post/index.md new file mode 100644 index 000000000..8af878ee7 --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-children/src/pages/blog/first-post/index.md @@ -0,0 +1,8 @@ +--- +title: 'First' +--- + +## My First Blog Post +Lorem Ipsum + +[back](/) \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-children/src/pages/blog/second-post/index.md b/packages/plugin-graphql/test/cases/query-children/src/pages/blog/second-post/index.md new file mode 100644 index 000000000..d64aad85e --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-children/src/pages/blog/second-post/index.md @@ -0,0 +1,8 @@ +--- +title: 'Second' +--- + +## My Second Blog Post +Lorem Ipsum + +[back](/) \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-children/src/pages/index.html b/packages/plugin-graphql/test/cases/query-children/src/pages/index.html new file mode 100644 index 000000000..4400ac0eb --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-children/src/pages/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/components/posts-list.js"></script> + </head> + + <body> + <posts-list></posts-list> + </body> + +</html> \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-config/greenwood.config.js b/packages/plugin-graphql/test/cases/query-config/greenwood.config.js new file mode 100644 index 000000000..edb1b78c3 --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-config/greenwood.config.js @@ -0,0 +1,10 @@ +const pluginGraphQL = require('../../../src/index'); + +module.exports = { + title: 'GraphQL ConfigQuery Spec', + + plugins: [ + ...pluginGraphQL() + ] + +}; \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-config/query-config.spec.js b/packages/plugin-graphql/test/cases/query-config/query-config.spec.js new file mode 100644 index 000000000..36d77ebcf --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-config/query-config.spec.js @@ -0,0 +1,108 @@ +/* + * Use Case + * Run Greenwood build command with GraphQL calls to get data from the project configuration. + * + * User Result + * Should generate a Greenwood build that dynamically serializes data from the config in the footer. + * + * User Command + * greenwood build + * + * Default Config (+ plugin-graphql) + * + * Custom Workspace + * greenwood.config.js + * src/ + * components/ + * footer.js + * pages/ + * index.html + */ +const expect = require('chai').expect; +const greenwoodConfig = require('./greenwood.config'); +const glob = require('glob-promise'); +const { JSDOM } = require('jsdom'); +const path = require('path'); +const runSmokeTest = require('../../../../../test/smoke-test'); +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'ConfigQuery from GraphQL'; + const apolloStateRegex = /window.__APOLLO_STATE__ = true/; + let setup; + + before(async function() { + setup = new TestBed(); + + const greenwoodGraphqlCoreLibs = (await glob(`${process.cwd()}/packages/plugin-graphql/src/core/*.js`)).map((lib) => { + return { + dir: 'node_modules/@greenwood/plugin-graphql/src/core/', + name: path.basename(lib) + }; + }); + + const greenwoodGraphqlQueryLibs = (await glob(`${process.cwd()}/packages/plugin-graphql/src/queries/*.gql`)).map((lib) => { + return { + dir: 'node_modules/@greenwood/plugin-graphql/src/queries/', + name: path.basename(lib) + }; + }); + + this.context = await setup.setupTestBed(__dirname, [ + ...greenwoodGraphqlCoreLibs, + ...greenwoodGraphqlQueryLibs + ]); + }); + + describe(LABEL, function() { + + before(async function() { + await setup.runGreenwoodCommand('build'); + }); + + runSmokeTest(['public', 'index'], LABEL); + + describe('displaying config title in the footer using ConfigQuery', function() { + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + it('should have a <footer> in the <body> with greenwoodConfig#title as the text value', function() { + const footer = dom.window.document.querySelector('body footer'); + + expect(footer.innerHTML).to.be.equal(greenwoodConfig.title); + }); + + it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', function() { + const scriptTags = dom.window.document.querySelectorAll('script'); + const apolloScriptTags = Array.prototype.slice.call(scriptTags).filter(script => { + return script.getAttribute('data-state') === 'apollo'; + }); + const innerHTML = apolloScriptTags[0].innerHTML; + + expect(apolloScriptTags.length).to.equal(1); + expect(innerHTML).to.match(apolloStateRegex); + }); + + it('should output a single (partial) *-cache.json file, one per each query made', async function() { + expect(await glob.promise(path.join(this.context.publicDir, './*-cache.json'))).to.have.lengthOf(1); + }); + + it('should output a (partial) *-cache.json files, one per each query made, that are all defined', async function() { + const cacheFiles = await glob.promise(path.join(this.context.publicDir, './*-cache.json')); + + cacheFiles.forEach(file => { + const cache = require(file); + + expect(cache).to.not.be.undefined; + }); + }); + + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-config/src/components/footer.js b/packages/plugin-graphql/test/cases/query-config/src/components/footer.js new file mode 100644 index 000000000..cddb2ca57 --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-config/src/components/footer.js @@ -0,0 +1,22 @@ +import client from '@greenwood/plugin-graphql/core/client'; +import ConfigQuery from '@greenwood/plugin-graphql/queries/config'; + +class FooterComponent extends HTMLElement { + constructor() { + super(); + + this.root = this.attachShadow({ mode: 'open' }); + } + + async connectedCallback() { + await client.query({ + query: ConfigQuery + }).then((response) => { + this.root.innerHTML = ` + <footer>${response.data.config.title}</footer> + `; + }); + } +} + +customElements.define('app-footer', FooterComponent); \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-config/src/pages/index.html b/packages/plugin-graphql/test/cases/query-config/src/pages/index.html new file mode 100644 index 000000000..cf32923ab --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-config/src/pages/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/components/footer.js"></script> + </head> + + <body> + <app-footer></app-footer> + </body> + +</html> \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-custom-frontmatter/greenwood.config.js b/packages/plugin-graphql/test/cases/query-custom-frontmatter/greenwood.config.js new file mode 100644 index 000000000..3bbc9d5e1 --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-custom-frontmatter/greenwood.config.js @@ -0,0 +1,9 @@ +const pluginGraphQL = require('../../../src/index'); + +module.exports = { + + plugins: [ + ...pluginGraphQL() + ] + +}; \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-custom-frontmatter/package.json b/packages/plugin-graphql/test/cases/query-custom-frontmatter/package.json new file mode 100644 index 000000000..83a44c517 --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-custom-frontmatter/package.json @@ -0,0 +1,6 @@ +{ + "name": "plugin-graphql-test-query-custom-frontmatter", + "dependencies": { + "lit-element": "^2.4.0" + } +} \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-custom-frontmatter/query-custom-frontmatter.spec.js b/packages/plugin-graphql/test/cases/query-custom-frontmatter/query-custom-frontmatter.spec.js new file mode 100644 index 000000000..73f78bd9c --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-custom-frontmatter/query-custom-frontmatter.spec.js @@ -0,0 +1,177 @@ +/* + * Use Case + * Run Greenwood build command with GraphQL calls to get data about the projects graph using ChildrenQuery, simulating + * a link of blog posts derivered from a pages/blog directory with custom frontmatter. Also uses LitElement. + * + * User Result + * Should generate a Greenwood build that dynamically serializes data from the graph in the body + * of the home page as a list of blog post links. + * + * User Command + * greenwood build + * + * Default Config (+ plugin-graphql) + * + * Custom Workspace + * src/ + * components/ + * posts-list.js + * pages/ + * blog/ + * first-post/ + * index.md + * second-post/ + * index.md + * index.html + */ +const expect = require('chai').expect; +const glob = require('glob-promise'); +const { JSDOM } = require('jsdom'); +const path = require('path'); +const runSmokeTest = require('../../../../../test/smoke-test'); +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'Custom GraphQuery for Front Matter from GraphQL'; + const apolloStateRegex = /window.__APOLLO_STATE__ = true/; + let setup; + + before(async function() { + setup = new TestBed(); + + const greenwoodGraphqlCoreLibs = (await glob(`${process.cwd()}/packages/plugin-graphql/src/core/*.js`)).map((lib) => { + return { + dir: 'node_modules/@greenwood/plugin-graphql/src/core/', + name: path.basename(lib) + }; + }); + const greenwoodGraphqlQueryLibs = (await glob(`${process.cwd()}/packages/plugin-graphql/src/queries/*.gql`)).map((lib) => { + return { + dir: 'node_modules/@greenwood/plugin-graphql/src/queries/', + name: path.basename(lib) + }; + }); + const litElementLibs = (await glob(`${process.cwd()}/node_modules/lit-element/lib/*.js`)).map((lib) => { + return { + dir: 'node_modules/lit-element/lib/', + name: path.basename(lib) + }; + }); + const litHtmlLibs = (await glob(`${process.cwd()}/node_modules/lit-html/lib/*.js`)).map((lib) => { + return { + dir: 'node_modules/lit-html/lib/', + name: path.basename(lib) + }; + }); + + this.context = await setup.setupTestBed(__dirname, [ + ...greenwoodGraphqlCoreLibs, + ...greenwoodGraphqlQueryLibs, + { + // lit-element (+ lit-html) + dir: 'node_modules/lit-element/', + name: 'lit-element.js' + }, { + dir: 'node_modules/lit-element/', + name: 'package.json' + }, + + ...litElementLibs, + + { + dir: 'node_modules/lit-html/', + name: 'lit-html.js' + }, { + dir: 'node_modules/lit-html/', + name: 'package.json' + }, + + ...litHtmlLibs + ]); + }); + + describe(LABEL, function() { + + before(async function() { + await setup.runGreenwoodCommand('build'); + }); + + runSmokeTest(['public', 'index'], LABEL); + + describe('Home Page <posts-list> w/ custom Graph query', function() { + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', function() { + const scriptTags = dom.window.document.querySelectorAll('script'); + const apolloScriptTags = Array.prototype.slice.call(scriptTags).filter(script => { + return script.getAttribute('data-state') === 'apollo'; + }); + const innerHTML = apolloScriptTags[0].innerHTML; + + expect(apolloScriptTags.length).to.equal(1); + expect(innerHTML).to.match(apolloStateRegex); + }); + + it('should output a single (partial) *-cache.json file, one per each query made', async function() { + const cacheFiles = await glob.promise(path.join(this.context.publicDir, './*-cache.json')); + + expect(cacheFiles).to.have.lengthOf(1); + }); + + it('should output a (partial) *-cache.json files, one per each query made, that are all defined', async function() { + const cacheFiles = await glob.promise(path.join(this.context.publicDir, './*-cache.json')); + + cacheFiles.forEach(file => { + const cache = require(file); + + expect(cache).to.not.be.undefined; + }); + }); + + it('should have a <ul> in the <body>', function() { + const lists = dom.window.document.querySelectorAll('body ul'); + + expect(lists.length).to.be.equal(1); + }); + + it('should have a expected Query output in the <body> tag for posts list links', function() { + const listItems = dom.window.document.querySelectorAll('body ul li'); + const link1 = listItems[0].querySelector('a'); + const link2 = listItems[1].querySelector('a'); + + expect(listItems.length).to.be.equal(2); + + expect(link1.href.replace('file://', '')).to.be.equal('/blog/first-post/'); + expect(link1.title).to.be.equal('Click to read my First blog post'); + expect(link1.innerHTML).to.contain('First'); + + expect(link2.href.replace('file://', '')).to.be.equal('/blog/second-post/'); + expect(link2.title).to.be.equal('Click to read my Second blog post'); + expect(link2.innerHTML).to.contain('Second'); + }); + + it('should have a expected Query output in the <body> tag for posts list authors and dates from custom frontmatter', function() { + const authors = dom.window.document.querySelectorAll('body ul li span.author'); + const dates = dom.window.document.querySelectorAll('body ul li span.date'); + + expect(authors.length).to.be.equal(2); + expect(dates.length).to.be.equal(2); + + expect(authors[0].innerHTML).to.be.contain('Written By: someone@blog.com'); + expect(dates[0].innerHTML).to.be.contain('On: 07.08.2020'); + + expect(authors[1].innerHTML).to.be.contain('Written By: someone_else@blog.com'); + expect(dates[1].innerHTML).to.be.contain('On: 07.09.2020'); + }); + }); + + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-custom-frontmatter/src/components/posts-list.js b/packages/plugin-graphql/test/cases/query-custom-frontmatter/src/components/posts-list.js new file mode 100644 index 000000000..74af3dadb --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-custom-frontmatter/src/components/posts-list.js @@ -0,0 +1,66 @@ +import { html, LitElement } from 'lit-element'; +import client from '@greenwood/plugin-graphql/core/client'; + +class PostsListTemplate extends LitElement { + + static get properties() { + return { + posts: { + type: Array + } + }; + } + + constructor() { + super(); + this.posts = []; + } + + async connectedCallback() { + super.connectedCallback(); + const response = await client.query({ + query: ` + query { + graph { + route, + title, + data { + author, + date + } + } + } + ` + }); + + this.posts = response.data.graph + .filter(page => page.route.indexOf('/blog/') >= 0) + .filter(page => page.id !== 'index'); + } + + /* eslint-disable indent */ + render() { + const { posts } = this; + + return html` + <h1>My Posts</h1> + + <div class="posts"> + <ul> + ${posts.map((post) => { + return html` + <li> + <a href="${post.route}" title="Click to read my ${post.title} blog post">${post.title}</a> + <span class="author">Written By: ${post.data.author}</span> + <span class="date">On: ${post.data.date}</span> + </li> + `; + })} + </ul> + </div> + `; + } + /* eslint-enable */ +} + +customElements.define('posts-list', PostsListTemplate); \ No newline at end of file diff --git a/packages/cli/test/cases/build.data.graph/src/pages/blog/first-post/index.md b/packages/plugin-graphql/test/cases/query-custom-frontmatter/src/pages/blog/first-post/index.md similarity index 71% rename from packages/cli/test/cases/build.data.graph/src/pages/blog/first-post/index.md rename to packages/plugin-graphql/test/cases/query-custom-frontmatter/src/pages/blog/first-post/index.md index 259b5451c..1eaed8a19 100644 --- a/packages/cli/test/cases/build.data.graph/src/pages/blog/first-post/index.md +++ b/packages/plugin-graphql/test/cases/query-custom-frontmatter/src/pages/blog/first-post/index.md @@ -1,8 +1,7 @@ --- -template: 'post' title: 'First' -menu: navigation date: '07.08.2020' +author: 'someone@blog.com' --- ## My First Blog Post diff --git a/packages/cli/test/cases/build.data.graph/src/pages/blog/second-post/index.md b/packages/plugin-graphql/test/cases/query-custom-frontmatter/src/pages/blog/second-post/index.md similarity index 72% rename from packages/cli/test/cases/build.data.graph/src/pages/blog/second-post/index.md rename to packages/plugin-graphql/test/cases/query-custom-frontmatter/src/pages/blog/second-post/index.md index 6cb0aff73..73e20d0a9 100644 --- a/packages/cli/test/cases/build.data.graph/src/pages/blog/second-post/index.md +++ b/packages/plugin-graphql/test/cases/query-custom-frontmatter/src/pages/blog/second-post/index.md @@ -1,8 +1,7 @@ --- -template: 'post' title: 'Second' -menu: navigation date: '07.09.2020' +author: 'someone_else@blog.com' --- ## My Second Blog Post diff --git a/packages/plugin-graphql/test/cases/query-custom-frontmatter/src/pages/index.html b/packages/plugin-graphql/test/cases/query-custom-frontmatter/src/pages/index.html new file mode 100644 index 000000000..4400ac0eb --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-custom-frontmatter/src/pages/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/components/posts-list.js"></script> + </head> + + <body> + <posts-list></posts-list> + </body> + +</html> \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-graph/greenwood.config.js b/packages/plugin-graphql/test/cases/query-graph/greenwood.config.js new file mode 100644 index 000000000..8530fa0c6 --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-graph/greenwood.config.js @@ -0,0 +1,9 @@ +const pluginGraphQL = require('../../../src/index'); + +module.exports = { + + plugins: [ + ...pluginGraphQL() + ] + +}; \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-graph/package.json b/packages/plugin-graphql/test/cases/query-graph/package.json new file mode 100644 index 000000000..0acf9dd07 --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-graph/package.json @@ -0,0 +1,6 @@ +{ + "name": "plugin-graphql-test-graph-query", + "dependencies": { + "lit-element": "^2.4.0" + } +} \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-graph/query-graph.spec.js b/packages/plugin-graphql/test/cases/query-graph/query-graph.spec.js new file mode 100644 index 000000000..4cd4a32fa --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-graph/query-graph.spec.js @@ -0,0 +1,150 @@ +/* + * Use Case + * Run Greenwood build command with GraphQL calls to get data about the projects graph using GraphQuaery. + * + * User Result + * Should generate a Greenwood build that tests basic output from the GraphQuery. + * + * User Command + * greenwood build + * + * Default Config + * + * Custom Workspace + * src/ + * components/ + * debug-output.js + * pages/ + * blog/ + * first-post.md + * second-post.md + * index.html + */ +const expect = require('chai').expect; +const glob = require('glob-promise'); +const { JSDOM } = require('jsdom'); +const path = require('path'); +const runSmokeTest = require('../../../../../test/smoke-test'); +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'Graph from GraphQL'; + const apolloStateRegex = /window.__APOLLO_STATE__ = true/; + let setup; + + before(async function() { + setup = new TestBed(); + + const greenwoodGraphqlCoreLibs = (await glob(`${process.cwd()}/packages/plugin-graphql/src/core/*.js`)).map((lib) => { + return { + dir: 'node_modules/@greenwood/plugin-graphql/src/core/', + name: path.basename(lib) + }; + }); + const greenwoodGraphqlQueryLibs = (await glob(`${process.cwd()}/packages/plugin-graphql/src/queries/*.gql`)).map((lib) => { + return { + dir: 'node_modules/@greenwood/plugin-graphql/src/queries/', + name: path.basename(lib) + }; + }); + const litElementLibs = (await glob(`${process.cwd()}/node_modules/lit-element/lib/*.js`)).map((lib) => { + return { + dir: 'node_modules/lit-element/lib/', + name: path.basename(lib) + }; + }); + const litHtmlLibs = (await glob(`${process.cwd()}/node_modules/lit-html/lib/*.js`)).map((lib) => { + return { + dir: 'node_modules/lit-html/lib/', + name: path.basename(lib) + }; + }); + + this.context = await setup.setupTestBed(__dirname, [ + ...greenwoodGraphqlCoreLibs, + ...greenwoodGraphqlQueryLibs, + { + // lit-element (+ lit-html) + dir: 'node_modules/lit-element/', + name: 'lit-element.js' + }, { + dir: 'node_modules/lit-element/', + name: 'package.json' + }, + + ...litElementLibs, + + { + dir: 'node_modules/lit-html/', + name: 'lit-html.js' + }, { + dir: 'node_modules/lit-html/', + name: 'package.json' + }, + + ...litHtmlLibs + ]); + }); + + describe(LABEL, function() { + + before(async function() { + await setup.runGreenwoodCommand('build'); + }); + + runSmokeTest(['public', 'index'], LABEL); + + describe('Home Page output w/ GraphQuery', function() { + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', function() { + const scriptTags = dom.window.document.querySelectorAll('script'); + const apolloScriptTags = Array.prototype.slice.call(scriptTags).filter(script => { + return script.getAttribute('data-state') === 'apollo'; + }); + const innerHTML = apolloScriptTags[0].innerHTML; + + expect(apolloScriptTags.length).to.equal(1); + expect(innerHTML).to.match(apolloStateRegex); + }); + + it('should output a single (partial) *-cache.json file, one per each query made', async function() { + expect(await glob.promise(path.join(this.context.publicDir, './*-cache.json'))).to.have.lengthOf(1); + }); + + it('should output a (partial) *-cache.json files, one per each query made, that are all defined', async function() { + const cacheFiles = await glob.promise(path.join(this.context.publicDir, './*-cache.json')); + + cacheFiles.forEach(file => { + const cache = require(file); + + expect(cache).to.not.be.undefined; + }); + }); + + it('should have a <ul> in the <body>', function() { + const lists = dom.window.document.querySelectorAll('body ul'); + + expect(lists.length).to.be.equal(1); + }); + + it('should have a expected navigation output in the <header> based on pages with menu: navigation frontmatter', function() { + const listItems = dom.window.document.querySelectorAll('body ul li'); + + expect(listItems.length).to.be.equal(3); + + expect(listItems[0].innerHTML).to.be.contain('First Post'); + expect(listItems[1].innerHTML).to.be.contain('Second Post'); + expect(listItems[2].innerHTML).to.be.contain('Index'); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-graph/src/components/debug-output.js b/packages/plugin-graphql/test/cases/query-graph/src/components/debug-output.js new file mode 100644 index 000000000..df5ff8d6d --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-graph/src/components/debug-output.js @@ -0,0 +1,48 @@ +import { html, LitElement } from 'lit-element'; +import client from '@greenwood/plugin-graphql/core/client'; +import GraphQuery from '@greenwood/plugin-graphql/queries/graph'; + +class DebugOutputComponent extends LitElement { + + static get properties() { + return { + pages: Array + }; + } + + constructor() { + super(); + this.pages = []; + } + + async connectedCallback() { + super.connectedCallback(); + const response = await client.query({ + query: GraphQuery + }); + + this.pages = response.data.graph; + } + + /* eslint-disable indent */ + render() { + const { pages } = this; + + return html` + <h1>My Posts</h1> + + <div class="pages"> + <ul> + ${pages.map((page) => { + return html` + <li>${page.label}</li> + `; + })} + </ul> + </div> + `; + } + /* eslint-enable */ +} + +customElements.define('debug-output', DebugOutputComponent); \ No newline at end of file diff --git a/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/blog/first-post.md b/packages/plugin-graphql/test/cases/query-graph/src/pages/blog/first-post.md similarity index 100% rename from packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/blog/first-post.md rename to packages/plugin-graphql/test/cases/query-graph/src/pages/blog/first-post.md diff --git a/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/blog/second-post.md b/packages/plugin-graphql/test/cases/query-graph/src/pages/blog/second-post.md similarity index 100% rename from packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/blog/second-post.md rename to packages/plugin-graphql/test/cases/query-graph/src/pages/blog/second-post.md diff --git a/packages/plugin-graphql/test/cases/query-graph/src/pages/index.html b/packages/plugin-graphql/test/cases/query-graph/src/pages/index.html new file mode 100644 index 000000000..d1523ca91 --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-graph/src/pages/index.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/components/debug-output.js"></script> + </head> + + <body> + <h1>Home Page</h1> + <debug-output></debug-output> + </body> + +</html> \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-menu/greenwood.config.js b/packages/plugin-graphql/test/cases/query-menu/greenwood.config.js new file mode 100644 index 000000000..8530fa0c6 --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-menu/greenwood.config.js @@ -0,0 +1,9 @@ +const pluginGraphQL = require('../../../src/index'); + +module.exports = { + + plugins: [ + ...pluginGraphQL() + ] + +}; \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-menu/package.json b/packages/plugin-graphql/test/cases/query-menu/package.json new file mode 100644 index 000000000..eeef8448b --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-menu/package.json @@ -0,0 +1,6 @@ +{ + "name": "plugin-graphql-test-menu-query", + "dependencies": { + "lit-element": "^2.4.0" + } +} \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-menu/query-menu.spec.js b/packages/plugin-graphql/test/cases/query-menu/query-menu.spec.js new file mode 100644 index 000000000..c4934a86f --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-menu/query-menu.spec.js @@ -0,0 +1,159 @@ +/* + * Use Case + * Run Greenwood build command with GraphQL calls to get data about the projects graph using MenuQuery, simluting + * a site navigation based on top level page routes. Also uses LitElement. + * + * User Result + * Should generate a Greenwood build that dynamically serializes data from the graph from the header. + * + * User Command + * greenwood build + * + * Default Config (+ plugin-graphql) + * + * Custom Workspace + * src/ + * components/ + * header.js + * pages/ + * about.md + * contact.md + * index.md + * templates/ + * page.html + */ +const expect = require('chai').expect; +const glob = require('glob-promise'); +const { JSDOM } = require('jsdom'); +const path = require('path'); +const runSmokeTest = require('../../../../../test/smoke-test'); +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'MenuQuery from GraphQL'; + const apolloStateRegex = /window.__APOLLO_STATE__ = true/; + let setup; + + before(async function() { + setup = new TestBed(); + + const greenwoodGraphqlCoreLibs = (await glob(`${process.cwd()}/packages/plugin-graphql/src/core/*.js`)).map((lib) => { + return { + dir: 'node_modules/@greenwood/plugin-graphql/src/core/', + name: path.basename(lib) + }; + }); + const greenwoodGraphqlQueryLibs = (await glob(`${process.cwd()}/packages/plugin-graphql/src/queries/*.gql`)).map((lib) => { + return { + dir: 'node_modules/@greenwood/plugin-graphql/src/queries/', + name: path.basename(lib) + }; + }); + const litElementLibs = (await glob(`${process.cwd()}/node_modules/lit-element/lib/*.js`)).map((lib) => { + return { + dir: 'node_modules/lit-element/lib/', + name: path.basename(lib) + }; + }); + const litHtmlLibs = (await glob(`${process.cwd()}/node_modules/lit-html/lib/*.js`)).map((lib) => { + return { + dir: 'node_modules/lit-html/lib/', + name: path.basename(lib) + }; + }); + + this.context = await setup.setupTestBed(__dirname, [ + ...greenwoodGraphqlCoreLibs, + ...greenwoodGraphqlQueryLibs, + { + // lit-element (+ lit-html) + dir: 'node_modules/lit-element/', + name: 'lit-element.js' + }, { + dir: 'node_modules/lit-element/', + name: 'package.json' + }, + + ...litElementLibs, + + { + dir: 'node_modules/lit-html/', + name: 'lit-html.js' + }, { + dir: 'node_modules/lit-html/', + name: 'package.json' + }, + + ...litHtmlLibs + ]); + }); + + describe(LABEL, function() { + + before(async function() { + await setup.runGreenwoodCommand('build'); + }); + + runSmokeTest(['public', 'index'], LABEL); + + describe('Home Page navigation w/ MenuQuery', function() { + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', function() { + const scriptTags = dom.window.document.querySelectorAll('script'); + const apolloScriptTags = Array.prototype.slice.call(scriptTags).filter(script => { + return script.getAttribute('data-state') === 'apollo'; + }); + const innerHTML = apolloScriptTags[0].innerHTML; + + expect(apolloScriptTags.length).to.equal(1); + expect(innerHTML).to.match(apolloStateRegex); + }); + + it('should output a single (partial) *-cache.json file, one per each query made', async function() { + expect(await glob.promise(path.join(this.context.publicDir, './*-cache.json'))).to.have.lengthOf(1); + }); + + it('should output a (partial) *-cache.json files, one per each query made, that are all defined', async function() { + const cacheFiles = await glob.promise(path.join(this.context.publicDir, './*-cache.json')); + + cacheFiles.forEach(file => { + const cache = require(file); + + expect(cache).to.not.be.undefined; + }); + }); + + it('should have a <header> in the <body>', function() { + const headers = dom.window.document.querySelectorAll('body header'); + + expect(headers.length).to.be.equal(1); + }); + + it('should have a expected navigation output in the <header> based on pages with menu: navigation frontmatter', function() { + const listItems = dom.window.document.querySelectorAll('body header ul li'); + const link1 = listItems[0].querySelector('a'); + const link2 = listItems[1].querySelector('a'); + + expect(listItems.length).to.be.equal(2); + + expect(link1.href.replace('file://', '')).to.be.equal('/about/'); + expect(link1.title).to.be.equal('Click to visit the About page'); + expect(link1.innerHTML).to.contain('About'); + + expect(link2.href.replace('file://', '')).to.be.equal('/contact/'); + expect(link2.title).to.be.equal('Click to visit the Contact page'); + expect(link2.innerHTML).to.contain('Contact'); + }); + }); + + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.data.graph/src/components/header.js b/packages/plugin-graphql/test/cases/query-menu/src/components/header.js similarity index 71% rename from packages/cli/test/cases/build.data.graph/src/components/header.js rename to packages/plugin-graphql/test/cases/query-menu/src/components/header.js index 1c189a635..e7d516445 100644 --- a/packages/cli/test/cases/build.data.graph/src/components/header.js +++ b/packages/plugin-graphql/test/cases/query-menu/src/components/header.js @@ -1,6 +1,6 @@ import { LitElement, html } from 'lit-element'; -import client from '@greenwood/cli/data/client'; -import MenuQuery from '@greenwood/cli/data/queries/menu'; +import client from '@greenwood/plugin-graphql/core/client'; +import MenuQuery from '@greenwood/plugin-graphql/queries/menu'; class HeaderComponent extends LitElement { @@ -27,7 +27,7 @@ class HeaderComponent extends LitElement { } }); - this.navigation = response.data.menu.children; + this.navigation = response.data.menu.children.map(item => item.item); } /* eslint-disable indent */ @@ -39,10 +39,10 @@ class HeaderComponent extends LitElement { <nav> <ul> - ${navigation.map(({ item }) => { + ${navigation.map((item) => { return html` <li> - <a href="${item.link}" title="Click to visit the ${item.label} blog post">${item.label}</a> + <a href="${item.route}" title="Click to visit the ${item.label} page">${item.label}</a> </li> `; })} diff --git a/packages/plugin-graphql/test/cases/query-menu/src/pages/about.md b/packages/plugin-graphql/test/cases/query-menu/src/pages/about.md new file mode 100644 index 000000000..116be1c43 --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-menu/src/pages/about.md @@ -0,0 +1,7 @@ +--- +menu: navigation +--- + +## About Page + +A little something about us. \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-menu/src/pages/contact.md b/packages/plugin-graphql/test/cases/query-menu/src/pages/contact.md new file mode 100644 index 000000000..1c18cef3c --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-menu/src/pages/contact.md @@ -0,0 +1,7 @@ +--- +menu: navigation +--- + +## Contact Page + +Looking forward to hearing from you! \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-menu/src/pages/index.md b/packages/plugin-graphql/test/cases/query-menu/src/pages/index.md new file mode 100644 index 000000000..af0d6db0b --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-menu/src/pages/index.md @@ -0,0 +1,3 @@ +## Home Page + +This our home page, thanks for stopping by! \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-menu/src/templates/page.html b/packages/plugin-graphql/test/cases/query-menu/src/templates/page.html new file mode 100644 index 000000000..a05d2c2c3 --- /dev/null +++ b/packages/plugin-graphql/test/cases/query-menu/src/templates/page.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="/components/header.js"></script> + </head> + + <body> + <app-header></app-header> + </body> + +</html> \ No newline at end of file diff --git a/packages/cli/test/unit/data/common.spec.js b/packages/plugin-graphql/test/unit/common.spec.js similarity index 71% rename from packages/cli/test/unit/data/common.spec.js rename to packages/plugin-graphql/test/unit/common.spec.js index c8d7e836f..8a09b6d1d 100644 --- a/packages/cli/test/unit/data/common.spec.js +++ b/packages/plugin-graphql/test/unit/common.spec.js @@ -1,6 +1,5 @@ const expect = require('chai').expect; -const { gql } = require('apollo-server'); -const { getQueryHash } = require('../../../src/data/common'); +const { getQueryHash } = require('../../src/core/common.server'); describe('Unit Test: Data', function() { @@ -10,14 +9,14 @@ describe('Unit Test: Data', function() { it('should return the expected hash for a standard graph query', function () { // __typename is added by server.js - const query = gql` + const query = ` query { graph { id, title, - link, - filePath, - fileName, + route, + path, + filename, template, __typename } @@ -25,15 +24,15 @@ describe('Unit Test: Data', function() { `; const hash = getQueryHash(query); - expect(hash).to.be.equal('876029931'); + expect(hash).to.be.equal('380713565'); }); it('should return the expected hash for a custom graph query with custom data', function () { - const query = gql` + const query = ` query { graph { title, - link, + route, data { date, image @@ -43,18 +42,18 @@ describe('Unit Test: Data', function() { `; const hash = getQueryHash(query); - expect(hash).to.be.equal('1656784831'); + expect(hash).to.be.equal('1136154652'); }); it('should return the expected hash for a children query with a variable', function () { - const query = gql` + const query = ` query($parent: String!) { children(parent: $parent) { id, title, - link, - filePath, - fileName, + route, + path, + filename, template } } @@ -63,7 +62,7 @@ describe('Unit Test: Data', function() { parent: '/docs/' }); - expect(hash).to.be.equal('1366211136'); + expect(hash).to.be.equal('1696894039'); }); }); diff --git a/packages/cli/test/unit/data/mocks/config.js b/packages/plugin-graphql/test/unit/mocks/config.js similarity index 78% rename from packages/cli/test/unit/data/mocks/config.js rename to packages/plugin-graphql/test/unit/mocks/config.js index c8efdbfae..891467990 100644 --- a/packages/cli/test/unit/data/mocks/config.js +++ b/packages/plugin-graphql/test/unit/mocks/config.js @@ -1,14 +1,12 @@ const MOCK_CONFIG = { config: { devServer: { - port: 1984, - host: 'localhost' + port: 1984 }, meta: [ { name: 'twitter:site', content: '@PrjEvergreen' }, { rel: 'icon', href: '/assets/favicon.ico' } ], - publicPath: '/some-dir', title: 'My App', workspace: 'src' } diff --git a/packages/plugin-graphql/test/unit/mocks/graph.js b/packages/plugin-graphql/test/unit/mocks/graph.js new file mode 100644 index 000000000..8c6b800ac --- /dev/null +++ b/packages/plugin-graphql/test/unit/mocks/graph.js @@ -0,0 +1,2274 @@ +/* eslint-disable */ +module.exports = { + graph: [ + { + data: { + menu: "", + index: "", + linkheadings: 0, + tableOfContents: [], + }, + filename: "./index.md", + id: "index", + label: "Index", + route: "/", + template: "home", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/index.md", + title: "", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 3, + linkheadings: 0, + tableOfContents: [], + }, + filename: "./about/community.md", + id: "community", + label: "Community", + route: "/about/community", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/about/community.md", + title: "Community", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 2, + linkheadings: 0, + tableOfContents: [], + }, + filename: "./about/features.md", + id: "features", + label: "Features", + route: "/about/features", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/about/features.md", + title: "Features", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: "", + linkheadings: 0, + tableOfContents: [], + }, + filename: "./about/goals.md", + id: "Goals", + label: "Goals", + route: "/about/goals", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/about/goals.md", + title: "Goals", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 1, + linkheadings: 3, + tableOfContents: [ + { + content: "CLI", + slug: "cli", + lvl: 3, + i: 1, + seen: 0, + }, + { + content: "Evergreen Build", + slug: "evergreen-build", + lvl: 3, + i: 2, + seen: 0, + }, + { + content: "Browser Support", + slug: "browser-support", + lvl: 3, + i: 3, + seen: 0, + }, + { + content: "Polyfills", + slug: "polyfills", + lvl: 3, + i: 4, + seen: 0, + }, + ], + }, + filename: "./about/how-it-works.md", + id: "how-it-works", + label: "How It Works", + route: "/about/how-it-works", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/about/how-it-works.md", + title: "How It Works", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "navigation", + index: "", + linkheadings: 0, + tableOfContents: [], + }, + filename: "./about/index.md", + id: "about", + label: "About", + route: "/about/", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/about/index.md", + title: "About", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 1, + linkheadings: 0, + tableOfContents: [], + }, + filename: "./docs/component-model.md", + id: "component-model", + label: "Component Model", + route: "/docs/component-model", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/component-model.md", + title: "Component Model", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 2, + linkheadings: 3, + tableOfContents: [ + { + content: "Dev Server", + slug: "dev-server", + lvl: 3, + i: 1, + seen: 0, + }, + { + content: "Example", + slug: "example", + lvl: 4, + i: 2, + seen: 0, + }, + { + content: "Meta", + slug: "meta", + lvl: 3, + i: 3, + seen: 0, + }, + { + content: "Example", + slug: "example-1", + lvl: 4, + i: 4, + seen: 1, + }, + { + content: "Public Path", + slug: "public-path", + lvl: 3, + i: 5, + seen: 0, + }, + { + content: "Example", + slug: "example-2", + lvl: 4, + i: 6, + seen: 2, + }, + { + content: "Title", + slug: "title", + lvl: 3, + i: 7, + seen: 0, + }, + { + content: "Example", + slug: "example-3", + lvl: 4, + i: 8, + seen: 3, + }, + { + content: "Workspace", + slug: "workspace", + lvl: 3, + i: 9, + seen: 0, + }, + { + content: "Example", + slug: "example-4", + lvl: 4, + i: 10, + seen: 4, + }, + ], + }, + filename: "./docs/configuration.md", + id: "configuration", + label: "Configuration", + route: "/docs/configuration", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/configuration.md", + title: "Configuration", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 5, + linkheadings: 3, + tableOfContents: [ + { + content: "Theming", + slug: "theming", + lvl: 3, + i: 1, + seen: 0, + }, + { + content: "Example", + slug: "example", + lvl: 4, + i: 2, + seen: 0, + }, + { + content: "Shadow DOM", + slug: "shadow-dom", + lvl: 3, + i: 3, + seen: 0, + }, + { + content: "Example", + slug: "example-1", + lvl: 4, + i: 4, + seen: 1, + }, + { + content: "Assets and Images", + slug: "assets-and-images", + lvl: 3, + i: 5, + seen: 0, + }, + { + content: "Example", + slug: "example-2", + lvl: 4, + i: 6, + seen: 2, + }, + ], + }, + filename: "./docs/css-and-images.md", + id: "css-and-images", + label: "CSS and Images", + route: "/docs/css-and-images", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/css-and-images.md", + title: "Styles and Assets", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 7, + linkheadings: 3, + tableOfContents: [ + { + content: "Internal Sources", + slug: "internal-sources", + lvl: 3, + i: 1, + seen: 0, + }, + { + content: "Schema", + slug: "schema", + lvl: 4, + i: 2, + seen: 0, + }, + { + content: "Queries", + slug: "queries", + lvl: 4, + i: 3, + seen: 0, + }, + { + content: "Graph", + slug: "graph", + lvl: 5, + i: 4, + seen: 0, + }, + { + content: "Definition", + slug: "definition", + lvl: 6, + i: 5, + seen: 0, + }, + { + content: "Usage", + slug: "usage", + lvl: 6, + i: 6, + seen: 0, + }, + { + content: "Response", + slug: "response", + lvl: 6, + i: 7, + seen: 0, + }, + { + content: "Menu Query", + slug: "menu-query", + lvl: 5, + i: 8, + seen: 0, + }, + { + content: "Children", + slug: "children", + lvl: 5, + i: 9, + seen: 0, + }, + { + content: "Definition", + slug: "definition-1", + lvl: 6, + i: 10, + seen: 1, + }, + { + content: "Usage", + slug: "usage-1", + lvl: 6, + i: 11, + seen: 1, + }, + { + content: "Response", + slug: "response-1", + lvl: 6, + i: 12, + seen: 1, + }, + { + content: "Config", + slug: "config", + lvl: 5, + i: 13, + seen: 0, + }, + { + content: "Definition", + slug: "definition-2", + lvl: 6, + i: 14, + seen: 2, + }, + { + content: "Usage", + slug: "usage-2", + lvl: 6, + i: 15, + seen: 2, + }, + { + content: "Response", + slug: "response-2", + lvl: 6, + i: 16, + seen: 2, + }, + { + content: "Custom", + slug: "custom", + lvl: 5, + i: 17, + seen: 0, + }, + { + content: "example:", + slug: "example", + lvl: 6, + i: 18, + seen: 0, + }, + { + content: "Complete Example", + slug: "complete-example", + lvl: 5, + i: 19, + seen: 0, + }, + { + content: "External Sources", + slug: "external-sources", + lvl: 4, + i: 20, + seen: 0, + }, + ], + }, + filename: "./docs/data.md", + id: "data-sources", + label: "Data Sources", + route: "/docs/data", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/data.md", + fileName: "data", + relativeExpectedPath: "'../docs/data/data.js'", + title: "Data Sources", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 3, + linkheadings: 3, + tableOfContents: [ + { + content: "Element Label", + slug: "element-label", + lvl: 3, + i: 1, + seen: 0, + }, + { + content: "Example", + slug: "example", + lvl: 4, + i: 2, + seen: 0, + }, + { + content: "Imports", + slug: "imports", + lvl: 3, + i: 3, + seen: 0, + }, + { + content: "Example", + slug: "example-1", + lvl: 4, + i: 4, + seen: 1, + }, + { + content: "Template", + slug: "template", + lvl: 3, + i: 5, + seen: 0, + }, + { + content: "Example", + slug: "example-2", + lvl: 4, + i: 6, + seen: 2, + }, + { + content: "Title", + slug: "title", + lvl: 3, + i: 7, + seen: 0, + }, + { + content: "Example", + slug: "example-3", + lvl: 4, + i: 8, + seen: 3, + }, + { + content: "Custom Data", + slug: "custom-data", + lvl: 3, + i: 9, + seen: 0, + }, + { + content: "Example", + slug: "example-4", + lvl: 4, + i: 10, + seen: 4, + }, + ], + }, + filename: "./docs/front-matter.md", + id: "front-matter", + label: "Front Matter", + route: "/docs/front-matter", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/front-matter.md", + title: "Front Matter", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "navigation", + index: "", + linkheadings: 0, + tableOfContents: [], + }, + filename: "./docs/index.md", + id: "docs", + label: "Docs", + route: "/docs/", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/index.md", + title: "Docs", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 6, + linkheadings: 3, + tableOfContents: [ + { + content: "Page Template", + slug: "page-template", + lvl: 3, + i: 1, + seen: 0, + }, + { + content: "Template Hooks", + slug: "template-hooks", + lvl: 4, + i: 2, + seen: 0, + }, + { + content: "App Template", + slug: "app-template", + lvl: 3, + i: 3, + seen: 0, + }, + { + content: "Pages", + slug: "pages", + lvl: 3, + i: 4, + seen: 0, + }, + ], + }, + filename: "./docs/layouts.md", + id: "templates", + label: "Templates", + route: "/docs/layouts", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/layouts.md", + title: "Templates and Pages", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 4, + linkheadings: 3, + tableOfContents: [ + { + content: "Syntax Highlighting", + slug: "syntax-highlighting", + lvl: 3, + i: 1, + seen: 0, + }, + { + content: "Imports", + slug: "imports", + lvl: 3, + i: 2, + seen: 0, + }, + { + content: "Example", + slug: "example", + lvl: 4, + i: 3, + seen: 0, + }, + ], + }, + filename: "./docs/markdown.md", + id: "markdown", + label: "Markdown", + route: "/docs/markdown", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/markdown.md", + title: "Markdown", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 5, + linkheadings: 3, + tableOfContents: [ + { + content: "Declare Menu", + slug: "declare-menu", + lvl: 3, + i: 1, + seen: 0, + }, + { + content: "Retrieve Menu", + slug: "retrieve-menu", + lvl: 3, + i: 2, + seen: 0, + }, + { + content: "Sorting", + slug: "sorting", + lvl: 3, + i: 3, + seen: 0, + }, + { + content: "Filtering By Path", + slug: "filtering-by-path", + lvl: 3, + i: 4, + seen: 0, + }, + ], + }, + filename: "./docs/menus.md", + id: "menus", + label: "Menus", + route: "/docs/menus", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/menus.md", + title: "Menus", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 8, + linkheadings: 3, + tableOfContents: [ + { + content: "NodeJS", + slug: "nodejs", + lvl: 3, + i: 1, + seen: 0, + }, + { + content: "Web Components", + slug: "web-components", + lvl: 3, + i: 2, + seen: 0, + }, + { + content: "Webpack", + slug: "webpack", + lvl: 3, + i: 3, + seen: 0, + }, + { + content: "Development", + slug: "development", + lvl: 3, + i: 4, + seen: 0, + }, + ], + }, + filename: "./docs/tech-stack.md", + id: "tech-stack", + label: "Tech Stack", + route: "/docs/tech-stack", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/docs/tech-stack.md", + title: "Tech Stack", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 5, + linkheadings: 3, + tableOfContents: [ + { + content: "Web Components", + slug: "web-components", + lvl: 3, + i: 1, + seen: 0, + }, + { + content: "CSS", + slug: "css", + lvl: 3, + i: 2, + seen: 0, + }, + ], + }, + filename: "./getting-started/branding.md", + id: "branding", + label: "Branding", + route: "/getting-started/branding", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/branding.md", + title: "Styles and Web Components", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 6, + linkheadings: 0, + tableOfContents: [], + }, + filename: "./getting-started/build-and-deploy.md", + id: "build-and-deploy", + label: "Build And Deploy", + route: "/getting-started/build-and-deploy", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/build-and-deploy.md", + title: "Build and Deploy", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 4, + linkheadings: 3, + tableOfContents: [ + { + content: "Objectives", + slug: "objectives", + lvl: 3, + i: 1, + seen: 0, + }, + { + content: "Home Page Template", + slug: "home-page-template", + lvl: 3, + i: 2, + seen: 0, + }, + { + content: "Blog Posts Template", + slug: "blog-posts-template", + lvl: 3, + i: 3, + seen: 0, + }, + { + content: "Creating Pages", + slug: "creating-pages", + lvl: 3, + i: 4, + seen: 0, + }, + { + content: "Development Server", + slug: "development-server", + lvl: 3, + i: 5, + seen: 0, + }, + ], + }, + filename: "./getting-started/creating-content.md", + id: "creating-content", + label: "Creating Content", + route: "/getting-started/creating-content", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/creating-content.md", + title: "Creating Content", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "navigation", + index: "", + linkheadings: 0, + tableOfContents: [], + }, + filename: "./getting-started/index.md", + id: "getting-started", + label: "Getting Started", + route: "/getting-started/", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/index.md", + title: "Getting Started", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 2, + linkheadings: 3, + tableOfContents: [ + { + content: "Workspace", + slug: "workspace", + lvl: 3, + i: 1, + seen: 0, + }, + { + content: "Templates", + slug: "templates", + lvl: 3, + i: 2, + seen: 0, + }, + { + content: "Pages", + slug: "pages", + lvl: 3, + i: 3, + seen: 0, + }, + ], + }, + filename: "./getting-started/key-concepts.md", + id: "key-concepts", + label: "Key Concepts", + route: "/getting-started/key-concepts", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/key-concepts.md", + title: "Key Concepts", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 7, + linkheadings: 0, + tableOfContents: [], + }, + filename: "./getting-started/next-steps.md", + id: "next-steps", + label: "Next Steps", + route: "/getting-started/next-steps", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/next-steps.md", + title: "Next Steps", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 2, + linkheadings: 3, + tableOfContents: [ + { + content: "Installing Greenwood", + slug: "installing-greenwood", + lvl: 3, + i: 1, + seen: 0, + }, + { + content: "Configuring Workflows", + slug: "configuring-workflows", + lvl: 3, + i: 2, + seen: 0, + }, + { + content: "Project Structure", + slug: "project-structure", + lvl: 3, + i: 3, + seen: 0, + }, + ], + }, + filename: "./getting-started/project-setup.md", + id: "project-setup", + label: "Project Setup", + route: "/getting-started/project-setup", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/project-setup.md", + title: "Project Setup", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 1, + linkheadings: 0, + tableOfContents: [], + }, + filename: "./getting-started/quick-start.md", + id: "quick-start", + label: "Quick Start", + route: "/getting-started/quick-start", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/getting-started/quick-start.md", + title: "Quick Start", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 1, + linkheadings: 0, + tableOfContents: [], + }, + filename: "./plugins/composite-plugins.md", + id: "composite-plugins", + label: "Composite Plugins", + route: "/plugins/composite-plugins", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/plugins/composite-plugins.md", + title: "Composite Plugins", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 2, + linkheadings: 0, + tableOfContents: [], + }, + filename: "./plugins/index-hooks.md", + id: "index-hooks", + label: "Index Hooks", + route: "/plugins/index-hooks", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/plugins/index-hooks.md", + title: "Index Hooks", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "navigation", + index: "", + linkheadings: 0, + tableOfContents: [], + }, + filename: "./plugins/index.md", + id: "plugins", + label: "Plugins", + route: "/plugins/", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/plugins/index.md", + title: "Plugins", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + { + data: { + menu: "side", + index: 3, + linkheadings: 0, + tableOfContents: [], + }, + filename: "./plugins/webpack.md", + id: "webpack", + label: "Webpack", + route: "/plugins/webpack", + template: "page", + path: + "/media/skynet/DATA/workspace/evergreen/greenwood/www/pages/plugins/webpack.md", + title: "Webpack Plugins", + meta: [ + { + name: "description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + name: "twitter:site", + content: "@PrjEvergreen", + }, + { + property: "og:title", + content: "Greenwood", + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:url", + content: "https://www.greenwoodjs.io", + }, + { + property: "og:image", + content: + "https://s3.amazonaws.com/hosted.greenwoodjs.io/greenwood-logo.png", + }, + { + property: "og:description", + content: + "A modern and performant static site generator supporting Web Component based development", + }, + { + rel: "shortcut icon", + href: "/assets/favicon.ico", + }, + { + rel: "icon", + href: "/assets/favicon.ico", + }, + { + name: "google-site-verification", + content: "4rYd8k5aFD0jDnN0CCFgUXNe4eakLP4NnA18mNnK5P0", + }, + ], + }, + ], +}; diff --git a/packages/cli/test/unit/data/schema/config.spec.js b/packages/plugin-graphql/test/unit/schema/config.spec.js similarity index 65% rename from packages/cli/test/unit/data/schema/config.spec.js rename to packages/plugin-graphql/test/unit/schema/config.spec.js index b8959e94c..811c67766 100644 --- a/packages/cli/test/unit/data/schema/config.spec.js +++ b/packages/plugin-graphql/test/unit/schema/config.spec.js @@ -1,6 +1,6 @@ const expect = require('chai').expect; const MOCK_CONFIG = require('../mocks/config'); -const { configResolvers } = require('../../../../src/data/schema/config'); +const { configResolvers } = require('../../../src/schema/config'); describe('Unit Test: Data', function() { @@ -19,10 +19,6 @@ describe('Unit Test: Data', function() { it('should have the expected devServer.port', function() { expect(config.devServer.port).to.equal(devServer.port); }); - - it('should have the expected devServer.host', function() { - expect(config.devServer.host).to.equal(devServer.host); - }); }); describe('Meta', function() { @@ -33,40 +29,28 @@ describe('Unit Test: Data', function() { expect(nameMeta.name).to.equal(meta[0].name); }); - - it('should have the expected devServer.host', function() { - const nameMeta = config.meta[0]; - - expect(nameMeta.content).to.equal(meta[0].content); - }); it('should have the expected rel meta in the second index', function() { const relMeta = config.meta[1]; expect(relMeta.rel).to.equal(meta[1].rel); }); - - it('should have the expected devServer.host', function() { - const relMeta = config.meta[1]; - - expect(relMeta.content).to.equal(meta[1].content); - }); }); describe('Mode', function() { - it('should have a default optimization setting of spa', function() { - expect(config.optimization).to.equal(MOCK_CONFIG.config.optimization); + it('should have a default mode setting of mode', function() { + expect(config.mode).to.equal(MOCK_CONFIG.config.mode); }); }); - describe('Public Path', function() { - const { publicPath } = MOCK_CONFIG.config; + describe('Optimization', function() { - it('should have the expected publicPath', function() { - expect(publicPath).to.equal(config.publicPath); + it('should have a default optimization setting of default', function() { + expect(config.optimization).to.equal(MOCK_CONFIG.config.optimization); }); + }); describe('Title', function() { diff --git a/packages/cli/test/unit/data/schema/graph.menu.spec.js b/packages/plugin-graphql/test/unit/schema/graph.menu.spec.js similarity index 72% rename from packages/cli/test/unit/data/schema/graph.menu.spec.js rename to packages/plugin-graphql/test/unit/schema/graph.menu.spec.js index efca9f2c8..5230c658c 100644 --- a/packages/cli/test/unit/data/schema/graph.menu.spec.js +++ b/packages/plugin-graphql/test/unit/schema/graph.menu.spec.js @@ -1,6 +1,6 @@ const expect = require('chai').expect; const MOCK_GRAPH = require('../mocks/graph'); -const { graphResolvers } = require('../../../../src/data/schema/graph'); +const { graphResolvers } = require('../../../src/schema/graph'); describe('Unit Test: Data', function() { @@ -28,28 +28,28 @@ describe('Unit Test: Data', function() { const item = navigation.children[0].item; expect(item.label).to.be.equal('About'); - expect(item.link).to.be.equal('/about/'); + expect(item.route).to.be.equal('/about/'); }); it('should have Docs as the second item', function() { const item = navigation.children[1].item; expect(item.label).to.be.equal('Docs'); - expect(item.link).to.be.equal('/docs/'); + expect(item.route).to.be.equal('/docs/'); }); it('should have Getting Started as the third item', function() { const item = navigation.children[2].item; expect(item.label).to.be.equal('Getting Started'); - expect(item.link).to.be.equal('/getting-started/'); + expect(item.route).to.be.equal('/getting-started/'); }); it('should have Plugins as the fourth item', function() { const item = navigation.children[3].item; expect(item.label).to.be.equal('Plugins'); - expect(item.link).to.be.equal('/plugins/'); + expect(item.route).to.be.equal('/plugins/'); }); }); }); @@ -73,26 +73,26 @@ describe('Unit Test: Data', function() { it('should be labeled and linked to Styles and Web Components', function() { const item = shelf.children[0].item; - expect(item.label).to.be.equal('Styles and Web Components'); - expect(item.link).to.be.equal('/getting-started/branding'); + expect(item.label).to.be.equal('Branding'); + expect(item.route).to.be.equal('/getting-started/branding'); }); it('should have the correct sub items', function() { const subitem = shelf.children[0].children; expect(subitem[0].item.label).to.be.equal('Web Components'); - expect(subitem[0].item.link).to.be.equal('#web-components'); + expect(subitem[0].item.route).to.be.equal('#web-components'); expect(subitem[1].item.label).to.be.equal('CSS'); - expect(subitem[1].item.link).to.be.equal('#css'); + expect(subitem[1].item.route).to.be.equal('#css'); }); }); describe('the second item:', function() { - it('should be labeled and linked to Build and Deploy', function() { + it('should be labeled and linked to Build And Deploy', function() { const item = shelf.children[1].item; - expect(item.label).to.be.equal('Build and Deploy'); - expect(item.link).to.be.equal('/getting-started/build-and-deploy'); + expect(item.label).to.be.equal('Build And Deploy'); + expect(item.route).to.be.equal('/getting-started/build-and-deploy'); }); }); @@ -101,7 +101,7 @@ describe('Unit Test: Data', function() { const item = shelf.children[2].item; expect(item.label).to.be.equal('Creating Content'); - expect(item.link).to.be.equal('/getting-started/creating-content'); + expect(item.route).to.be.equal('/getting-started/creating-content'); }); it('should have the correct sub items', function() { @@ -135,7 +135,7 @@ describe('Unit Test: Data', function() { const item = shelf.children[0].item; expect(item.label).to.be.equal('Quick Start'); - expect(item.link).to.be.equal('/getting-started/quick-start'); + expect(item.route).to.be.equal('/getting-started/quick-start'); }); }); @@ -144,18 +144,18 @@ describe('Unit Test: Data', function() { const item = shelf.children[1].item; expect(item.label).to.be.equal('Key Concepts'); - expect(item.link).to.be.equal('/getting-started/key-concepts'); + expect(item.route).to.be.equal('/getting-started/key-concepts'); }); it('should have the correct sub items', function() { const subitem = shelf.children[1].children; expect(subitem[0].item.label).to.be.equal('Workspace'); - expect(subitem[0].item.link).to.be.equal('#workspace'); + expect(subitem[0].item.route).to.be.equal('#workspace'); expect(subitem[1].item.label).to.be.equal('Templates'); - expect(subitem[1].item.link).to.be.equal('#templates'); + expect(subitem[1].item.route).to.be.equal('#templates'); expect(subitem[2].item.label).to.be.equal('Pages'); - expect(subitem[2].item.link).to.be.equal('#pages'); + expect(subitem[2].item.route).to.be.equal('#pages'); }); }); @@ -164,18 +164,18 @@ describe('Unit Test: Data', function() { const item = shelf.children[2].item; expect(item.label).to.be.equal('Project Setup'); - expect(item.link).to.be.equal('/getting-started/project-setup'); + expect(item.route).to.be.equal('/getting-started/project-setup'); }); it('should have the correct sub items', function() { const subitem = shelf.children[2].children; expect(subitem[0].item.label).to.be.equal('Installing Greenwood'); - expect(subitem[0].item.link).to.be.equal('#installing-greenwood'); + expect(subitem[0].item.route).to.be.equal('#installing-greenwood'); expect(subitem[1].item.label).to.be.equal('Configuring Workflows'); - expect(subitem[1].item.link).to.be.equal('#configuring-workflows'); + expect(subitem[1].item.route).to.be.equal('#configuring-workflows'); expect(subitem[2].item.label).to.be.equal('Project Structure'); - expect(subitem[2].item.link).to.be.equal('#project-structure'); + expect(subitem[2].item.route).to.be.equal('#project-structure'); }); }); }); @@ -200,16 +200,16 @@ describe('Unit Test: Data', function() { const item = shelf.children[0].item; expect(item.label).to.be.equal('Next Steps'); - expect(item.link).to.be.equal('/getting-started/next-steps'); + expect(item.route).to.be.equal('/getting-started/next-steps'); }); }); describe('the second item:', function() { - it('should be labeled and linked to Build and Deploy', function() { + it('should be labeled and linked to Build And Deploy', function() { const item = shelf.children[1].item; - expect(item.label).to.be.equal('Build and Deploy'); - expect(item.link).to.be.equal('/getting-started/build-and-deploy'); + expect(item.label).to.be.equal('Build And Deploy'); + expect(item.route).to.be.equal('/getting-started/build-and-deploy'); }); }); @@ -217,17 +217,17 @@ describe('Unit Test: Data', function() { it('should be labeled and linked to Styles and Web Components', function() { const item = shelf.children[2].item; - expect(item.label).to.be.equal('Styles and Web Components'); - expect(item.link).to.be.equal('/getting-started/branding'); + expect(item.label).to.be.equal('Branding'); + expect(item.route).to.be.equal('/getting-started/branding'); }); it('should have the correct sub items', function() { const subitem = shelf.children[2].children; expect(subitem[0].item.label).to.be.equal('Web Components'); - expect(subitem[0].item.link).to.be.equal('#web-components'); + expect(subitem[0].item.route).to.be.equal('#web-components'); expect(subitem[1].item.label).to.be.equal('CSS'); - expect(subitem[1].item.link).to.be.equal('#css'); + expect(subitem[1].item.route).to.be.equal('#css'); }); }); }); @@ -248,40 +248,49 @@ describe('Unit Test: Data', function() { }); describe('the first item:', function() { - it('should be labeled and linked to Build and Deploy', function() { + it('should be labeled and linked to Branding', function() { const item = shelf.children[0].item; - expect(item.label).to.be.equal('Build and Deploy'); - expect(item.link).to.be.equal('/getting-started/build-and-deploy'); + expect(item.label).to.be.equal('Branding'); + expect(item.route).to.be.equal('/getting-started/branding'); }); }); describe('the second item:', function() { - it('should be labeled and linked to Creating Content', function() { + it('should be labeled and linked to Build And Deploy', function() { const item = shelf.children[1].item; - expect(item.label).to.be.equal('Creating Content'); - expect(item.link).to.be.equal('/getting-started/creating-content'); + expect(item.label).to.be.equal('Build And Deploy'); + expect(item.route).to.be.equal('/getting-started/build-and-deploy'); }); }); describe('the third item:', function() { - it('should be labeled and linked to Key Concepts', function() { + it('should be labeled and linked to Creating Content', function() { const item = shelf.children[2].item; + expect(item.label).to.be.equal('Creating Content'); + expect(item.route).to.be.equal('/getting-started/creating-content'); + }); + }); + + describe('the fourth item:', function() { + it('should be labeled and linked to Key Concepts', function() { + const item = shelf.children[3].item; + expect(item.label).to.be.equal('Key Concepts'); - expect(item.link).to.be.equal('/getting-started/key-concepts'); + expect(item.route).to.be.equal('/getting-started/key-concepts'); }); it('should have the correct sub items', function() { - const subitem = shelf.children[2].children; + const subitem = shelf.children[3].children; expect(subitem[0].item.label).to.be.equal('Workspace'); - expect(subitem[0].item.link).to.be.equal('#workspace'); + expect(subitem[0].item.route).to.be.equal('#workspace'); expect(subitem[1].item.label).to.be.equal('Templates'); - expect(subitem[1].item.link).to.be.equal('#templates'); + expect(subitem[1].item.route).to.be.equal('#templates'); expect(subitem[2].item.label).to.be.equal('Pages'); - expect(subitem[2].item.link).to.be.equal('#pages'); + expect(subitem[2].item.route).to.be.equal('#pages'); }); }); }); @@ -302,49 +311,40 @@ describe('Unit Test: Data', function() { }); describe('the first item:', function() { - it('should be labeled and linked to Styles and Web Components', function() { - const item = shelf.children[0].item; - - expect(item.label).to.be.equal('Styles and Web Components'); - expect(item.link).to.be.equal('/getting-started/branding'); - }); - - it('should have the correct sub items', function() { - const subitem = shelf.children[0].children; - - expect(subitem[0].item.label).to.be.equal('Web Components'); - expect(subitem[0].item.link).to.be.equal('#web-components'); - expect(subitem[1].item.label).to.be.equal('CSS'); - expect(subitem[1].item.link).to.be.equal('#css'); - }); - }); - - describe('the second item:', function() { it('should be labeled and linked to Quick Start', function() { - const item = shelf.children[1].item; + const item = shelf.children[0].item; expect(item.label).to.be.equal('Quick Start'); - expect(item.link).to.be.equal('/getting-started/quick-start'); + expect(item.route).to.be.equal('/getting-started/quick-start'); }); }); - describe('the third item:', function() { + describe('the second item:', function() { it('should be labeled and linked to Project Setup', function() { - const item = shelf.children[2].item; + const item = shelf.children[1].item; expect(item.label).to.be.equal('Project Setup'); - expect(item.link).to.be.equal('/getting-started/project-setup'); + expect(item.route).to.be.equal('/getting-started/project-setup'); }); it('should have the correct sub items', function() { - const subitem = shelf.children[2].children; + const subitem = shelf.children[1].children; expect(subitem[0].item.label).to.be.equal('Installing Greenwood'); - expect(subitem[0].item.link).to.be.equal('#installing-greenwood'); + expect(subitem[0].item.route).to.be.equal('#installing-greenwood'); expect(subitem[1].item.label).to.be.equal('Configuring Workflows'); - expect(subitem[1].item.link).to.be.equal('#configuring-workflows'); + expect(subitem[1].item.route).to.be.equal('#configuring-workflows'); expect(subitem[2].item.label).to.be.equal('Project Structure'); - expect(subitem[2].item.link).to.be.equal('#project-structure'); + expect(subitem[2].item.route).to.be.equal('#project-structure'); + }); + }); + + describe('the third item:', function() { + it('should be labeled and linked to Next Steps', function() { + const item = shelf.children[2].item; + + expect(item.label).to.be.equal('Next Steps'); + expect(item.route).to.be.equal('/getting-started/next-steps'); }); }); }); diff --git a/packages/cli/test/unit/data/schema/graph.spec.js b/packages/plugin-graphql/test/unit/schema/graph.spec.js similarity index 63% rename from packages/cli/test/unit/data/schema/graph.spec.js rename to packages/plugin-graphql/test/unit/schema/graph.spec.js index c33792d65..9ad105b87 100644 --- a/packages/cli/test/unit/data/schema/graph.spec.js +++ b/packages/plugin-graphql/test/unit/schema/graph.spec.js @@ -1,6 +1,6 @@ const expect = require('chai').expect; const MOCK_GRAPH = require('../mocks/graph'); -const { graphResolvers } = require('../../../../src/data/schema/graph'); +const { graphResolvers } = require('../../../src/schema/graph'); describe('Unit Test: Data', function() { @@ -22,11 +22,11 @@ describe('Unit Test: Data', function() { it('should have all expected properties for each page', function() { pages.forEach(function(page) { expect(page.id).to.exist; - expect(page.filePath).to.exist; - expect(page.fileName).to.exist; + expect(page.path).to.exist; + expect(page.filename).to.exist; expect(page.template).to.exist; expect(page.title).to.exist; - expect(page.link).to.exist; + expect(page.route).to.exist; }); }); }); @@ -39,38 +39,39 @@ describe('Unit Test: Data', function() { }); it('should have 8 children', function() { + // console.debug(children); expect(children.length).to.equal(7); }); it('should have the expected value for id for each child', function() { - expect(children[0].id).to.equal('css-components'); - expect(children[1].id).to.equal('deploy'); - expect(children[2].id).to.equal('create-content'); - expect(children[3].id).to.equal('concepts'); - expect(children[4].id).to.equal('next'); + expect(children[0].id).to.equal('branding'); + expect(children[1].id).to.equal('build-and-deploy'); + expect(children[2].id).to.equal('creating-content'); + expect(children[3].id).to.equal('key-concepts'); + expect(children[4].id).to.equal('next-steps'); expect(children[5].id).to.equal('project-setup'); - expect(children[6].id).to.equal('start'); + expect(children[6].id).to.equal('quick-start'); }); - it('should have the expected filePath for each child', function() { + it('should have the expected route for each child', function() { children.forEach(function(child) { - expect(child.link).to.equal(`/getting-started/${child.fileName}`); + expect(child.route).to.equal(`/getting-started/${child.id}`); }); }); - it('should have the expected fileName for each child', function() { - expect(children[0].fileName).to.equal('branding'); - expect(children[1].fileName).to.equal('build-and-deploy'); - expect(children[2].fileName).to.equal('creating-content'); - expect(children[3].fileName).to.equal('key-concepts'); - expect(children[4].fileName).to.equal('next-steps'); - expect(children[5].fileName).to.equal('project-setup'); - expect(children[6].fileName).to.equal('quick-start'); + it('should have the expected label for each child', function() { + expect(children[0].label).to.equal('Branding'); + expect(children[1].label).to.equal('Build And Deploy'); + expect(children[2].label).to.equal('Creating Content'); + expect(children[3].label).to.equal('Key Concepts'); + expect(children[4].label).to.equal('Next Steps'); + expect(children[5].label).to.equal('Project Setup'); + expect(children[6].label).to.equal('Quick Start'); }); - it('should have the expected link for each child', function() { + it('should have the expected path for each child', function() { children.forEach(function(child) { - expect(child.filePath).to.equal(`./getting-started/${child.fileName}.md`); + expect(child.path).to.contain(`/getting-started/${child.id}.md`); }); }); diff --git a/packages/plugin-import-commonjs/README.md b/packages/plugin-import-commonjs/README.md new file mode 100644 index 000000000..7c1495678 --- /dev/null +++ b/packages/plugin-import-commonjs/README.md @@ -0,0 +1,45 @@ +# @greenwood/plugin-import-commonjs + +## Overview +A plugin for Greenwood for loading CommonJS based modules (`require` / `module.exports`) in the browser using ESM (`import` / `export`) syntax. _**Note**: It is highly encouraged that you favor ESM based packages for the cleanest / fastest interop and developer experience. Additional processing time and dependencies are required to handle the conversion._ + +> This package assumes you already have `@greenwood/cli` installed. + +## Installation +You can use your favorite JavaScript package manager to install this package. + +_examples:_ +```bash +# npm +npm -i @greenwood/plugin-import-commonjs --save-dev + +# yarn +yarn add @greenwood/plugin-import-commonjs --dev +``` + +## Usage +Add this plugin to your _greenwood.config.js_ and spread the `export`. + +```javascript +const pluginImportCommonJs = require('@greenwood/plugin-import-commonjs'); + +module.exports = { + ... + + plugins: [ + ...pluginImportCommonJs() // notice the spread ... ! + ] +} +``` + +This will then allow you to use a CommonJS based modules in the browser. For example, here is how you could use [**lodash**](https://lodash.com/) (although as mentioend above, in this case, you would want to use [**lodash-es**](https://www.npmjs.com/package/lodash-es) instead.) + +```javascript +// <script src="my-file.js"> +import _ from 'lodash'; + +. +. + +console.log(_.defaults({ 'a': 1 }, { 'a': 3, 'b': 2 })); // { 'a': 1, 'b': 2 } +``` \ No newline at end of file diff --git a/packages/plugin-import-commonjs/package.json b/packages/plugin-import-commonjs/package.json new file mode 100644 index 000000000..39ade5d77 --- /dev/null +++ b/packages/plugin-import-commonjs/package.json @@ -0,0 +1,33 @@ +{ + "name": "@greenwood/plugin-import-commonjs", + "version": "0.10.0-alpha.10", + "description": "A plugin for loading CommonJS based modules in the browser using ESM (import / export) syntax.", + "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-commonjs", + "author": "Owen Buckley <owen@thegreenhouse.io>", + "license": "MIT", + "keywords": [ + "Greenwood", + "CommonJS", + "Static Site Generator", + "NodeJS" + ], + "main": "src/index.js", + "files": [ + "src/" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@greenwood/cli": "^0.4.0" + }, + "dependencies": { + "@rollup/plugin-commonjs": "^17.0.0", + "@rollup/stream": "^2.0.0", + "cjs-module-lexer": "^1.0.0" + }, + "devDependencies": { + "@greenwood/cli": "^0.10.0-alpha.10", + "lodash": "^4.17.20" + } +} diff --git a/packages/plugin-import-commonjs/src/index.js b/packages/plugin-import-commonjs/src/index.js new file mode 100644 index 000000000..67c555d9a --- /dev/null +++ b/packages/plugin-import-commonjs/src/index.js @@ -0,0 +1,96 @@ +/* + * + * Detects and fully resolves import requests for CommonJS files in node_modules. + * + */ +const commonjs = require('@rollup/plugin-commonjs'); +const fs = require('fs'); +const path = require('path'); +const { parse } = require('cjs-module-lexer'); +const { ResourceInterface } = require('@greenwood/cli/src/lib/resource-interface'); +const rollupStream = require('@rollup/stream'); + +// bit of a workaround for now, but maybe this could be supported by cjs-module-lexar natively? +// https://github.com/guybedford/cjs-module-lexer/issues/35 +const testForCjsModule = async(url) => { + let isCommonJs = false; + + if (path.extname(url) === '.js' && (/node_modules/).test(url) && url.indexOf('es-module-shims.js') < 0) { + try { + const body = await fs.promises.readFile(url, 'utf-8'); + await parse(body); + + isCommonJs = true; + } catch (e) { + const { message } = e; + const isProbablyLexarErrorSoIgnore = message.indexOf('Unexpected import statement in CJS module.') >= 0 + || message.indexOf('Unexpected export statement in CJS module.') >= 0; + + if (!isProbablyLexarErrorSoIgnore) { + // we probably _shouldn't_ ignore this, so let's log it since we don't want to swallow all errors + console.error(e); + } + } + } + + return Promise.resolve(isCommonJs); +}; + +class ImportCommonJsResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + } + + async shouldIntercept(url) { + return new Promise(async (resolve, reject) => { + try { + const isCommonJs = await testForCjsModule(url); + + return resolve(isCommonJs); + } catch (e) { + console.error(e); + reject(e); + } + }); + } + + async intercept(url) { + return new Promise(async(resolve, reject) => { + try { + const options = { + input: url, + output: { format: 'esm' }, + plugins: [ + commonjs() + ] + }; + const stream = rollupStream(options); + let bundle = ''; + + stream.on('data', (data) => (bundle += data)); + stream.on('end', () => { + console.debug(`proccessed module "${url}" as a CommonJS module type.`); + resolve({ + body: bundle + }); + }); + } catch (e) { + reject(e); + } + }); + } +} + +module.exports = (options = {}) => { + return [{ + type: 'resource', + name: 'plugin-import-commonjs:resource', + provider: (compilation) => new ImportCommonJsResource(compilation, options) + }, { + type: 'rollup', + name: 'plugin-import-commonjs:rollup', + provider: () => [ + commonjs() + ] + }]; +}; \ No newline at end of file diff --git a/packages/plugin-import-commonjs/test/cases/default/default.spec.js b/packages/plugin-import-commonjs/test/cases/default/default.spec.js new file mode 100644 index 000000000..6a2bed88a --- /dev/null +++ b/packages/plugin-import-commonjs/test/cases/default/default.spec.js @@ -0,0 +1,91 @@ +/* + * Use Case + * Run Greenwood with pluginImportCommonjs plugin with default options. + * + * Uaer Result + * Should generate a bare bones Greenwood build without erroring on a CommonJS module. + * + * User Command + * greenwood build + * + * User Config + * const pluginImportCommonjs = require('@greenwod/plugin-import-commonjs'); + * + * { + * plugins: [{ + * ...pluginImportCommonjs() + * }] + * } + * + * User Workspace + * src/ + * pages/ + * index.html + * scripts/ + * main.js + */ +const expect = require('chai').expect; +const glob = require('glob-promise'); +const { JSDOM } = require('jsdom'); +const path = require('path'); +const runSmokeTest = require('../../../../../test/smoke-test'); +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'Import CommonJs Plugin with default options'; + + let setup; + + before(async function() { + setup = new TestBed(); + + this.context = await setup.setupTestBed(__dirname, [{ + dir: 'node_modules/lodash/', + name: 'lodash.js' + }, { + dir: 'node_modules/lodash/', + name: 'package.json' + }]); + }); + + describe(LABEL, function() { + before(async function() { + await setup.runGreenwoodCommand('build'); + }); + + runSmokeTest(['public', 'index'], LABEL); + + describe('Script tag in the <head> tag', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + it('should have one <script> tag for main.js loaded in the <head> tag', function() { + const scriptTags = dom.window.document.querySelectorAll('head > script'); + const mainScriptTag = Array.prototype.slice.call(scriptTags).filter(script => { + return (/main.*.js/).test(script.src); + }); + + expect(mainScriptTag.length).to.be.equal(1); + }); + + it('should have the expected main.js file in the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, 'main.*.js'))).to.have.lengthOf(1); + }); + + it('should have the expected output from main.js (lodash) in the page output', async function() { + const spanTags = dom.window.document.querySelectorAll('body > span'); + + expect(spanTags.length).to.be.equal(1); + expect(spanTags[0].textContent).to.be.equal('import from lodash {"a":1,"b":2}'); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-import-commonjs/test/cases/default/greenwood.config.js b/packages/plugin-import-commonjs/test/cases/default/greenwood.config.js new file mode 100644 index 000000000..12a486c67 --- /dev/null +++ b/packages/plugin-import-commonjs/test/cases/default/greenwood.config.js @@ -0,0 +1,7 @@ +const pluginImportCommonJs = require('../../../src/index'); + +module.exports = { + plugins: [ + ...pluginImportCommonJs() + ] +}; \ No newline at end of file diff --git a/packages/plugin-import-commonjs/test/cases/default/package.json b/packages/plugin-import-commonjs/test/cases/default/package.json new file mode 100644 index 000000000..9de6bde7a --- /dev/null +++ b/packages/plugin-import-commonjs/test/cases/default/package.json @@ -0,0 +1,6 @@ +{ + "name": "test-plugin-import-commonjs", + "dependencies": { + "lodash": "^4.17.20" + } +} \ No newline at end of file diff --git a/packages/plugin-import-commonjs/test/cases/default/src/pages/index.html b/packages/plugin-import-commonjs/test/cases/default/src/pages/index.html new file mode 100644 index 000000000..caf35bf84 --- /dev/null +++ b/packages/plugin-import-commonjs/test/cases/default/src/pages/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="../scripts/main.js"></script> + </head> + + <body> + <span></span> + </body> + +</html> \ No newline at end of file diff --git a/packages/plugin-import-commonjs/test/cases/default/src/scripts/main.js b/packages/plugin-import-commonjs/test/cases/default/src/scripts/main.js new file mode 100644 index 000000000..3f990be4e --- /dev/null +++ b/packages/plugin-import-commonjs/test/cases/default/src/scripts/main.js @@ -0,0 +1,4 @@ +import _ from 'lodash'; + +const output = JSON.stringify(_.defaults({ 'a': 1 }, { 'a': 3, 'b': 2 })); +document.getElementsByTagName('span')[0].innerHTML = `import from lodash ${output}`; \ No newline at end of file diff --git a/packages/plugin-import-css/README.md b/packages/plugin-import-css/README.md new file mode 100644 index 000000000..b930291e5 --- /dev/null +++ b/packages/plugin-import-css/README.md @@ -0,0 +1,41 @@ +# @greenwood/plugin-import-css + +## Overview +A Greenwood plugin to allow you use ESM (`import`) syntax to load your CSS. + +> This package assumes you already have `@greenwood/cli` installed. + +## Installation +You can use your favorite JavaScript package manager to install this package. + +_examples:_ +```bash +# npm +npm -i @greenwood/plugin-import-css --save-dev + +# yarn +yarn add @greenwood/plugin-import-css --dev +``` + +## Usage +Add this plugin to your _greenwood.config.js_ and spread the `export`. + +```javascript +const pluginImportCss = require('@greenwood/plugin-import-css'); + +module.exports = { + ... + + plugins: [ + ...pluginImportCss() // notice the spread ... ! + ] +} +``` + +> 👉 _If you are using this along with [**plugin-postcss**](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-postcss), make sure **plugin-postcss** comes first. All non standard transformations need to come last._ + + +This will then allow you use `import` to include CSS in your JavaScript files. +```js +import cardCss from './card.css'; +``` \ No newline at end of file diff --git a/packages/plugin-import-css/package.json b/packages/plugin-import-css/package.json new file mode 100644 index 000000000..22e862cf2 --- /dev/null +++ b/packages/plugin-import-css/package.json @@ -0,0 +1,31 @@ +{ + "name": "@greenwood/plugin-import-css", + "version": "0.10.0-alpha.10", + "description": "A Greenwood plugin to allow you use ESM (import) syntax to load your CSS.", + "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-postcss", + "author": "Owen Buckley <owen@thegreenhouse.io>", + "license": "MIT", + "keywords": [ + "Greenwood", + "CommonJS", + "Static Site Generator", + "NodeJS", + "Css-in-Js" + ], + "main": "src/index.js", + "files": [ + "src/" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@greenwood/cli": "^0.4.0" + }, + "dependencies": { + "rollup-plugin-postcss": "^3.1.5" + }, + "devDependencies": { + "@greenwood/cli": "^0.10.0-alpha.10" + } +} diff --git a/packages/plugin-import-css/src/index.js b/packages/plugin-import-css/src/index.js new file mode 100644 index 000000000..d90b67104 --- /dev/null +++ b/packages/plugin-import-css/src/index.js @@ -0,0 +1,59 @@ + +/* + * + * Enables using JavaScript to import CSS files, using ESM syntax. + * + */ +const path = require('path'); +const postcssRollup = require('rollup-plugin-postcss'); +const { ResourceInterface } = require('@greenwood/cli/src/lib/resource-interface'); + +class ImportCssResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + this.extensions = ['.css']; + this.contentType = 'text/javascript'; + } + + async shouldIntercept(url, body, headers) { + // https://github.com/ProjectEvergreen/greenwood/issues/492 + const isCssInJs = path.extname(url) === this.extensions[0] + && headers.request.accept.indexOf('text/css') < 0 + && headers.request.accept.indexOf('application/signed-exchange') < 0; + + return Promise.resolve(isCssInJs); + } + + async intercept(url, body) { + return new Promise(async (resolve, reject) => { + try { + const cssInJsBody = `const css = "${body.replace(/\r?\n|\r/g, ' ')}";\nexport default css;`; + + resolve({ + body: cssInJsBody, + contentType: this.contentType + }); + } catch (e) { + reject(e); + } + }); + } +} + +module.exports = (options = {}) => { + return [{ + type: 'resource', + name: 'plugin-import-css:resource', + provider: (compilation) => new ImportCssResource(compilation, options) + }, { + type: 'rollup', + name: 'plugin-import-css:rollup', + provider: () => [ + postcssRollup({ + extract: false, + minimize: true, + inject: false + }) + ] + }]; +}; \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/default/default.spec.js b/packages/plugin-import-css/test/cases/default/default.spec.js new file mode 100644 index 000000000..b6b0070d6 --- /dev/null +++ b/packages/plugin-import-css/test/cases/default/default.spec.js @@ -0,0 +1,70 @@ +/* + * Use Case + * Run Greenwood with pluginImportCss plugin with default options. + * + * Uaer Result + * Should generate a bare bones Greenwood build without erroring when using ESM (import) with CSS. + * + * User Command + * greenwood build + * + * User Config + * const pluginImportCss = require('@greenwod/plugin-import-css'); + * + * { + * plugins: [{ + * ...pluginImportCss() + * }] + * } + * + * User Workspace + * src/ + * main.js + * styles.css + * pages/ + * index.html + */ +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'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'Import Css Plugin with default options'; + + 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'], LABEL); + + describe('importing CSS using ESM (import)', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); + }); + + it('should have the expected output from main.js (lodash) in the page output', async function() { + const spanTags = dom.window.document.querySelectorAll('body > span'); + + expect(spanTags.length).to.be.equal(1); + expect(spanTags[0].textContent).to.be.equal('import from styles.css: p { color: red; }'); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/default/greenwood.config.js b/packages/plugin-import-css/test/cases/default/greenwood.config.js new file mode 100644 index 000000000..8db897fe3 --- /dev/null +++ b/packages/plugin-import-css/test/cases/default/greenwood.config.js @@ -0,0 +1,7 @@ +const pluginImportCss = require('../../../src/index'); + +module.exports = { + plugins: [ + ...pluginImportCss() + ] +}; \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/default/src/main.js b/packages/plugin-import-css/test/cases/default/src/main.js new file mode 100644 index 000000000..a9bb01567 --- /dev/null +++ b/packages/plugin-import-css/test/cases/default/src/main.js @@ -0,0 +1,3 @@ +import stylesCss from './styles.css'; + +document.getElementsByTagName('span')[0].innerHTML = `import from styles.css: ${stylesCss}`; \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/default/src/pages/index.html b/packages/plugin-import-css/test/cases/default/src/pages/index.html new file mode 100644 index 000000000..e58731458 --- /dev/null +++ b/packages/plugin-import-css/test/cases/default/src/pages/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <script type="module" src="../main.js"></script> + </head> + + <body> + <span></span> + </body> + +</html> \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/default/src/styles.css b/packages/plugin-import-css/test/cases/default/src/styles.css new file mode 100644 index 000000000..e21e2c285 --- /dev/null +++ b/packages/plugin-import-css/test/cases/default/src/styles.css @@ -0,0 +1,3 @@ +p { + color: red; +} \ No newline at end of file diff --git a/packages/plugin-polyfills/README.md b/packages/plugin-polyfills/README.md index 41d162946..1104361af 100644 --- a/packages/plugin-polyfills/README.md +++ b/packages/plugin-polyfills/README.md @@ -1,21 +1,19 @@ # @greenwood/plugin-polyfills ## Overview -> _**NOTE: This package is currently installed by default by Greenwood so you don't need to install it yourself.**_ - -A composite plugin for Greenwood for adding support for adding Web Component related polyfills for browser that need support for it. It uses [feature detection]()https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs#using-webcomponents-loaderjs to determine what polyfills are actually needed based on the user's browser, to ensure only the minumum extra code is needed. +A Greenwood plugin adding support for [Web Component related polyfills](https://github.com/webcomponents/polyfills) for browsers that need support for part of the Web Component spec like **Custom Elements** and **Shadow DOM**. It uses [feature detection](https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs#using-webcomponents-loaderjs) to determine what polyfills are actually needed based on the user's browser, to ensure only the minumum extra code is loaded. As of right now, you will likely need this plugin to load additional polyfills if you want to support these browser(s): - Internet Explorer <= 11 - Mobile Browsers -> See Greenwood's [browser support](https://www.greenwoodjs.io/about/how-it-works#browser-support) and [evergreen build](https://www.greenwoodjs.io/about/how-it-works#evergreen-build) docs for more information on how Greenwood handles browser support out of the box. Or visit [caniuse.com](https://caniuse.com/) to look up specific support for specific browsers. +See Greenwood's [browser support](https://www.greenwoodjs.io/about/how-it-works#browser-support) and [evergreen build](https://www.greenwoodjs.io/about/how-it-works#evergreen-build) docs for more information on how Greenwood handles browser support out of the box. Or visit [caniuse.com](https://caniuse.com/) to look up specific support for specific browsers. -> This package assumes you already have `@greenwood/cli` installed. +> _For more information and complete docs about Greenwood, please visit the [Greenwood website](https://www.greenwoodjs.io/)._ ## Installation -You can use your favorite JavaScript package manager to install this package. +You can use your favorite JavaScript package manager to install this package. This package assumes you already have `@greenwood/cli` installed. _examples:_ ```bash @@ -29,8 +27,6 @@ yarn add @greenwood/plugin-polyfills --dev ## Usage Use this plugin in your _greenwood.config.js_. -> As this is a composite plugin, you will need to spread the result. - ```javascript const polyfillsPlugin = require('@greenwood/plugin-polyfills'); @@ -38,11 +34,11 @@ module.exports = { ... plugins: [ - ...polyfillsPlugin() + polyfillsPlugin() ] } ``` -This will then add the necessary Polyfills to have your project work in those browsers. +Now when your project builds for production, you will see a _bundles/_ directory in your output directory, as well as a file called _webcomponents-loader.js_, as well as a `<script>` tag for that file in the `<head>` of your _index.html_ files. When a page is loaded, the feature detection capabilities will then load the necessary polyfills to have your project work for a user's given browser. > Note: we would like to add support for [differntial loading](https://github.com/ProjectEvergreen/greenwood/issues/224) to avoid the cost of this for newer browsers. \ No newline at end of file diff --git a/packages/plugin-polyfills/package.json b/packages/plugin-polyfills/package.json index d2bc11326..cd72d2817 100644 --- a/packages/plugin-polyfills/package.json +++ b/packages/plugin-polyfills/package.json @@ -1,16 +1,14 @@ { "name": "@greenwood/plugin-polyfills", - "version": "0.9.0", - "description": "A composite plugin for Greenwood for adding polyfill support for older browsers.", + "version": "0.10.0-alpha.10", + "description": "A Greenwood plugin adding support for Web Component related polyfills like Custom Elements and Shadow DOM.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-polyfills", "author": "Owen Buckley <owen@thegreenhouse.io>", "license": "MIT", "keywords": [ "Greenwood", - "Web Components", - "Lit Element", - "Lit Html", "Static Site Generator", + "Web Components", "Polyfills" ], "main": "src/index.js", @@ -27,6 +25,6 @@ "@webcomponents/webcomponentsjs": "^2.3.0" }, "devDependencies": { - "@greenwood/cli": "^0.9.0" + "@greenwood/cli": "^0.10.0-alpha.10" } } diff --git a/packages/plugin-polyfills/src/index.js b/packages/plugin-polyfills/src/index.js index e8deecf33..167faeb97 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: ` - <!-- Web Components poyfill --> - <script src="/${filename}"></script> - ` - }; - } - }, { - 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); + } + + async shouldOptimize(url) { + return Promise.resolve(path.extname(url) === '.html'); + } + + async optimize(url, body) { + 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 = body.replace('<head>', ` + <head> + <script src="/${filename}"></script> + `); + + 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 ee963f8e3..bff4cd0e1 100644 --- a/packages/plugin-polyfills/test/cases/default/default.spec.js +++ b/packages/plugin-polyfills/test/cases/default/default.spec.js @@ -22,11 +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'); +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'; @@ -35,10 +48,16 @@ describe('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,12 +65,12 @@ describe('Build Greenwood With: ', function() { await setup.runGreenwoodCommand('build'); }); - runSmokeTest(['public', 'index', 'not-found', 'hello'], LABEL); + runSmokeTest(['public', 'index'], LABEL); describe('Script tag in the <head> tag', function() { let dom; - beforeEach(async function() { + before(async function() { dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); }); @@ -61,10 +80,18 @@ describe('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 diff --git a/packages/plugin-postcss/README.md b/packages/plugin-postcss/README.md new file mode 100644 index 000000000..dce43e03b --- /dev/null +++ b/packages/plugin-postcss/README.md @@ -0,0 +1,76 @@ +# @greenwood/plugin-postcss + +## Overview +A Greenwood plugin for loading [**PostCSS**](https://postcss.org/) configuration and applying it to your CSS. + +> This package assumes you already have `@greenwood/cli` installed. + +## Installation +You can use your favorite JavaScript package manager to install this package. + +_examples:_ +```bash +# npm +npm -i @greenwood/plugin-postcss --save-dev + +# yarn +yarn add @greenwood/plugin-postcss --dev +``` + +## Usage +Add this plugin to your _greenwood.config.js_. + +```javascript +const pluginPostCss = require('@greenwood/plugin-postcss'); + +module.exports = { + ... + + plugins: [ + pluginPostCss() + ] +} +``` + +> 👉 _If you are using this along with [**plugin-import-css**](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-css), make sure **plugin-postcss** comes first. All non stanrd transformation need to come last._ + +Optionally, create a _postcss.config.js_ in the root of your project with your own custom plugins / settings that you've installed. + +```javascript +module.exports = { + plugins: [ + require('postcss-nested') + ] +}; +``` + + +## Options +This plugin provides a default _postcss.config.js_ that includes support for [**postcss-preset-env**](https://github.com/csstools/postcss-preset-env) using [**browserslist**](https://github.com/browserslist/browserslist) with reasonable [default configs](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-postcss/src/) for each. + +If you would like to use it with your own custom _postcss.config.js_, you will need to enable the `extendConfig` option +```js +const pluginPostcss = require('@greenwood/plugin-postcss'); + +module.exports = { + ... + + plugins: [ + pluginPostcss({ + extendConfig: true + }) + ] +} +``` + +By default, the configuration provided by this plugin is: +```javascript +module.exports = { + plugins: [ + require('cssnano'), // just for production builds + require('postcss-preset-env') + ] +}; +``` + +This will then process your CSS with PostCSS with the configurated plugins / settings you provide, merged after the default `plugins` listed above. \ No newline at end of file diff --git a/packages/plugin-postcss/package.json b/packages/plugin-postcss/package.json new file mode 100644 index 000000000..aec0612e0 --- /dev/null +++ b/packages/plugin-postcss/package.json @@ -0,0 +1,32 @@ +{ + "name": "@greenwood/plugin-postcss", + "version": "0.10.0-alpha.10", + "description": "A Greenwood plugin for loading PostCSS configuration and applying it to your CSS.", + "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-postcss", + "author": "Owen Buckley <owen@thegreenhouse.io>", + "license": "MIT", + "keywords": [ + "Greenwood", + "CommonJS", + "Static Site Generator", + "NodeJS", + "PostCSS" + ], + "main": "src/index.js", + "files": [ + "src/" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@greenwood/cli": "^0.4.0", + "postcss": "^7.0.32" + }, + "dependencies": { + "postcss-preset-env": "^6.7.0" + }, + "devDependencies": { + "@greenwood/cli": "^0.10.0-alpha.10" + } +} diff --git a/packages/plugin-postcss/src/.browserslistrc b/packages/plugin-postcss/src/.browserslistrc new file mode 100644 index 000000000..b84c67c70 --- /dev/null +++ b/packages/plugin-postcss/src/.browserslistrc @@ -0,0 +1,2 @@ +> 1% +not dead \ No newline at end of file diff --git a/packages/plugin-postcss/src/index.js b/packages/plugin-postcss/src/index.js new file mode 100644 index 000000000..0f71c8cb0 --- /dev/null +++ b/packages/plugin-postcss/src/index.js @@ -0,0 +1,90 @@ +/* + * + * Enable using PostCSS process for CSS files. + * + */ +const fs = require('fs'); +const path = require('path'); +const postcss = require('postcss'); +const { ResourceInterface } = require('@greenwood/cli/src/lib/resource-interface'); + +function getConfig (compilation, extendConfig = false) { + const { projectDirectory } = compilation.context; + const configFile = 'postcss.config'; + const defaultConfig = require(path.join(__dirname, configFile)); + const userConfig = fs.existsSync(path.join(projectDirectory, `${configFile}.js`)) + ? require(`${projectDirectory}/${configFile}`) + : {}; + let finalConfig = Object.assign({}, userConfig); + + if (userConfig && extendConfig) { + finalConfig.plugins = Array.isArray(userConfig.plugins) + ? [...defaultConfig.plugins, ...userConfig.plugins] + : [...defaultConfig.plugins]; + } + + return finalConfig; +} + +class PostCssResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + this.extensions = ['.css']; + this.contentType = ['text/css']; + } + + isCssFile(url) { + return path.extname(url) === '.css'; + } + + async shouldIntercept(url) { + return Promise.resolve(this.isCssFile(url)); + } + + async intercept(url, body) { + return new Promise(async(resolve, reject) => { + try { + const config = getConfig(this.compilation); + const plugins = config.plugins || []; + const css = plugins.length > 0 + ? (await postcss(plugins).process(body, { from: url })).css + : body; + + resolve({ + body: css + }); + } catch (e) { + reject(e); + } + }); + } + + async shouldOptimize(url) { + return Promise.resolve(this.isCssFile(url)); + } + + async optimize(url, body) { + const { outputDir, userWorkspace } = this.compilation.context; + const workspaceUrl = url.replace(outputDir, userWorkspace); + const config = getConfig(this.compilation, this.options.extendConfig); + const plugins = config.plugins || []; + + plugins.push( + require('cssnano') + ); + + const css = plugins.length > 0 + ? (await postcss(plugins).process(body, { from: workspaceUrl })).css + : body; + + return Promise.resolve(css); + } +} + +module.exports = (options = {}) => { + return { + type: 'resource', + name: 'plugin-postcss', + provider: (compilation) => new PostCssResource(compilation, options) + }; +}; \ No newline at end of file diff --git a/packages/plugin-postcss/src/postcss.config.js b/packages/plugin-postcss/src/postcss.config.js new file mode 100644 index 000000000..aa6439be5 --- /dev/null +++ b/packages/plugin-postcss/src/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + 'postcss-preset-env' + ] +}; \ No newline at end of file diff --git a/packages/plugin-postcss/test/cases/default/default.spec.js b/packages/plugin-postcss/test/cases/default/default.spec.js new file mode 100644 index 000000000..7a0d6edea --- /dev/null +++ b/packages/plugin-postcss/test/cases/default/default.spec.js @@ -0,0 +1,67 @@ +/* + * Use Case + * Run Greenwood with default PostCSS config. + * + * User Result + * Should generate a bare bones Greenwood build with the user's CSS file correctly minified. + * + * User Command + * greenwood build + * + * User Config + * const pluginPostCss = require('@greenwod/plugin-postcss'); + * + * { + * plugins: [ + * pluginPostCss() + * ] + * } + * + * User Workspace + * src/ + * pages/ + * index.html + * styles/ + * main.css + */ +const fs = require('fs'); +const glob = require('glob-promise'); +const path = require('path'); +const expect = require('chai').expect; +const runSmokeTest = require('../../../../../test/smoke-test'); +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'Default PostCSS configuration'; + 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'], LABEL); + + describe('Page referencing external nested CSS file', function() { + it('should output correctly processed nested CSS as non nested', function() { + const expectedCss = 'body{color:red}h1{color:#00f}'; + const cssFiles = glob.sync(path.join(this.context.publicDir, 'styles', '*.css')); + const css = fs.readFileSync(cssFiles[0], 'utf-8'); + + expect(cssFiles.length).to.equal(1); + expect(css).to.equal(expectedCss); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-postcss/test/cases/default/greenwood.config.js b/packages/plugin-postcss/test/cases/default/greenwood.config.js new file mode 100644 index 000000000..3c46ed010 --- /dev/null +++ b/packages/plugin-postcss/test/cases/default/greenwood.config.js @@ -0,0 +1,7 @@ +const pluginPostCss = require('../../../src/index'); + +module.exports = { + plugins: [ + pluginPostCss() + ] +}; \ No newline at end of file diff --git a/packages/plugin-postcss/test/cases/default/src/pages/index.html b/packages/plugin-postcss/test/cases/default/src/pages/index.html new file mode 100644 index 000000000..68663407a --- /dev/null +++ b/packages/plugin-postcss/test/cases/default/src/pages/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <link rel="stylesheet" href="/styles/main.css"></link> + </head> + + <body> + <h1>Hello World!</h1> + </body> + +</html> \ No newline at end of file diff --git a/packages/plugin-postcss/test/cases/default/src/styles/main.css b/packages/plugin-postcss/test/cases/default/src/styles/main.css new file mode 100644 index 000000000..9ff1eeec0 --- /dev/null +++ b/packages/plugin-postcss/test/cases/default/src/styles/main.css @@ -0,0 +1,7 @@ +body { + color: red; +} + +h1 { + color: blue; +} \ No newline at end of file diff --git a/packages/plugin-postcss/test/cases/options.extend-config/greenwood.config.js b/packages/plugin-postcss/test/cases/options.extend-config/greenwood.config.js new file mode 100644 index 000000000..3c46ed010 --- /dev/null +++ b/packages/plugin-postcss/test/cases/options.extend-config/greenwood.config.js @@ -0,0 +1,7 @@ +const pluginPostCss = require('../../../src/index'); + +module.exports = { + plugins: [ + pluginPostCss() + ] +}; \ No newline at end of file diff --git a/packages/plugin-postcss/test/cases/options.extend-config/options.extend-config.spec.js b/packages/plugin-postcss/test/cases/options.extend-config/options.extend-config.spec.js new file mode 100644 index 000000000..625363f52 --- /dev/null +++ b/packages/plugin-postcss/test/cases/options.extend-config/options.extend-config.spec.js @@ -0,0 +1,74 @@ +/* + * Use Case + * Run Greenwood with a custom PostCSS config + * + * User Result + * Should generate a bare bones Greenwood build with the user's CSS file correctly un-nested and minified + * + * User Command + * greenwood build + * + * User Config + * const pluginPostCss = require('@greenwod/plugin-postcss'); + * + * { + * plugins: [ + * pluginPostCss() + * ] + * } + * + * User Workspace + * src/ + * pages/ + * index.html + * styles/ + * main.css + * + * User postcss.config.js + * module.exports = { + * plugins: [ + * require('postcss-nested') + * ] + * }; + */ +const fs = require('fs'); +const glob = require('glob-promise'); +const path = require('path'); +const expect = require('chai').expect; +const runSmokeTest = require('../../../../../test/smoke-test'); +const TestBed = require('../../../../../test/test-bed'); + +describe('Build Greenwood With: ', function() { + const LABEL = 'Custom PostCSS configuration'; + 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'], LABEL); + + describe('Page referencing external nested CSS file', function() { + it('should output correctly processed nested CSS as non nested', function() { + const expectedCss = 'body{color:red}body h1{color:#00f}'; + const cssFiles = glob.sync(path.join(this.context.publicDir, 'styles', '*.css')); + const css = fs.readFileSync(cssFiles[0], 'utf-8'); + + expect(cssFiles.length).to.equal(1); + expect(css).to.equal(expectedCss); + }); + }); + }); + + after(function() { + setup.teardownTestBed(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-postcss/test/cases/options.extend-config/postcss.config.js b/packages/plugin-postcss/test/cases/options.extend-config/postcss.config.js new file mode 100644 index 000000000..80bcddab0 --- /dev/null +++ b/packages/plugin-postcss/test/cases/options.extend-config/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + require('postcss-nested') + ] +}; \ No newline at end of file diff --git a/packages/plugin-postcss/test/cases/options.extend-config/src/pages/index.html b/packages/plugin-postcss/test/cases/options.extend-config/src/pages/index.html new file mode 100644 index 000000000..68663407a --- /dev/null +++ b/packages/plugin-postcss/test/cases/options.extend-config/src/pages/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" prefix="og:http://ogp.me/ns#"> + + <head> + <link rel="stylesheet" href="/styles/main.css"></link> + </head> + + <body> + <h1>Hello World!</h1> + </body> + +</html> \ No newline at end of file diff --git a/packages/plugin-postcss/test/cases/options.extend-config/src/styles/main.css b/packages/plugin-postcss/test/cases/options.extend-config/src/styles/main.css new file mode 100644 index 000000000..d44494250 --- /dev/null +++ b/packages/plugin-postcss/test/cases/options.extend-config/src/styles/main.css @@ -0,0 +1,8 @@ +body { + color: red; + + & h1 { + color: blue; + } + +} \ No newline at end of file diff --git a/postcss.config.js b/postcss.config.js index dbe4fb4a1..80bcddab0 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,9 +1,5 @@ module.exports = { - plugins: { - 'postcss-preset-env': { - stage: 1 - }, - 'postcss-nested': {}, - 'cssnano': {} - } + plugins: [ + require('postcss-nested') + ] }; \ No newline at end of file diff --git a/stylelint.config.js b/stylelint.config.js index cc68bbd71..514d06eb0 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -11,6 +11,10 @@ module.exports = { 'value-list-comma-newline-after': null, 'declaration-colon-newline-after': null, 'value-keyword-case': null, - 'declaration-bang-space-before': null + 'declaration-bang-space-before': null, + 'selector-type-no-unknown': [true, { + ignore: ['custom-elements'], + ignoreTypes: ['/^app-/'] + }] } }; \ No newline at end of file diff --git a/test/smoke-test.js b/test/smoke-test.js index ed6862c76..edeaf3ad0 100644 --- a/test/smoke-test.js +++ b/test/smoke-test.js @@ -12,7 +12,14 @@ const glob = require('glob-promise'); const { JSDOM } = require('jsdom'); const path = require('path'); -const mainBundleScriptRegex = /index.*.bundle\.js/; +function tagsMatch(tagName, html) { + const openTagRegex = new RegExp(`<${tagName}`, 'g'); + const closeTagRegex = new RegExp(`<\/${tagName.replace('>', '')}>`, 'g'); + const openingCount = (html.match(openTagRegex) || []).length; + const closingCount = (html.match(closeTagRegex) || []).length; + + return openingCount === closingCount; +} function publicDirectory(label) { describe(`Running Smoke Tests: ${label}`, function() { @@ -25,62 +32,8 @@ function publicDirectory(label) { expect(fs.existsSync(path.join(this.context.publicDir, './index.html'))).to.be.true; }); - it('should output a single 404.html file (not found page)', function() { - expect(fs.existsSync(path.join(this.context.publicDir, './404.html'))).to.be.true; - }); - - it('should output one JS bundle file', async function() { - expect(await glob.promise(path.join(this.context.publicDir, './index.*.bundle.js'))).to.have.lengthOf(1); - }); - }); - }); -} - -function defaultNotFound(label) { - describe(`Running Smoke Tests: ${label}`, function() { - describe('404 (Not Found) page', function() { - let dom; - - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, '404.html')); - }); - - it('should have one <script> tag in the <body> for the main bundle', function() { - const scriptTags = dom.window.document.querySelectorAll('body script'); - const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src.replace('file:///', ''); - - return mainBundleScriptRegex.test(src); - }); - - expect(bundledScript.length).to.be.equal(1); - }); - - it('should have no <script> tags for Apollo state', function() { - const scriptTags = dom.window.document.querySelectorAll('script'); - const bundleScripts = Array.prototype.slice.call(scriptTags).filter(script => { - return script.getAttribute('data-state') === 'apollo'; - }); - - expect(bundleScripts.length).to.be.equal(0); - }); - - it('should have no <script> tags in the <head>', function() { - const scriptTags = dom.window.document.querySelectorAll('head > script'); - - expect(scriptTags.length).to.be.equal(0); - }); - - it('should have a <title> tag in the <head>', function() { - const title = dom.window.document.querySelector('head title').textContent; - - expect(title).to.be.equal('404 - Not Found'); - }); - - it('should have a <h1> tag in the <body>', function() { - const heading = dom.window.document.querySelector('body h1').textContent; - - expect(heading).to.be.equal('404 Not Found'); + it('should output one graph.json file', async function() { + expect(await glob.promise(path.join(this.context.publicDir, 'graph.json'))).to.have.lengthOf(1); }); }); }); @@ -89,107 +42,69 @@ function defaultNotFound(label) { function defaultIndex(label) { describe(`Running Smoke Tests: ${label}`, function() { describe('Index (Home) page', function() { - const indexPageHeading = 'Greenwood'; - const indexPageBody = 'This is the home page built by Greenwood. Make your own pages in src/pages/index.js!'; let dom; + let html; - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); - }); - - it('should have a <title> tag in the <head>', function() { - const title = dom.window.document.querySelector('head title').textContent; + before(async function() { + const htmlPath = path.resolve(this.context.publicDir, 'index.html'); - expect(title).to.be.equal('My App'); + dom = await JSDOM.fromFile(htmlPath); + html = await fs.promises.readFile(htmlPath, 'utf-8'); }); - it('should have one <script> tag in the <body> for the main bundle', function() { - const scriptTags = dom.window.document.querySelectorAll('body > script'); - const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src.replace('file:///', ''); - - return mainBundleScriptRegex.test(src); + describe('document <head>', function() { + it('should have matching opening and closing <head> tags in the <head>', function() { + // add an expclit > here to avoid conflicting with <header> + // which is used in a lot of test case scaffolding + expect(tagsMatch('head>', html)).to.be.equal(true); }); - - expect(bundledScript.length).to.be.equal(1); - }); - - it('should have one <script> tag in the <body> for the main bundle loaded with async', function() { - const scriptTags = dom.window.document.querySelectorAll('body > script'); - const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => { - const src = script.src.replace('file:///', ''); - - return mainBundleScriptRegex.test(src); + + it('should have a <title> tag in the <head>', function() { + const title = dom.window.document.querySelector('head title').textContent; + + expect(title).to.not.be.undefined; }); - expect(bundledScript[0].getAttribute('async')).to.be.equal(''); - }); - - it('should have one <script> tag for Apollo state', function() { - const scriptTags = dom.window.document.querySelectorAll('script'); - const bundleScripts = Array.prototype.slice.call(scriptTags).filter(script => { - return script.getAttribute('data-state') === 'apollo'; + it('should have matching opening and closing <script> tags in the <head>', function() { + expect(tagsMatch('script', html)).to.be.equal(true); }); - expect(bundleScripts.length).to.be.equal(1); - }); - - it('should have a router outlet tag in the <body>', function() { - const outlet = dom.window.document.querySelectorAll('body eve-app'); - - expect(outlet.length).to.be.equal(1); - }); - - it('should have the correct route tags in the <body>', function() { - const routes = dom.window.document.querySelectorAll('body lit-route'); - - expect(routes.length).to.be.equal(3); - }); - - it('should have the expected heading text within the index page in the public directory', function() { - const heading = dom.window.document.querySelector('h3').textContent; - - expect(heading).to.equal(indexPageHeading); - }); - - it('should have the expected paragraph text within the index page in the public directory', function() { - const paragraph = dom.window.document.querySelector('p').textContent; - - expect(paragraph).to.equal(indexPageBody); - }); - }); - }); -} - -function defaultHelloPage(label) { - describe(`Running Smoke Tests: ${label}`, function() { - describe('Hello World (dummy) page', function() { - const helloPageHeading = 'Hello World'; - const helloPageBody = 'This is an example page built by Greenwood. Make your own in src/pages!'; - let dom; - - beforeEach(async function() { - dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './hello', './index.html')); - }); - - it('should output a hello page directory', function() { - expect(fs.existsSync(path.join(this.context.publicDir, './hello'))).to.be.true; - }); - - it('should output an index.html file within the default hello page directory', function() { - expect(fs.existsSync(path.join(this.context.publicDir, './hello', './index.html'))).to.be.true; - }); - - it('should have the expected heading text within the hello example page in the hello directory', function() { - const heading = dom.window.document.querySelector('h3').textContent; + it('should have matching opening and closing <link> tags in the <head>', function() { + const html = dom.window.document.querySelector('html').textContent; + + expect(tagsMatch('link', html)).to.be.equal(true); + }); - expect(heading).to.equal(helloPageHeading); + // note: one will always be present when using puppeteer + it('should have matching opening and closing <style> tags in the <head>', function() { + expect(tagsMatch('style', html)).to.be.equal(true); + }); }); - it('should have the expected paragraph text within the hello example page in the hello directory', function() { - let paragraph = dom.window.document.querySelector('p').textContent; - - expect(paragraph).to.equal(helloPageBody); + describe('document <body>', function() { + it('should have no <script> tags in the <body>', function() { + const bodyScripts = dom.window.document.querySelectorAll('body script'); + + expect(bodyScripts.length).to.be.equal(0); + }); + + it('should have no <link> tags in the <body>', function() { + const bodyLinks = dom.window.document.querySelectorAll('body link'); + + expect(bodyLinks.length).to.be.equal(0); + }); + + it('should have no <style> tags in the <body>', function() { + const bodyStyles = dom.window.document.querySelectorAll('body style'); + + expect(bodyStyles.length).to.be.equal(0); + }); + + it('should have no <meta> tags in the <body>', function() { + const bodyMetas = dom.window.document.querySelectorAll('body meta'); + + expect(bodyMetas.length).to.be.equal(0); + }); }); }); }); @@ -200,20 +115,14 @@ module.exports = runSmokeTest = async function(testCases, label) { testCases.forEach(async (testCase) => { switch (testCase) { - case 'not-found': - defaultNotFound(label); - break; case 'index': defaultIndex(label); break; - case 'hello': - defaultHelloPage(label); - break; case 'public': publicDirectory(label); break; default: - console.log(`unknown case ${testCase}`); // eslint-disable-line no-console + console.warn(`unknown case ${testCase}`); break; } diff --git a/test/test-bed.js b/test/test-bed.js index 7d930efd8..3eb24c851 100644 --- a/test/test-bed.js +++ b/test/test-bed.js @@ -6,13 +6,16 @@ * There are a number of examples in the CLI package you can use as a reference. * */ -const fs = require('fs-extra'); +const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawn } = require('child_process'); // needed for puppeteer - #193 const setupFiles = [{ + dir: 'node_modules/es-module-shims/dist', + name: 'es-module-shims.js' +}, { dir: 'node_modules/@webcomponents/webcomponentsjs', name: 'webcomponents-bundle.js' }]; @@ -41,8 +44,10 @@ module.exports = class TestBed { await new Promise(async(resolve, reject) => { try { - await fs.ensureDir(targetDir, { recursive: true }); - await fs.copy(targetSrc, targetPath); + await fs.promises.mkdir(targetDir, { + recursive: true + }); + await fs.promises.copyFile(targetSrc, targetPath); resolve(); } catch (err) { reject(err); @@ -69,12 +74,22 @@ module.exports = class TestBed { teardownTestBed() { return new Promise(async(resolve, reject) => { try { - await fs.remove(path.join(this.rootDir, '.greenwood')); - await fs.remove(path.join(this.rootDir, 'public')); + if (fs.existsSync(this.buildDir)) { + await fs.promises.rmdir(this.buildDir, { recursive: true }); + } + + if (fs.existsSync(this.publicDir)) { + await fs.promises.rmdir(this.publicDir, { recursive: true }); + } await Promise.all(setupFiles.map((file) => { - return fs.remove(path.join(this.rootDir, file.dir.split('/')[0])); + const dir = path.join(this.rootDir, file.dir.split('/')[0]); + + return fs.existsSync(dir) + ? fs.promises.rmdir(dir, { recursive: true }) + : Promise.resolve(); })); + resolve(); } catch (err) { reject(err); diff --git a/www/assets/simple.png b/www/assets/simple.png new file mode 100644 index 000000000..634822efc Binary files /dev/null and b/www/assets/simple.png differ diff --git a/www/assets/webpack.svg b/www/assets/webpack.svg deleted file mode 100644 index 5a14f2138..000000000 --- a/www/assets/webpack.svg +++ /dev/null @@ -1,68 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - viewBox="0 0 774 774.99999" - version="1.1" - id="svg4599" - sodipodi:docname="webpack.svg" - width="774" - height="775" - inkscape:version="0.92.4 (33fec40, 2019-01-16)"> - <metadata - id="metadata4605"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title>icon</dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <defs - id="defs4603" /> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1920" - inkscape:window-height="1052" - id="namedview4601" - showgrid="false" - inkscape:zoom="0.98892313" - inkscape:cx="387" - inkscape:cy="437.85001" - inkscape:window-x="0" - inkscape:window-y="0" - inkscape:window-maximized="1" - inkscape:current-layer="svg4599" /> - <title - id="title4591">icon - - - - diff --git a/www/components/banner/banner.css b/www/components/banner/banner.css index e850b4058..e4dd3659a 100644 --- a/www/components/banner/banner.css +++ b/www/components/banner/banner.css @@ -20,7 +20,7 @@ :host { & .banner { - background-color: #f6f2f4; + background-color: #fff; min-height: 60vh; & .content { @@ -40,12 +40,12 @@ & h1 { font-size: 3.5rem; - text-shadow: 1px 1px rgba(0, 0, 0, 0.6); + color: #201e2e; } & h3 { padding-top: 10px; - text-shadow: 1px 1px rgba(0, 0, 0, 0.6); + color: #201e2e; } & img { @@ -107,4 +107,4 @@ } } } -} +} \ No newline at end of file diff --git a/www/components/banner/banner.js b/www/components/banner/banner.js index 96553bfe9..7a75ac9a4 100644 --- a/www/components/banner/banner.js +++ b/www/components/banner/banner.js @@ -1,11 +1,6 @@ -import { html, LitElement } from 'lit-element'; +import { css, html, LitElement, unsafeCSS } from 'lit-element'; import bannerCss from './banner.css'; import buttonCss from './button.css'; -import greenwoodLogo300 from '../../assets/greenwood-logo-300w.png'; -import greenwoodLogo500 from '../../assets/greenwood-logo-500w.png'; -import greenwoodLogo750 from '../../assets/greenwood-logo-750w.png'; -import greenwoodLogo1000 from '../../assets/greenwood-logo-1000w.png'; -import greenwoodLogo1500 from '../../assets/greenwood-logo-1500w.png'; import '@evergreen-wc/eve-button'; import '@evergreen-wc/eve-container'; @@ -25,6 +20,13 @@ class Banner extends LitElement { ]; } + static get styles() { + return css` + ${unsafeCSS(buttonCss)} + ${unsafeCSS(bannerCss)} + `; + } + cycleProjectTypes() { this.currentProjectIndex = this.currentProjectIndex += 1; @@ -50,25 +52,21 @@ class Banner extends LitElement { const currentProjectType = this.projectTypes[this.currentProjectIndex]; return html` - - @@ -76,4 +74,4 @@ class Banner extends LitElement { } } -customElements.define('eve-banner', Banner); +customElements.define('app-banner', Banner); \ No newline at end of file diff --git a/www/components/banner/button.css b/www/components/banner/button.css index 5abe0814b..63c3a5d63 100644 --- a/www/components/banner/button.css +++ b/www/components/banner/button.css @@ -1,16 +1,17 @@ +/* stylelint-disable a11y/media-prefers-reduced-motion */ :host .btn { display: inline-block; - border-radius: 5px; - font-size: 1.5rem; - padding: 1rem; + border-radius: 10px; + font-size: 1.2rem; + padding: 0.75rem; color: white; - background-color: #1d337a; + background-color: #201e2e; border: 1px solid white; + transition: 0.3s; } :host .btn:hover, :host .btn:focus { - color: #020202; - background-color: #f9e7ca; - border: 1px solid #020202; -} + background-color: #425d58; + border: 1px solid white; +} \ No newline at end of file diff --git a/www/components/card/card.css b/www/components/card/card.css index 53b333946..7c53d9c7f 100644 --- a/www/components/card/card.css +++ b/www/components/card/card.css @@ -1,57 +1,16 @@ :host { - & .card { - padding: 2.5rem; - position: relative; - display: flex; - flex-direction: column; - min-width: 0; - word-wrap: break-word; - background-color: #fff; - background-clip: initial; - text-align: center; - - @media (max-width: 768px) { - padding: 0; - } - - & .body { - padding: 10px; - } - } - - & .card-xs { - margin: 0.25rem; - flex: 1 1 16.666%; - max-width: 250px; - - @media (max-width: 1024px) { - max-width: 100%; - } - } - - & .card-sm { - margin: 0.25rem; - flex: 1 1 25%; - max-width: 380px; - - @media (max-width: 1024px) { - max-width: 100%; - } - } - - & .card-md { - margin: 0.25rem; - flex: 1 1 33.333%; - max-width: 450px; - - @media (max-width: 1024px) { - max-width: 100%; - } - } - - & .card-full { - margin: 0.25rem; - flex: 1 1 33.333%; + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: initial; + text-align: center; + + & slot { + text-align: left; + font-size: 1rem; } & .card-img-top { @@ -60,8 +19,9 @@ background-position-y: center; & img { - width: 100%; + width: 200px; + height: 200px; max-width: 200px; } } -} +} \ No newline at end of file diff --git a/www/components/card/card.js b/www/components/card/card.js index d36563567..34921d230 100644 --- a/www/components/card/card.js +++ b/www/components/card/card.js @@ -1,8 +1,14 @@ -import { html, LitElement } from 'lit-element'; -import css from './card.css'; +import { css, html, LitElement, unsafeCSS } from 'lit-element'; +import cardCss from './card.css'; class Card extends LitElement { + static get styles() { + return css` + ${unsafeCSS(cardCss)} + `; + } + static get properties() { return { img: { @@ -37,18 +43,11 @@ class Card extends LitElement { render() { return html` - -
- ${this.renderImage()} - ${this.renderTitle()} -
- -
-
+ ${this.renderImage()} + ${this.renderTitle()} + `; } } -customElements.define('eve-card', Card); \ No newline at end of file +customElements.define('app-card', Card); diff --git a/www/components/footer/footer.css b/www/components/footer/footer.css index 7b61c75d3..7d4074190 100644 --- a/www/components/footer/footer.css +++ b/www/components/footer/footer.css @@ -18,4 +18,4 @@ text-decoration: none; } } -} +} \ No newline at end of file diff --git a/www/components/footer/footer.js b/www/components/footer/footer.js index f568a31f3..6f32d0eba 100644 --- a/www/components/footer/footer.js +++ b/www/components/footer/footer.js @@ -1,14 +1,19 @@ -import { html, LitElement } from 'lit-element'; -// make sure version gets bumped first when building for release -import { version } from '../../package.json'; +import { css, html, LitElement, unsafeCSS } from 'lit-element'; +import json from '../../package.json'; import footerCss from './footer.css'; class FooterComponent extends LitElement { + + static get styles() { + return css` + ${unsafeCSS(footerCss)} + `; + } + render() { + const { version } = json; + return html` -