diff --git a/.circleci/config.yml b/.circleci/config.yml
index f89bda0ccf410..59d0a86634dd2 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -155,7 +155,13 @@ jobs:
- e2e-test:
test_path: e2e-tests/gatsby-image
- e2e_tests_runtime:
+ e2e_tests_development_runtime:
+ <<: *e2e-executor
+ steps:
+ - e2e-test:
+ test_path: e2e-tests/development-runtime
+
+ e2e_tests_production_runtime:
<<: *e2e-executor
steps:
- e2e-test:
@@ -200,7 +206,9 @@ workflows:
<<: *e2e-test-workflow
- e2e_tests_gatsby-image:
<<: *e2e-test-workflow
- - e2e_tests_runtime:
+ - e2e_tests_development_runtime:
+ <<: *e2e-test-workflow
+ - e2e_tests_production_runtime:
<<: *e2e-test-workflow
- starters_publish:
filters:
diff --git a/.eslintrc.json b/.eslintrc.json
index 8505600fca219..56c0ebc6d8fc1 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -24,6 +24,7 @@
"jest": true
},
"globals": {
+ "before": true,
"spyOn": true,
"__PATH_PREFIX__": true
},
diff --git a/e2e-tests/development-runtime/.gitignore b/e2e-tests/development-runtime/.gitignore
new file mode 100644
index 0000000000000..a17a058d51437
--- /dev/null
+++ b/e2e-tests/development-runtime/.gitignore
@@ -0,0 +1,76 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Typescript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# dotenv environment variables file
+.env
+
+# gatsby files
+.cache/
+public
+
+# Mac files
+.DS_Store
+
+# Yarn
+yarn-error.log
+.pnp/
+.pnp.js
+# Yarn Integrity file
+.yarn-integrity
+
+# misc files
+cypress/screenshots
+cypress/fixtures
+cypress/videos
+
+__history__.json
diff --git a/e2e-tests/development-runtime/LICENSE b/e2e-tests/development-runtime/LICENSE
new file mode 100644
index 0000000000000..5169a5e4135e9
--- /dev/null
+++ b/e2e-tests/development-runtime/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 gatsbyjs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/e2e-tests/development-runtime/README.md b/e2e-tests/development-runtime/README.md
new file mode 100644
index 0000000000000..01a842cc769ee
--- /dev/null
+++ b/e2e-tests/development-runtime/README.md
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+ Gatsby's default starter
+
+
+Kick off your project with this default boilerplate ([live demo](https://gatsby-starter-default-demo.netlify.com/)). This barebones starter ships with the main Gatsby configuration files you might need.
+
+_Have another more specific idea? You may want to check out our vibrant collection of [official and community-created starters](https://www.gatsbyjs.org/docs/gatsby-starters/)._
+
+## π Quick start
+
+1. **Create a Gatsby site.**
+
+ Use the Gatsby CLI to create a new site, specifying the default starter.
+
+ ```sh
+ # create a new Gatsby site using the default starter
+ npx gatsby new my-default-starter
+ ```
+
+1. **Start developing.**
+
+ Navigate into your new siteβs directory and start it up.
+
+ ```sh
+ cd my-default-starter/
+ gatsby develop
+ ```
+
+1. **Open the source code and start editing!**
+
+ Your site is now running at `http://localhost:8000`!
+
+ \_Note: You'll also see a second link: `http://localhost:8000/___graphql`. This is a tool you can use to experiment with querying your data. Learn more about using this tool in the [Gatsby tutorial](https://www.gatsbyjs.org/tutorial/part-five/#introducing-graphiql).\_
+
+ Open the `my-default-starter` directory in your code editor of choice and edit `src/pages/index.js`. Save your changes and the browser will update in real time!
+
+## π§ What's inside?
+
+A quick look at the top-level files and directories you'll see in a Gatsby project.
+
+ .
+ βββ node_modules
+ βββ src
+ βββ .gitignore
+ βββ .prettierrc
+ βββ gatsby-browser.js
+ βββ gatsby-config.js
+ βββ gatsby-node.js
+ βββ gatsby-ssr.js
+ βββ LICENSE
+ βββ package-lock.json
+ βββ package.json
+ βββ README.md
+ βββ yarn.lock
+
+1. **`/node_modules`**: This directory contains all of the modules of code that your project depends on (npm packages) are automatically installed.
+
+2. **`/src`**: This directory will contain all of the code related to what you will see on the front-end of your site (what you see in the browser) such as your site header or a page template. `src` is a convention for βsource codeβ.
+
+3. **`.gitignore`**: This file tells git which files it should not track / not maintain a version history for.
+
+4. **`.prettierrc`**: This is a configuration file for [Prettier](https://prettier.io/). Prettier is a tool to help keep the formatting of your code consistent.
+
+5. **`gatsby-browser.js`**: This file is where Gatsby expects to find any usage of the [Gatsby browser APIs](https://www.gatsbyjs.org/docs/browser-apis/) (if any). These allow customization/extension of default Gatsby settings affecting the browser.
+
+6. **`gatsby-config.js`**: This is the main configuration file for a Gatsby site. This is where you can specify information about your site (metadata) like the site title and description, which Gatsby plugins youβd like to include, etc. (Check out the [config docs](https://www.gatsbyjs.org/docs/gatsby-config/) for more detail).
+
+7. **`gatsby-node.js`**: This file is where Gatsby expects to find any usage of the [Gatsby Node APIs](https://www.gatsbyjs.org/docs/node-apis/) (if any). These allow customization/extension of default Gatsby settings affecting pieces of the site build process.
+
+8. **`gatsby-ssr.js`**: This file is where Gatsby expects to find any usage of the [Gatsby server-side rendering APIs](https://www.gatsbyjs.org/docs/ssr-apis/) (if any). These allow customization of default Gatsby settings affecting server-side rendering.
+
+9. **`LICENSE`**: Gatsby is licensed under the MIT license.
+
+10. **`package-lock.json`** (See `package.json` below, first). This is an automatically generated file based on the exact versions of your npm dependencies that were installed for your project. **(You wonβt change this file directly).**
+
+11. **`package.json`**: A manifest file for Node.js projects, which includes things like metadata (the projectβs name, author, etc). This manifest is how npm knows which packages to install for your project.
+
+12. **`README.md`**: A text file containing useful reference information about your project.
+
+13. **`yarn.lock`**: [Yarn](https://yarnpkg.com/) is a package manager alternative to npm. You can use either yarn or npm, though all of the Gatsby docs reference npm. This file serves essentially the same purpose as `package-lock.json`, just for a different package management system.
+
+## π Learning Gatsby
+
+Looking for more guidance? Full documentation for Gatsby lives [on the website](https://www.gatsbyjs.org/). Here are some places to start:
+
+- **For most developers, we recommend starting with our [in-depth tutorial for creating a site with Gatsby](https://www.gatsbyjs.org/tutorial/).** It starts with zero assumptions about your level of ability and walks through every step of the process.
+
+- **To dive straight into code samples, head [to our documentation](https://www.gatsbyjs.org/docs/).** In particular, check out the _Guides_, _API Reference_, and _Advanced Tutorials_ sections in the sidebar.
+
+## π« Deploy
+
+[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/gatsbyjs/gatsby-starter-default)
diff --git a/e2e-tests/development-runtime/content/2018-12-14-hello-world.md b/e2e-tests/development-runtime/content/2018-12-14-hello-world.md
new file mode 100644
index 0000000000000..7598fd3256790
--- /dev/null
+++ b/e2e-tests/development-runtime/content/2018-12-14-hello-world.md
@@ -0,0 +1,8 @@
+---
+title: Hello World
+date: 2018-12-14
+---
+
+This is a truly meaningful blog post
+
+%SUB_TITLE%
diff --git a/e2e-tests/development-runtime/cypress.json b/e2e-tests/development-runtime/cypress.json
new file mode 100644
index 0000000000000..cd20b72e75761
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress.json
@@ -0,0 +1,4 @@
+{
+ "baseUrl": "http://localhost:8000",
+ "failOnStatusCode": false
+}
diff --git a/e2e-tests/development-runtime/cypress/integration/functionality/data-update.js b/e2e-tests/development-runtime/cypress/integration/functionality/data-update.js
new file mode 100644
index 0000000000000..a6f88b021473b
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/functionality/data-update.js
@@ -0,0 +1,31 @@
+const FILE_CONTENT = `
+---
+title: A new post
+date: ${new Date().toJSON()}
+---
+
+A brand new post
+
+`.trim()
+
+describe(`on new file`, () => {
+ beforeEach(() => {
+ cy.visit(`/`).waitForAPI(`onRouteUpdate`)
+ })
+
+ /*
+ * TODO: This seems to cause a page re-load
+ */
+ it.skip(`re-runs GraphQL queries with new file contents`, () => {
+ cy.exec(
+ `npm run update -- --file content/sample.md --file-content '${JSON.stringify(
+ FILE_CONTENT
+ )}'`
+ )
+
+ cy.get(`ul`)
+ .find(`li`)
+ .its(`length`)
+ .should(`be.gt`, 1)
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/integration/functionality/hooks.js b/e2e-tests/development-runtime/cypress/integration/functionality/hooks.js
new file mode 100644
index 0000000000000..5fb5b2074dd45
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/functionality/hooks.js
@@ -0,0 +1,27 @@
+const COUNT_ID = `count`
+
+describe(`hooks`, () => {
+ beforeEach(() => {
+ cy.visit(`/hooks`).waitForAPI(`onRouteUpdate`)
+ })
+
+ it(`displays initial state`, () => {
+ cy.getTestElement(COUNT_ID)
+ .invoke(`text`)
+ .should(`eq`, `0`)
+ })
+
+ it(`can update local state`, () => {
+ cy.getTestElement(`increment`).click()
+
+ cy.getTestElement(COUNT_ID)
+ .invoke(`text`)
+ .should(`eq`, `1`)
+
+ cy.getTestElement(`decrement`).click()
+
+ cy.getTestElement(COUNT_ID)
+ .invoke(`text`)
+ .should(`eq`, `0`)
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/integration/functionality/page-arrow-functions.js b/e2e-tests/development-runtime/cypress/integration/functionality/page-arrow-functions.js
new file mode 100644
index 0000000000000..2704e57642cbf
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/functionality/page-arrow-functions.js
@@ -0,0 +1,21 @@
+describe(`anonymous arrow function pages`, () => {
+ beforeEach(() => {
+ cy.visit(`/anonymous-arrow`).waitForAPI(`onRouteUpdate`)
+ })
+
+ it(`displays arrow function component correctly`, () => {
+ cy.getTestElement(`title`)
+ .invoke(`text`)
+ .should(`contain`, `Anonymous Arrow Function`)
+ })
+
+ it(`updates page on navigation`, () => {
+ cy.getTestElement(`link`)
+ .click()
+ .waitForAPI(`onRouteUpdate`)
+
+ cy.getTestElement(`title`)
+ .invoke(`text`)
+ .should(`contain`, `Two`)
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/integration/functionality/route-update.js b/e2e-tests/development-runtime/cypress/integration/functionality/route-update.js
new file mode 100644
index 0000000000000..8da582a9f5f0c
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/functionality/route-update.js
@@ -0,0 +1,17 @@
+describe(`hooks`, () => {
+ beforeEach(() => {
+ cy.visit(`/`).waitForAPI(`onRouteUpdate`)
+ })
+
+ it(`registers one route update on initial route`, () => {
+ cy.lifecycleCallCount(`onRouteUpdate`).should(`eq`, 1)
+ })
+
+ it(`registers new route update on page navigation`, () => {
+ cy.getTestElement(`page-two`)
+ .click()
+ .waitForAPI(`onRouteUpdate`)
+
+ cy.lifecycleCallCount(`onRouteUpdate`).should(`eq`, 2)
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/integration/hot-reloading/arrow-functions.js b/e2e-tests/development-runtime/cypress/integration/hot-reloading/arrow-functions.js
new file mode 100644
index 0000000000000..1742e8a2a34a4
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/hot-reloading/arrow-functions.js
@@ -0,0 +1,30 @@
+const IDS = {
+ title: `title`,
+ subTitle: `sub-title`,
+}
+
+describe(`hot-reloading anonymous arrow functions`, () => {
+ beforeEach(() => {
+ cy.visit(`/arrows`).waitForAPI(`onRouteUpdate`)
+ })
+ it(`displays placeholders on launch`, () => {
+ cy.getTestElement(IDS.title)
+ .invoke(`text`)
+ .should(`contain`, `%TITLE%`)
+
+ cy.getTestElement(IDS.subTitle)
+ .invoke(`text`)
+ .should(`contain`, `%SUB_TITLE%`)
+ })
+
+ it(`upates on change`, () => {
+ const text = `The title`
+ cy.exec(
+ `npm run update -- --file src/components/title.js --replacements "TITLE:${text}"`
+ )
+
+ cy.getTestElement(IDS.title)
+ .invoke(`text`)
+ .should(`eq`, text)
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/integration/hot-reloading/class-component.js b/e2e-tests/development-runtime/cypress/integration/hot-reloading/class-component.js
new file mode 100644
index 0000000000000..7c50c01a1ea31
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/hot-reloading/class-component.js
@@ -0,0 +1,34 @@
+const TEST_ID = `class-component`
+
+describe(`reloading class component`, () => {
+ beforeEach(() => {
+ cy.visit(`/`).waitForAPI(`onRouteUpdate`)
+ })
+ it(`displays placeholder on launch`, () => {
+ cy.getTestElement(TEST_ID)
+ .invoke(`text`)
+ .should(`contain`, `%CLASS_COMPONENT%`)
+ })
+
+ it(`updates placeholder and hot reloads`, () => {
+ const text = `class component`
+ cy.exec(
+ `npm run update -- --file src/components/class-component.js --replacements "CLASS_COMPONENT:${text}"`
+ )
+
+ cy.getTestElement(TEST_ID)
+ .invoke(`text`)
+ .should(`contain`, text)
+ })
+
+ it(`updates state and hot reloads`, () => {
+ const value = `custom`
+ cy.exec(
+ `npm run update -- --file src/components/class-component.js --replacements "CUSTOM_STATE:${value}"`
+ )
+
+ cy.getTestElement(`stateful-${TEST_ID}`)
+ .invoke(`text`)
+ .should(`eq`, `Custom Message`)
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/integration/hot-reloading/hooks.js b/e2e-tests/development-runtime/cypress/integration/hot-reloading/hooks.js
new file mode 100644
index 0000000000000..b471394446704
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/hot-reloading/hooks.js
@@ -0,0 +1,20 @@
+const COUNT_ID = `count`
+
+describe(`hot-reloading hooks`, () => {
+ beforeEach(() => {
+ cy.visit(`/hooks`).waitForAPI(`onRouteUpdate`)
+ })
+
+ it.skip(`can update component`, () => {
+ const amount = 100
+ cy.exec(
+ `npm run update -- --file src/pages/hooks.js --replacements "count + 1:count + ${amount}" --exact`
+ )
+
+ cy.getTestElement(`increment`).click()
+
+ cy.getTestElement(COUNT_ID)
+ .invoke(`text`)
+ .should(`eq`, `${amount}`)
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/integration/hot-reloading/new-file.js b/e2e-tests/development-runtime/cypress/integration/hot-reloading/new-file.js
new file mode 100644
index 0000000000000..6a06ef4a68311
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/hot-reloading/new-file.js
@@ -0,0 +1,30 @@
+describe(`hot reloading new page component`, () => {
+ before(() => {
+ cy.exec(`npm run update -- --file src/pages/sample.js`)
+ })
+
+ beforeEach(() => {
+ cy.visit(`/`).waitForAPI(`onRouteUpdate`)
+ })
+
+ it(`can navigate to new page`, () => {
+ cy.visit(`/sample`).waitForAPI(`onRouteUpdate`)
+
+ cy.getTestElement(`message`)
+ .invoke(`text`)
+ .should(`contain`, `Hello`)
+ })
+
+ it(`can hot reload a new page file`, () => {
+ const text = `World`
+ cy.exec(
+ `npm run update -- --file src/pages/sample.js --replacements "REPLACEMENT:${text}"`
+ )
+
+ cy.visit(`/sample`).waitForAPI(`onRouteUpdate`)
+
+ cy.getTestElement(`message`)
+ .invoke(`text`)
+ .should(`eq`, `Hello ${text}`)
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/integration/hot-reloading/non-js-file.js b/e2e-tests/development-runtime/cypress/integration/hot-reloading/non-js-file.js
new file mode 100644
index 0000000000000..e8abc8d5b56b0
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/hot-reloading/non-js-file.js
@@ -0,0 +1,25 @@
+const TEMPLATE = `SUB_TITLE`
+const TEST_ID = `sub-title`
+
+describe(`hot reloading non-js file`, () => {
+ beforeEach(() => {
+ cy.visit(`/2018-12-14-hello-world/`).waitForAPI(`onRouteUpdate`)
+ })
+
+ it(`displays placeholder content on launch`, () => {
+ cy.getTestElement(TEST_ID)
+ .invoke(`text`)
+ .should(`contain`, TEMPLATE)
+ })
+
+ it.skip(`hot reloads with new content`, () => {
+ const message = `This is a sub-title`
+ cy.exec(
+ `npm run update -- --file content/2018-12-14-hello-world.md --replacements "${TEMPLATE}:${message}"`
+ )
+
+ cy.getTestElement(TEST_ID)
+ .invoke(`text`)
+ .should(`eq`, message)
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/integration/hot-reloading/page-component.js b/e2e-tests/development-runtime/cypress/integration/hot-reloading/page-component.js
new file mode 100644
index 0000000000000..d827815dd438b
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/hot-reloading/page-component.js
@@ -0,0 +1,23 @@
+const TEST_ID = `page-component`
+
+describe(`hot reloading page component`, () => {
+ beforeEach(() => {
+ cy.visit(`/`).waitForAPI(`onRouteUpdate`)
+ })
+ it(`displays placeholder content on launch`, () => {
+ cy.getTestElement(TEST_ID)
+ .invoke(`text`)
+ .should(`contain`, `%GATSBY_SITE%`)
+ })
+
+ it(`hot reloads with new content`, () => {
+ const text = `Gatsby site`
+ cy.exec(
+ `npm run update -- --file src/pages/index.js --replacements "GATSBY_SITE:${text}"`
+ )
+
+ cy.getTestElement(TEST_ID)
+ .invoke(`text`)
+ .should(`contain`, text)
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/integration/hot-reloading/template-component.js b/e2e-tests/development-runtime/cypress/integration/hot-reloading/template-component.js
new file mode 100644
index 0000000000000..1d23d188dad03
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/hot-reloading/template-component.js
@@ -0,0 +1,24 @@
+const TEST_ID = `message`
+const TEMPLATE = `MESSAGE`
+
+describe(`hot reloading template component`, () => {
+ beforeEach(() => {
+ cy.visit(`/2018-12-14-hello-world/`).waitForAPI(`onRouteUpdate`)
+ })
+ it(`displays placeholder content on launch`, () => {
+ cy.getTestElement(TEST_ID)
+ .invoke(`text`)
+ .should(`contain`, TEMPLATE)
+ })
+
+ it(`hot reloads with new content`, () => {
+ const message = `World`
+ cy.exec(
+ `npm run update -- --file src/templates/blog-post.js --replacements "${TEMPLATE}:${message}"`
+ )
+
+ cy.getTestElement(TEST_ID)
+ .invoke(`text`)
+ .should(`eq`, `Hello ${message}`)
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/integration/navigation/linking.js b/e2e-tests/development-runtime/cypress/integration/navigation/linking.js
new file mode 100644
index 0000000000000..7fcddc06c9b89
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/navigation/linking.js
@@ -0,0 +1,40 @@
+describe(`navigation`, () => {
+ beforeEach(() => {
+ cy.visit(`/`).waitForAPI(`onRouteUpdate`)
+ })
+
+ it(`displays content from other pages`, () => {
+ cy.visit(`/page-2`).waitForAPI(`onRouteUpdate`)
+
+ cy.getTestElement(`page-2-message`)
+ .invoke(`text`)
+ .should(`equal`, `Hi from the second page`)
+ })
+
+ it(`re-routes on link click`, () => {
+ cy.getTestElement(`page-two`).click()
+
+ cy.location(`pathname`).should(`equal`, `/page-2/`)
+ })
+
+ it(`can navigate to and from pages`, () => {
+ cy.getTestElement(`page-two`).click()
+
+ cy.getTestElement(`back-button`).click()
+
+ cy.location(`pathname`).should(`equal`, `/`)
+ })
+
+ /*
+ * Browser API onRouteUpdate is not triggered on a 404
+ */
+ it.skip(`displays 404 page on broken link`, () => {
+ cy.getTestElement(`broken-link`)
+ .click()
+ .waitForAPI(`onRouteUpdate`)
+
+ cy.get(`h1`)
+ .invoke(`text`)
+ .should(`eq`, `Gatsby.js development 404 page`)
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/integration/page-not-found/404.js b/e2e-tests/development-runtime/cypress/integration/page-not-found/404.js
new file mode 100644
index 0000000000000..02d017088360a
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/page-not-found/404.js
@@ -0,0 +1,23 @@
+describe(`page not found`, () => {
+ beforeEach(() => {
+ cy.visit(`/__404__`)
+ })
+ it(`should display message `, () => {
+ cy.get(`h1`)
+ .invoke(`text`)
+ .should(`eq`, `Gatsby.js development 404 page`)
+ })
+ it.skip(`can preview 404 page`, () => {
+ cy.get(`button`).click()
+
+ cy.get(`h1`)
+ .invoke(`text`)
+ .should(`eq`, `NOT FOUND`)
+ })
+ it(`shows page listing`, () => {
+ cy.get(`ul`)
+ .find(`li`)
+ .its(`length`)
+ .should(`be.gte`, 4)
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/plugins/index.js b/e2e-tests/development-runtime/cypress/plugins/index.js
new file mode 100644
index 0000000000000..fd170fba6912b
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/plugins/index.js
@@ -0,0 +1,17 @@
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+module.exports = (on, config) => {
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+}
diff --git a/e2e-tests/development-runtime/cypress/support/commands.js b/e2e-tests/development-runtime/cypress/support/commands.js
new file mode 100644
index 0000000000000..62bedced4b6be
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/support/commands.js
@@ -0,0 +1,10 @@
+Cypress.Commands.add(`lifecycleCallCount`, action =>
+ cy
+ .window()
+ .then(
+ win =>
+ win.___PageComponentLifecycleCallsLog.filter(
+ entry => entry.action === action
+ ).length
+ )
+)
diff --git a/e2e-tests/development-runtime/cypress/support/index.js b/e2e-tests/development-runtime/cypress/support/index.js
new file mode 100644
index 0000000000000..13970dd45babb
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/support/index.js
@@ -0,0 +1,2 @@
+import "../../../../packages/cypress-gatsby"
+import "./commands"
diff --git a/e2e-tests/development-runtime/gatsby-browser.js b/e2e-tests/development-runtime/gatsby-browser.js
new file mode 100644
index 0000000000000..8fc6d2c5b2850
--- /dev/null
+++ b/e2e-tests/development-runtime/gatsby-browser.js
@@ -0,0 +1,22 @@
+if (typeof window !== `undefined`) {
+ window.___PageComponentLifecycleCallsLog = []
+}
+
+const addLogEntry = (action, location) => {
+ window.___PageComponentLifecycleCallsLog.push({
+ action,
+ pathname: location.pathname,
+ })
+}
+
+exports.onPreRouteUpdate = ({ location }) => {
+ addLogEntry(`onPreRouteUpdate`, location)
+}
+
+exports.onRouteUpdate = ({ location }) => {
+ addLogEntry(`onRouteUpdate`, location)
+}
+
+exports.onPrefetchPathname = ({ pathname }) => {
+ addLogEntry(`onPrefetchPathname`, pathname)
+}
diff --git a/e2e-tests/development-runtime/gatsby-config.js b/e2e-tests/development-runtime/gatsby-config.js
new file mode 100644
index 0000000000000..e229721485cea
--- /dev/null
+++ b/e2e-tests/development-runtime/gatsby-config.js
@@ -0,0 +1,47 @@
+module.exports = {
+ siteMetadata: {
+ title: `Gatsby Default Starter`,
+ description: `Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`,
+ author: `@gatsbyjs`,
+ },
+ plugins: [
+ `gatsby-plugin-react-helmet`,
+ {
+ resolve: `gatsby-source-filesystem`,
+ options: {
+ name: `images`,
+ path: `${__dirname}/src/images`,
+ },
+ },
+ {
+ resolve: `gatsby-source-filesystem`,
+ options: {
+ name: `blog`,
+ path: `${__dirname}/content`,
+ },
+ },
+ `gatsby-transformer-sharp`,
+ {
+ resolve: `gatsby-transformer-remark`,
+ options: {
+ plugins: [],
+ },
+ },
+ `gatsby-plugin-sharp`,
+ {
+ resolve: `gatsby-plugin-manifest`,
+ options: {
+ name: `gatsby-starter-default`,
+ short_name: `starter`,
+ start_url: `/`,
+ background_color: `#663399`,
+ theme_color: `#663399`,
+ display: `minimal-ui`,
+ icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site.
+ },
+ },
+ // this (optional) plugin enables Progressive Web App + Offline functionality
+ // To learn more, visit: https://gatsby.app/offline
+ // 'gatsby-plugin-offline',
+ ],
+}
diff --git a/e2e-tests/development-runtime/gatsby-node.js b/e2e-tests/development-runtime/gatsby-node.js
new file mode 100644
index 0000000000000..63f0550f84d0f
--- /dev/null
+++ b/e2e-tests/development-runtime/gatsby-node.js
@@ -0,0 +1,60 @@
+const path = require(`path`)
+const { createFilePath } = require(`gatsby-source-filesystem`)
+
+exports.onCreateNode = function onCreateNode({
+ actions: { createNodeField },
+ node,
+ getNode,
+}) {
+ switch (node.internal.type) {
+ case `MarkdownRemark`: {
+ const slug = createFilePath({
+ node,
+ getNode,
+ })
+
+ createNodeField({
+ name: `slug`,
+ value: slug,
+ node,
+ })
+ break
+ }
+
+ default: {
+ break
+ }
+ }
+}
+
+exports.createPages = async function createPages({
+ actions: { createPage },
+ graphql,
+}) {
+ const { data } = await graphql(`
+ {
+ posts: allMarkdownRemark {
+ edges {
+ node {
+ fields {
+ slug
+ }
+ }
+ }
+ }
+ }
+ `)
+
+ const blogPostTemplate = path.resolve(`src/templates/blog-post.js`)
+
+ data.posts.edges.forEach(({ node }) => {
+ const { slug } = node.fields
+ createPage({
+ path: slug,
+ component: blogPostTemplate,
+ context: {
+ slug,
+ },
+ })
+ })
+}
diff --git a/e2e-tests/development-runtime/gatsby-ssr.js b/e2e-tests/development-runtime/gatsby-ssr.js
new file mode 100644
index 0000000000000..b17b8fc19d659
--- /dev/null
+++ b/e2e-tests/development-runtime/gatsby-ssr.js
@@ -0,0 +1,7 @@
+/**
+ * Implement Gatsby's SSR (Server Side Rendering) APIs in this file.
+ *
+ * See: https://www.gatsbyjs.org/docs/ssr-apis/
+ */
+
+// You can delete this file if you're not using it
diff --git a/e2e-tests/development-runtime/package.json b/e2e-tests/development-runtime/package.json
new file mode 100644
index 0000000000000..f3dc3285df795
--- /dev/null
+++ b/e2e-tests/development-runtime/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "e2e-development-runtime",
+ "description": "An e2e-test validating development runtime functionality",
+ "version": "1.0.0",
+ "author": "Dustin Schau ",
+ "dependencies": {
+ "gatsby": "^2.0.71",
+ "gatsby-image": "^2.0.20",
+ "gatsby-plugin-manifest": "^2.0.9",
+ "gatsby-plugin-offline": "^2.0.20",
+ "gatsby-plugin-react-helmet": "^3.0.2",
+ "gatsby-plugin-sharp": "^2.0.14",
+ "gatsby-source-filesystem": "^2.0.12",
+ "gatsby-transformer-remark": "^2.1.17",
+ "gatsby-transformer-sharp": "^2.1.8",
+ "prop-types": "^15.6.2",
+ "react": "16.7.0-alpha.2",
+ "react-dom": "16.7.0-alpha.2",
+ "react-helmet": "^5.2.0"
+ },
+ "keywords": [
+ "gatsby",
+ "gatsby-e2e"
+ ],
+ "license": "MIT",
+ "scripts": {
+ "build": "gatsby build",
+ "develop": "gatsby develop",
+ "serve": "gatsby serve",
+ "start": "npm run develop",
+ "format": "prettier --write \"src/**/*.js\"",
+ "test": "npm run start-server-and-test || npm run reset",
+ "posttest": "npm run reset",
+ "reset": "node scripts/reset.js",
+ "update": "node scripts/update.js",
+ "start-server-and-test": "start-server-and-test develop http://localhost:8000 cy:run",
+ "cy:open": "cypress open",
+ "cy:run": "cypress run --browser chrome"
+ },
+ "devDependencies": {
+ "cypress": "^3.1.3",
+ "fs-extra": "^7.0.1",
+ "prettier": "^1.15.2",
+ "start-server-and-test": "^1.7.11",
+ "yargs": "^12.0.5"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/gatsbyjs/gatsby"
+ }
+}
diff --git a/e2e-tests/development-runtime/scripts/history.js b/e2e-tests/development-runtime/scripts/history.js
new file mode 100644
index 0000000000000..2b6996deffe35
--- /dev/null
+++ b/e2e-tests/development-runtime/scripts/history.js
@@ -0,0 +1,25 @@
+const fs = require(`fs-extra`)
+
+const HISTORY_FILE = `__history__.json`
+
+exports.__HISTORY_FILE__ = HISTORY_FILE
+
+exports.getHistory = async (file = HISTORY_FILE) => {
+ try {
+ const contents = await fs
+ .readFile(file, `utf8`)
+ .then(contents => JSON.parse(contents))
+
+ return new Map(contents)
+ } catch (e) {
+ return new Map()
+ }
+}
+
+exports.writeHistory = async (contents, file = HISTORY_FILE) => {
+ try {
+ await fs.writeFile(file, JSON.stringify([...contents]), `utf8`)
+ } catch (e) {
+ console.error(e)
+ }
+}
diff --git a/e2e-tests/development-runtime/scripts/reset.js b/e2e-tests/development-runtime/scripts/reset.js
new file mode 100644
index 0000000000000..9aabb5ed7bb82
--- /dev/null
+++ b/e2e-tests/development-runtime/scripts/reset.js
@@ -0,0 +1,21 @@
+const fs = require(`fs-extra`)
+const path = require(`path`)
+
+const { __HISTORY_FILE__, getHistory } = require(`./history`)
+
+async function reset() {
+ const history = await getHistory()
+
+ await Promise.all(
+ Array.from(history).map(([filePath, value]) => {
+ if (typeof value === `string`) {
+ return fs.writeFile(path.resolve(filePath), value, `utf8`)
+ }
+ return fs.remove(path.resolve(filePath))
+ })
+ )
+
+ await fs.remove(__HISTORY_FILE__)
+}
+
+reset()
diff --git a/e2e-tests/development-runtime/scripts/update.js b/e2e-tests/development-runtime/scripts/update.js
new file mode 100644
index 0000000000000..589b9f98020bc
--- /dev/null
+++ b/e2e-tests/development-runtime/scripts/update.js
@@ -0,0 +1,72 @@
+const fs = require(`fs-extra`)
+const path = require(`path`)
+const yargs = require(`yargs`)
+
+const { getHistory, writeHistory } = require(`./history`)
+
+const args = yargs
+ .option(`file`, {
+ demand: true,
+ type: `string`,
+ })
+ .option(`replacements`, {
+ default: [],
+ type: `array`,
+ })
+ .option(`exact`, {
+ default: false,
+ type: `boolean`,
+ })
+ .option(`fileContent`, {
+ default: JSON.stringify(
+ `
+ import React from 'react';
+
+ import Layout from '../components/layout';
+
+ export default function SomeComponent() {
+ return (
+
+ Hello %REPLACEMENT%
+
+ )
+ }
+ `
+ ).trim(),
+ type: `string`,
+ }).argv
+
+async function update() {
+ const history = await getHistory()
+
+ const { file: fileArg, replacements } = args
+ const filePath = path.resolve(fileArg)
+ let exists = true
+ if (!fs.existsSync(filePath)) {
+ exists = false
+ await fs.writeFile(
+ filePath,
+ JSON.parse(args.fileContent).replace(/\+n/g, `\n`),
+ `utf8`
+ )
+ }
+ let file = await fs.readFile(filePath, `utf8`)
+
+ if (!history.has(filePath)) {
+ history.set(filePath, exists ? file : false)
+ }
+
+ const contents = replacements.reduce((replaced, pair) => {
+ const [key, value] = pair.split(`:`)
+ return replaced.replace(
+ args.exact ? key : new RegExp(`%${key}%`, `g`),
+ value
+ )
+ }, file)
+
+ await fs.writeFile(filePath, contents, `utf8`)
+
+ await writeHistory(history)
+}
+
+update()
diff --git a/e2e-tests/development-runtime/src/components/class-component.js b/e2e-tests/development-runtime/src/components/class-component.js
new file mode 100644
index 0000000000000..0c3553b15237b
--- /dev/null
+++ b/e2e-tests/development-runtime/src/components/class-component.js
@@ -0,0 +1,17 @@
+import React, { Component } from "react"
+
+class ClassComponent extends Component {
+ state = {
+ custom: true,
+ }
+
+ render() {
+ const custom = this.state[`%CUSTOM_STATE%`]
+ if (custom) {
+ return Custom Message
+ }
+ return I am a %CLASS_COMPONENT%
+ }
+}
+
+export default ClassComponent
diff --git a/e2e-tests/development-runtime/src/components/header.js b/e2e-tests/development-runtime/src/components/header.js
new file mode 100644
index 0000000000000..22f4e99c50e26
--- /dev/null
+++ b/e2e-tests/development-runtime/src/components/header.js
@@ -0,0 +1,42 @@
+import { Link } from "gatsby"
+import PropTypes from "prop-types"
+import React from "react"
+
+const Header = ({ siteTitle }) => (
+
+
+
+
+ {siteTitle}
+
+
+
+
+)
+
+Header.propTypes = {
+ siteTitle: PropTypes.string,
+}
+
+Header.defaultProps = {
+ siteTitle: ``,
+}
+
+export default Header
diff --git a/e2e-tests/development-runtime/src/components/image.js b/e2e-tests/development-runtime/src/components/image.js
new file mode 100644
index 0000000000000..2d068b89a89c9
--- /dev/null
+++ b/e2e-tests/development-runtime/src/components/image.js
@@ -0,0 +1,32 @@
+import React from "react"
+import { StaticQuery, graphql } from "gatsby"
+import Img from "gatsby-image"
+
+/*
+ * This component is built using `gatsby-image` to automatically serve optimized
+ * images with lazy loading and reduced file sizes. The image is loaded using a
+ * `StaticQuery`, which allows us to load the image from directly within this
+ * component, rather than having to pass the image data down from pages.
+ *
+ * For more information, see the docs:
+ * - `gatsby-image`: https://gatsby.app/gatsby-image
+ * - `StaticQuery`: https://gatsby.app/staticquery
+ */
+
+const Image = () => (
+ }
+ />
+)
+export default Image
diff --git a/e2e-tests/development-runtime/src/components/layout.css b/e2e-tests/development-runtime/src/components/layout.css
new file mode 100644
index 0000000000000..9e53afc65d0a4
--- /dev/null
+++ b/e2e-tests/development-runtime/src/components/layout.css
@@ -0,0 +1,624 @@
+html {
+ font-family: sans-serif;
+ -ms-text-size-adjust: 100%;
+ -webkit-text-size-adjust: 100%;
+}
+body {
+ margin: 0;
+}
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+main,
+menu,
+nav,
+section,
+summary {
+ display: block;
+}
+audio,
+canvas,
+progress,
+video {
+ display: inline-block;
+}
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+progress {
+ vertical-align: baseline;
+}
+[hidden],
+template {
+ display: none;
+}
+a {
+ background-color: transparent;
+ -webkit-text-decoration-skip: objects;
+}
+a:active,
+a:hover {
+ outline-width: 0;
+}
+abbr[title] {
+ border-bottom: none;
+ text-decoration: underline;
+ text-decoration: underline dotted;
+}
+b,
+strong {
+ font-weight: inherit;
+ font-weight: bolder;
+}
+dfn {
+ font-style: italic;
+}
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+mark {
+ background-color: #ff0;
+ color: #000;
+}
+small {
+ font-size: 80%;
+}
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+sub {
+ bottom: -0.25em;
+}
+sup {
+ top: -0.5em;
+}
+img {
+ border-style: none;
+}
+svg:not(:root) {
+ overflow: hidden;
+}
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+figure {
+ margin: 1em 40px;
+}
+hr {
+ box-sizing: content-box;
+ height: 0;
+ overflow: visible;
+}
+button,
+input,
+optgroup,
+select,
+textarea {
+ font: inherit;
+ margin: 0;
+}
+optgroup {
+ font-weight: 700;
+}
+button,
+input {
+ overflow: visible;
+}
+button,
+select {
+ text-transform: none;
+}
+[type="reset"],
+[type="submit"],
+button,
+html [type="button"] {
+ -webkit-appearance: button;
+}
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner,
+button::-moz-focus-inner {
+ border-style: none;
+ padding: 0;
+}
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring,
+button:-moz-focusring {
+ outline: 1px dotted ButtonText;
+}
+fieldset {
+ border: 1px solid silver;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+legend {
+ box-sizing: border-box;
+ color: inherit;
+ display: table;
+ max-width: 100%;
+ padding: 0;
+ white-space: normal;
+}
+textarea {
+ overflow: auto;
+}
+[type="checkbox"],
+[type="radio"] {
+ box-sizing: border-box;
+ padding: 0;
+}
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+[type="search"] {
+ -webkit-appearance: textfield;
+ outline-offset: -2px;
+}
+[type="search"]::-webkit-search-cancel-button,
+[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+::-webkit-input-placeholder {
+ color: inherit;
+ opacity: 0.54;
+}
+::-webkit-file-upload-button {
+ -webkit-appearance: button;
+ font: inherit;
+}
+html {
+ font: 112.5%/1.45em georgia, serif;
+ box-sizing: border-box;
+ overflow-y: scroll;
+}
+* {
+ box-sizing: inherit;
+}
+*:before {
+ box-sizing: inherit;
+}
+*:after {
+ box-sizing: inherit;
+}
+body {
+ color: hsla(0, 0%, 0%, 0.8);
+ font-family: georgia, serif;
+ font-weight: normal;
+ word-wrap: break-word;
+ font-kerning: normal;
+ -moz-font-feature-settings: "kern", "liga", "clig", "calt";
+ -ms-font-feature-settings: "kern", "liga", "clig", "calt";
+ -webkit-font-feature-settings: "kern", "liga", "clig", "calt";
+ font-feature-settings: "kern", "liga", "clig", "calt";
+}
+img {
+ max-width: 100%;
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+}
+h1 {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+ color: inherit;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+ Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+ font-weight: bold;
+ text-rendering: optimizeLegibility;
+ font-size: 2.25rem;
+ line-height: 1.1;
+}
+h2 {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+ color: inherit;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+ Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+ font-weight: bold;
+ text-rendering: optimizeLegibility;
+ font-size: 1.62671rem;
+ line-height: 1.1;
+}
+h3 {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+ color: inherit;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+ Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+ font-weight: bold;
+ text-rendering: optimizeLegibility;
+ font-size: 1.38316rem;
+ line-height: 1.1;
+}
+h4 {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+ color: inherit;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+ Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+ font-weight: bold;
+ text-rendering: optimizeLegibility;
+ font-size: 1rem;
+ line-height: 1.1;
+}
+h5 {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+ color: inherit;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+ Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+ font-weight: bold;
+ text-rendering: optimizeLegibility;
+ font-size: 0.85028rem;
+ line-height: 1.1;
+}
+h6 {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+ color: inherit;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+ Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+ font-weight: bold;
+ text-rendering: optimizeLegibility;
+ font-size: 0.78405rem;
+ line-height: 1.1;
+}
+hgroup {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+}
+ul {
+ margin-left: 1.45rem;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+ list-style-position: outside;
+ list-style-image: none;
+}
+ol {
+ margin-left: 1.45rem;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+ list-style-position: outside;
+ list-style-image: none;
+}
+dl {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+}
+dd {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+}
+p {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+}
+figure {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+}
+pre {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+ font-size: 0.85rem;
+ line-height: 1.42;
+ background: hsla(0, 0%, 0%, 0.04);
+ border-radius: 3px;
+ overflow: auto;
+ word-wrap: normal;
+ padding: 1.45rem;
+}
+table {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+ font-size: 1rem;
+ line-height: 1.45rem;
+ border-collapse: collapse;
+ width: 100%;
+}
+fieldset {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+}
+blockquote {
+ margin-left: 1.45rem;
+ margin-right: 1.45rem;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+}
+form {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+}
+noscript {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+}
+iframe {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+}
+hr {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: calc(1.45rem - 1px);
+ background: hsla(0, 0%, 0%, 0.2);
+ border: none;
+ height: 1px;
+}
+address {
+ margin-left: 0;
+ margin-right: 0;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ margin-bottom: 1.45rem;
+}
+b {
+ font-weight: bold;
+}
+strong {
+ font-weight: bold;
+}
+dt {
+ font-weight: bold;
+}
+th {
+ font-weight: bold;
+}
+li {
+ margin-bottom: calc(1.45rem / 2);
+}
+ol li {
+ padding-left: 0;
+}
+ul li {
+ padding-left: 0;
+}
+li > ol {
+ margin-left: 1.45rem;
+ margin-bottom: calc(1.45rem / 2);
+ margin-top: calc(1.45rem / 2);
+}
+li > ul {
+ margin-left: 1.45rem;
+ margin-bottom: calc(1.45rem / 2);
+ margin-top: calc(1.45rem / 2);
+}
+blockquote *:last-child {
+ margin-bottom: 0;
+}
+li *:last-child {
+ margin-bottom: 0;
+}
+p *:last-child {
+ margin-bottom: 0;
+}
+li > p {
+ margin-bottom: calc(1.45rem / 2);
+}
+code {
+ font-size: 0.85rem;
+ line-height: 1.45rem;
+}
+kbd {
+ font-size: 0.85rem;
+ line-height: 1.45rem;
+}
+samp {
+ font-size: 0.85rem;
+ line-height: 1.45rem;
+}
+abbr {
+ border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
+ cursor: help;
+}
+acronym {
+ border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
+ cursor: help;
+}
+abbr[title] {
+ border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
+ cursor: help;
+ text-decoration: none;
+}
+thead {
+ text-align: left;
+}
+td,
+th {
+ text-align: left;
+ border-bottom: 1px solid hsla(0, 0%, 0%, 0.12);
+ font-feature-settings: "tnum";
+ -moz-font-feature-settings: "tnum";
+ -ms-font-feature-settings: "tnum";
+ -webkit-font-feature-settings: "tnum";
+ padding-left: 0.96667rem;
+ padding-right: 0.96667rem;
+ padding-top: 0.725rem;
+ padding-bottom: calc(0.725rem - 1px);
+}
+th:first-child,
+td:first-child {
+ padding-left: 0;
+}
+th:last-child,
+td:last-child {
+ padding-right: 0;
+}
+tt,
+code {
+ background-color: hsla(0, 0%, 0%, 0.04);
+ border-radius: 3px;
+ font-family: "SFMono-Regular", Consolas, "Roboto Mono", "Droid Sans Mono",
+ "Liberation Mono", Menlo, Courier, monospace;
+ padding: 0;
+ padding-top: 0.2em;
+ padding-bottom: 0.2em;
+}
+pre code {
+ background: none;
+ line-height: 1.42;
+}
+code:before,
+code:after,
+tt:before,
+tt:after {
+ letter-spacing: -0.2em;
+ content: " ";
+}
+pre code:before,
+pre code:after,
+pre tt:before,
+pre tt:after {
+ content: "";
+}
+@media only screen and (max-width: 480px) {
+ html {
+ font-size: 100%;
+ }
+}
diff --git a/e2e-tests/development-runtime/src/components/layout.js b/e2e-tests/development-runtime/src/components/layout.js
new file mode 100644
index 0000000000000..ce4f8f7938738
--- /dev/null
+++ b/e2e-tests/development-runtime/src/components/layout.js
@@ -0,0 +1,41 @@
+import React from "react"
+import PropTypes from "prop-types"
+import { StaticQuery, graphql } from "gatsby"
+
+import Header from "./header"
+import "./layout.css"
+
+const Layout = ({ children }) => (
+ (
+ <>
+
+
+ {children}
+
+ >
+ )}
+ />
+)
+
+Layout.propTypes = {
+ children: PropTypes.node.isRequired,
+}
+
+export default Layout
diff --git a/e2e-tests/development-runtime/src/components/seo.js b/e2e-tests/development-runtime/src/components/seo.js
new file mode 100644
index 0000000000000..284672d55824e
--- /dev/null
+++ b/e2e-tests/development-runtime/src/components/seo.js
@@ -0,0 +1,96 @@
+import React from "react"
+import PropTypes from "prop-types"
+import Helmet from "react-helmet"
+import { StaticQuery, graphql } from "gatsby"
+
+function SEO({ description, lang, meta, keywords, title }) {
+ return (
+ {
+ const metaDescription =
+ description || data.site.siteMetadata.description
+ return (
+ 0
+ ? {
+ name: `keywords`,
+ content: keywords.join(`, `),
+ }
+ : []
+ )
+ .concat(meta)}
+ />
+ )
+ }}
+ />
+ )
+}
+
+SEO.defaultProps = {
+ lang: `en`,
+ meta: [],
+ keywords: [],
+}
+
+SEO.propTypes = {
+ description: PropTypes.string,
+ lang: PropTypes.string,
+ meta: PropTypes.array,
+ keywords: PropTypes.arrayOf(PropTypes.string),
+ title: PropTypes.string.isRequired,
+}
+
+export default SEO
+
+const detailsQuery = graphql`
+ query DefaultSEOQuery {
+ site {
+ siteMetadata {
+ title
+ description
+ author
+ }
+ }
+ }
+`
diff --git a/e2e-tests/development-runtime/src/components/sub-title.js b/e2e-tests/development-runtime/src/components/sub-title.js
new file mode 100644
index 0000000000000..2645ac9cebe8b
--- /dev/null
+++ b/e2e-tests/development-runtime/src/components/sub-title.js
@@ -0,0 +1,3 @@
+import React from "react"
+
+export default () => {`%SUB_TITLE%`}
diff --git a/e2e-tests/development-runtime/src/components/title.js b/e2e-tests/development-runtime/src/components/title.js
new file mode 100644
index 0000000000000..3733199c9f939
--- /dev/null
+++ b/e2e-tests/development-runtime/src/components/title.js
@@ -0,0 +1,3 @@
+import React from "react"
+
+export default () => {`%TITLE%`}
diff --git a/e2e-tests/development-runtime/src/images/gatsby-astronaut.png b/e2e-tests/development-runtime/src/images/gatsby-astronaut.png
new file mode 100644
index 0000000000000..da58ece0a8c5b
Binary files /dev/null and b/e2e-tests/development-runtime/src/images/gatsby-astronaut.png differ
diff --git a/e2e-tests/development-runtime/src/images/gatsby-icon.png b/e2e-tests/development-runtime/src/images/gatsby-icon.png
new file mode 100644
index 0000000000000..908bc78a7f559
Binary files /dev/null and b/e2e-tests/development-runtime/src/images/gatsby-icon.png differ
diff --git a/e2e-tests/development-runtime/src/pages/404.js b/e2e-tests/development-runtime/src/pages/404.js
new file mode 100644
index 0000000000000..26a69be3ae54a
--- /dev/null
+++ b/e2e-tests/development-runtime/src/pages/404.js
@@ -0,0 +1,14 @@
+import React from "react"
+
+import Layout from "../components/layout"
+import SEO from "../components/seo"
+
+const NotFoundPage = () => (
+
+
+ NOT FOUND
+ You just hit a route that does not exist... the sadness.
+
+)
+
+export default NotFoundPage
diff --git a/e2e-tests/development-runtime/src/pages/anonymous-arrow-two.js b/e2e-tests/development-runtime/src/pages/anonymous-arrow-two.js
new file mode 100644
index 0000000000000..b43fe900012e0
--- /dev/null
+++ b/e2e-tests/development-runtime/src/pages/anonymous-arrow-two.js
@@ -0,0 +1,13 @@
+import React from "react"
+import { Link } from "gatsby"
+
+import Layout from "../components/layout"
+
+export default () => (
+
+ Anonymous Arrow Function Two
+
+ First page
+
+
+)
diff --git a/e2e-tests/development-runtime/src/pages/anonymous-arrow.js b/e2e-tests/development-runtime/src/pages/anonymous-arrow.js
new file mode 100644
index 0000000000000..2022679c07f14
--- /dev/null
+++ b/e2e-tests/development-runtime/src/pages/anonymous-arrow.js
@@ -0,0 +1,13 @@
+import React from "react"
+import { Link } from "gatsby"
+
+import Layout from "../components/layout"
+
+export default () => (
+
+ Anonymous Arrow Function
+
+ Second page
+
+
+)
diff --git a/e2e-tests/development-runtime/src/pages/arrows.js b/e2e-tests/development-runtime/src/pages/arrows.js
new file mode 100644
index 0000000000000..67ff798779f73
--- /dev/null
+++ b/e2e-tests/development-runtime/src/pages/arrows.js
@@ -0,0 +1,16 @@
+import React from "react"
+
+import Layout from "../components/layout"
+import Title from "../components/title"
+import Subtitle from "../components/sub-title"
+
+function Arrows() {
+ return (
+
+
+
+
+ )
+}
+
+export default Arrows
diff --git a/e2e-tests/development-runtime/src/pages/hooks.js b/e2e-tests/development-runtime/src/pages/hooks.js
new file mode 100644
index 0000000000000..b26780701fa48
--- /dev/null
+++ b/e2e-tests/development-runtime/src/pages/hooks.js
@@ -0,0 +1,22 @@
+import React, { useState } from "react"
+
+import Layout from "../components/layout"
+
+function Hooks() {
+ const [count, setCount] = useState(0)
+ const increment = () => setCount(count + 1)
+ const decrement = () => setCount(count - 1)
+ return (
+
+ {count}
+
+ -
+
+
+ +
+
+
+ )
+}
+
+export default Hooks
diff --git a/e2e-tests/development-runtime/src/pages/index.js b/e2e-tests/development-runtime/src/pages/index.js
new file mode 100644
index 0000000000000..e78378d640377
--- /dev/null
+++ b/e2e-tests/development-runtime/src/pages/index.js
@@ -0,0 +1,57 @@
+import React from "react"
+import { Link, graphql } from "gatsby"
+
+import ClassComponent from "../components/class-component"
+import Layout from "../components/layout"
+import Image from "../components/image"
+import SEO from "../components/seo"
+import InstrumentPage from "../utils/instrument-page"
+
+const IndexPage = ({ data }) => (
+
+
+ Hi people
+ Welcome to your new %GATSBY_SITE%
+ Now go build something great.
+
+
+
+
+
+ Go to page 2
+
+
+ Go to a broken link
+
+ Blog posts
+
+ {data.posts.edges.map(({ node }) => (
+
+ {node.frontmatter.title}
+
+ ))}
+
+
+)
+
+export default InstrumentPage(IndexPage)
+
+export const indexQuery = graphql`
+ {
+ posts: allMarkdownRemark(
+ sort: { fields: [frontmatter___date], order: DESC }
+ ) {
+ edges {
+ node {
+ id
+ fields {
+ slug
+ }
+ frontmatter {
+ title
+ }
+ }
+ }
+ }
+ }
+`
diff --git a/e2e-tests/development-runtime/src/pages/page-2.js b/e2e-tests/development-runtime/src/pages/page-2.js
new file mode 100644
index 0000000000000..c6c81c46c047e
--- /dev/null
+++ b/e2e-tests/development-runtime/src/pages/page-2.js
@@ -0,0 +1,18 @@
+import React from "react"
+import { Link } from "gatsby"
+
+import Layout from "../components/layout"
+import SEO from "../components/seo"
+
+const SecondPage = () => (
+
+
+ Hi from the second page
+ Welcome to page 2
+
+ Go back to the homepage
+
+
+)
+
+export default SecondPage
diff --git a/e2e-tests/development-runtime/src/templates/blog-post.js b/e2e-tests/development-runtime/src/templates/blog-post.js
new file mode 100644
index 0000000000000..d978055ad7d48
--- /dev/null
+++ b/e2e-tests/development-runtime/src/templates/blog-post.js
@@ -0,0 +1,31 @@
+import React from "react"
+import { graphql, Link } from "gatsby"
+
+import Layout from "../components/layout"
+import SEO from "../components/seo"
+
+function BlogPost({ data: { post } }) {
+ return (
+
+
+ {post.frontmatter.title}
+ Hello %MESSAGE%
+
+ Back to home
+
+ )
+}
+
+export default BlogPost
+
+export const blogPostQuery = graphql`
+ query GetBlogPostBySlug($slug: String!) {
+ post: markdownRemark(fields: { slug: { eq: $slug } }) {
+ html
+ excerpt(pruneLength: 160)
+ frontmatter {
+ title
+ }
+ }
+ }
+`
diff --git a/e2e-tests/development-runtime/src/utils/instrument-page.js b/e2e-tests/development-runtime/src/utils/instrument-page.js
new file mode 100644
index 0000000000000..8ff2cf776584b
--- /dev/null
+++ b/e2e-tests/development-runtime/src/utils/instrument-page.js
@@ -0,0 +1,33 @@
+import React from "react"
+
+export default Page =>
+ class extends React.Component {
+ addLogEntry(action) {
+ if (typeof window !== `undefined`) {
+ window.___PageComponentLifecycleCallsLog.push({
+ action,
+ pageComponent: this.props.pageResources.page.componentChunkName,
+ locationPath: this.props.location.pathname,
+ pagePath: this.props.pageResources.page.path,
+ })
+ }
+ }
+
+ constructor(props) {
+ super(props)
+ this.addLogEntry(`constructor`)
+ }
+
+ componentDidMount() {
+ this.addLogEntry(`componentDidMount`)
+ }
+
+ componentWillUnmount() {
+ this.addLogEntry(`componentWillUnmount`)
+ }
+
+ render() {
+ this.addLogEntry(`render`)
+ return
+ }
+ }