- {props.data.markdown.nodes.map((node, index) => {
+ {props.data.markdown.nodes.sort((a, b) => a.fields.slug.localeCompare(b.fields.slug)).map((node, index) => {
return (
)
})}
- {props.data.images.nodes.map((node, index) => {
+ {props.data.images.nodes.sort((a, b) => a.parent.name.localeCompare(b.parent.name)).map((node, index) => {
return (
+ This is the modified exports for page templates test page
+ {/* Use typeof to avoid runtime error */}
+ {typeof config}
+
+ {typeof getServerData}
+
+
+ {importedConfig()}
+
+
+ {importedGetServerData()}
+
+
+ {helloWorld()}
+
+
+
+ )
+}
+
+export function config() {
+ return "config exported from a page template module"
+}
+
+export function getServerData() {
+ return "getServerData exported from a page template module"
+}
+
+export default ModifiedExports
diff --git a/e2e-tests/production-runtime/cypress/integration/gatsby-ssr-tsx.js b/e2e-tests/production-runtime/cypress/integration/gatsby-ssr-tsx.js
new file mode 100644
index 0000000000000..8f1f81fc22588
--- /dev/null
+++ b/e2e-tests/production-runtime/cypress/integration/gatsby-ssr-tsx.js
@@ -0,0 +1,6 @@
+describe(`gatsby-ssr.tsx`, () => {
+ it(`works`, () => {
+ cy.visit(`/`).waitForRouteChange()
+ cy.get(`.gatsby-ssr-tsx`).should(`have.attr`, `data-content`, `TSX with gatsby-ssr works`)
+ })
+})
\ No newline at end of file
diff --git a/e2e-tests/production-runtime/cypress/integration/modified-exports.js b/e2e-tests/production-runtime/cypress/integration/modified-exports.js
new file mode 100644
index 0000000000000..58939d4ccbbaf
--- /dev/null
+++ b/e2e-tests/production-runtime/cypress/integration/modified-exports.js
@@ -0,0 +1,65 @@
+/**
+ * Test that page templates have certain exports removed while other files are left alone.
+ *
+ * Page templates support only a default exported React component and named exports of
+ * `config` and `getServerData`, so it's not necessary (or possible) to test other exports
+ * in page templates.
+ */
+
+const config = `config exported from a non-page template module`
+const getServerData = `getServerData exported from a non-page template module`
+const helloWorld = `hello world`
+
+describe(`modifed exports`, () => {
+ beforeEach(() => {
+ cy.visit(`/modified-exports`).waitForRouteChange()
+ })
+
+ describe(`page templates`, () => {
+ it(`should have exports named config removed`, () => {
+ cy.getTestElement(`modified-exports-page-template-config`)
+ .invoke(`text`)
+ .should(`contain`, `undefined`)
+ })
+ it(`should have exports named getServerData removed`, () => {
+ cy.getTestElement(`modified-exports-page-template-get-server-data`)
+ .invoke(`text`)
+ .should(`contain`, `undefined`)
+ })
+ it(`should have imported exports named config left alone`, () => {
+ cy.getTestElement(`unmodified-exports-page-template-config`)
+ .invoke(`text`)
+ .should(`contain`, config)
+ })
+ it(`should have imported exports named getServerData left alone`, () => {
+ cy.getTestElement(`unmodified-exports-page-template-get-server-data`)
+ .invoke(`text`)
+ .should(`contain`, getServerData)
+ })
+ it(`should have other imported exports left alone`, () => {
+ cy.getTestElement(`unmodified-exports-page-template-hello-world`)
+ .invoke(`text`)
+ .should(`contain`, helloWorld)
+ })
+ })
+
+ describe(`other JS files`, () => {
+ it(`should have exports named config left alone`, () => {
+ cy.getTestElement(`unmodified-exports-config`)
+ .invoke(`text`)
+ .should(`contain`, config)
+ })
+
+ it(`should have exports named getServerData left alone`, () => {
+ cy.getTestElement(`unmodified-exports-get-server-data`)
+ .invoke(`text`)
+ .should(`contain`, getServerData)
+ })
+
+ it(`should have other named exports left alone`, () => {
+ cy.getTestElement(`unmodified-exports-hello-world`)
+ .invoke(`text`)
+ .should(`contain`, helloWorld)
+ })
+ })
+})
diff --git a/e2e-tests/production-runtime/gatsby-config.js b/e2e-tests/production-runtime/gatsby-config.js
index 1fddea9d12bba..e141df454f388 100644
--- a/e2e-tests/production-runtime/gatsby-config.js
+++ b/e2e-tests/production-runtime/gatsby-config.js
@@ -20,6 +20,7 @@ module.exports = {
},
},
`gatsby-plugin-local-worker`,
+ `gatsby-ssr-tsx`,
`gatsby-plugin-image`,
`gatsby-plugin-sharp`,
`gatsby-plugin-sass`,
diff --git a/e2e-tests/production-runtime/plugins/gatsby-ssr-tsx/gatsby-browser.tsx b/e2e-tests/production-runtime/plugins/gatsby-ssr-tsx/gatsby-browser.tsx
new file mode 100644
index 0000000000000..6b8a1e8c5dba8
--- /dev/null
+++ b/e2e-tests/production-runtime/plugins/gatsby-ssr-tsx/gatsby-browser.tsx
@@ -0,0 +1,11 @@
+import * as React from "react"
+import { GatsbyBrowser } from "gatsby"
+
+export const wrapPageElement: GatsbyBrowser["wrapPageElement"] = ({ element }) => {
+ return (
+ <>
+
+ {element}
+ >
+ )
+}
\ No newline at end of file
diff --git a/e2e-tests/production-runtime/plugins/gatsby-ssr-tsx/gatsby-ssr.tsx b/e2e-tests/production-runtime/plugins/gatsby-ssr-tsx/gatsby-ssr.tsx
new file mode 100644
index 0000000000000..afa72edbaf8f5
--- /dev/null
+++ b/e2e-tests/production-runtime/plugins/gatsby-ssr-tsx/gatsby-ssr.tsx
@@ -0,0 +1,11 @@
+import * as React from "react"
+import { GatsbySSR } from "gatsby"
+
+export const wrapPageElement: GatsbySSR["wrapPageElement"] = ({ element }) => {
+ return (
+ <>
+
+ {element}
+ >
+ )
+}
\ No newline at end of file
diff --git a/e2e-tests/production-runtime/plugins/gatsby-ssr-tsx/index.js b/e2e-tests/production-runtime/plugins/gatsby-ssr-tsx/index.js
new file mode 100644
index 0000000000000..625c0891b2c30
--- /dev/null
+++ b/e2e-tests/production-runtime/plugins/gatsby-ssr-tsx/index.js
@@ -0,0 +1 @@
+// noop
\ No newline at end of file
diff --git a/e2e-tests/production-runtime/plugins/gatsby-ssr-tsx/package.json b/e2e-tests/production-runtime/plugins/gatsby-ssr-tsx/package.json
new file mode 100644
index 0000000000000..e23d7374663bc
--- /dev/null
+++ b/e2e-tests/production-runtime/plugins/gatsby-ssr-tsx/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "gatsby-ssr-tsx",
+ "version": "1.0.0",
+ "main": "index.js"
+}
\ No newline at end of file
diff --git a/e2e-tests/production-runtime/src/components/unmodified-exports.js b/e2e-tests/production-runtime/src/components/unmodified-exports.js
new file mode 100644
index 0000000000000..5462bf2377400
--- /dev/null
+++ b/e2e-tests/production-runtime/src/components/unmodified-exports.js
@@ -0,0 +1,25 @@
+import React from "react"
+
+function UnmodifiedExports() {
+ return (
+
+
{config()}
+
{getServerData()}
+
{helloWorld()}
+
+ )
+}
+
+export function config() {
+ return "config exported from a non-page template module"
+}
+
+export function getServerData() {
+ return "getServerData exported from a non-page template module"
+}
+
+export function helloWorld() {
+ return "hello world"
+}
+
+export default UnmodifiedExports
diff --git a/e2e-tests/production-runtime/src/pages/modified-exports.js b/e2e-tests/production-runtime/src/pages/modified-exports.js
new file mode 100644
index 0000000000000..9a4a64506f820
--- /dev/null
+++ b/e2e-tests/production-runtime/src/pages/modified-exports.js
@@ -0,0 +1,39 @@
+import React from "react"
+import UnmodifiedExports, {
+ config as importedConfig,
+ getServerData as importedGetServerData,
+ helloWorld,
+} from "../components/unmodified-exports"
+
+function ModifiedExports() {
+ return (
+
+
This is the modified exports for page templates test page
+ {/* Use typeof to avoid runtime error */}
+
{typeof config}
+
+ {typeof getServerData}
+
+
+ {importedConfig()}
+
+
+ {importedGetServerData()}
+
+
+ {helloWorld()}
+
+
+
+ )
+}
+
+export function config() {
+ return () => "config exported from a page template module" // Expects config to be a function factory
+}
+
+export function getServerData() {
+ return "getServerData exported from a page template module"
+}
+
+export default ModifiedExports
diff --git a/e2e-tests/trailing-slash/.gitignore b/e2e-tests/trailing-slash/.gitignore
new file mode 100644
index 0000000000000..52c8ffaeb94bc
--- /dev/null
+++ b/e2e-tests/trailing-slash/.gitignore
@@ -0,0 +1,13 @@
+# Project dependencies
+.cache
+node_modules
+yarn-error.log
+
+# Build assets
+/public
+.DS_Store
+/assets
+
+# Cypress output
+cypress/videos/
+cypress/screenshots/
diff --git a/e2e-tests/trailing-slash/README.md b/e2e-tests/trailing-slash/README.md
new file mode 100644
index 0000000000000..b1627f34c11ef
--- /dev/null
+++ b/e2e-tests/trailing-slash/README.md
@@ -0,0 +1,15 @@
+# trailing-slash E2E Test
+
+This Cypress suite tests the `trailingSlash` option inside `gatsby-config` and its various different settings it takes. When you want to work on it, start watching packages inside the `packages` and start `gatsby-dev-cli` in this E2E test suite.
+
+Locally you can run for development:
+
+```shell
+TRAILING_SLASH=your-option yarn debug:develop
+```
+
+And for a build + serve:
+
+```shell
+TRAILING_SLASH=your-option yarn build && yarn debug:build
+```
diff --git a/e2e-tests/trailing-slash/cypress-always.json b/e2e-tests/trailing-slash/cypress-always.json
new file mode 100644
index 0000000000000..937b58765b5a1
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress-always.json
@@ -0,0 +1,5 @@
+{
+ "videoUploadOnPasses": false,
+ "chromeWebSecurity": false,
+ "testFiles": ["always.js", "functions.js", "static.js"]
+}
diff --git a/e2e-tests/trailing-slash/cypress-ignore.json b/e2e-tests/trailing-slash/cypress-ignore.json
new file mode 100644
index 0000000000000..607c1c7475fed
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress-ignore.json
@@ -0,0 +1,5 @@
+{
+ "videoUploadOnPasses": false,
+ "chromeWebSecurity": false,
+ "testFiles": ["ignore.js", "functions.js", "static.js"]
+}
diff --git a/e2e-tests/trailing-slash/cypress-legacy.json b/e2e-tests/trailing-slash/cypress-legacy.json
new file mode 100644
index 0000000000000..d4d72f3ae04e6
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress-legacy.json
@@ -0,0 +1,5 @@
+{
+ "videoUploadOnPasses": false,
+ "chromeWebSecurity": false,
+ "testFiles": ["legacy.js", "functions.js", "static.js"]
+}
diff --git a/e2e-tests/trailing-slash/cypress-never.json b/e2e-tests/trailing-slash/cypress-never.json
new file mode 100644
index 0000000000000..d5aaf9cf2df54
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress-never.json
@@ -0,0 +1,5 @@
+{
+ "videoUploadOnPasses": false,
+ "chromeWebSecurity": false,
+ "testFiles": ["never.js", "functions.js", "static.js"]
+}
diff --git a/e2e-tests/trailing-slash/cypress.json b/e2e-tests/trailing-slash/cypress.json
new file mode 100644
index 0000000000000..4c8aa3a9cac67
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress.json
@@ -0,0 +1,4 @@
+{
+ "videoUploadOnPasses": false,
+ "chromeWebSecurity": false
+}
diff --git a/e2e-tests/trailing-slash/cypress/fixtures/example.json b/e2e-tests/trailing-slash/cypress/fixtures/example.json
new file mode 100644
index 0000000000000..02e4254378e97
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+ "name": "Using fixtures to represent data",
+ "email": "hello@cypress.io",
+ "body": "Fixtures are a great way to mock data for responses to routes"
+}
diff --git a/e2e-tests/trailing-slash/cypress/integration/always.js b/e2e-tests/trailing-slash/cypress/integration/always.js
new file mode 100644
index 0000000000000..722a4471982e9
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress/integration/always.js
@@ -0,0 +1,240 @@
+import { assertPageVisits } from "../support/utils/trailing-slash"
+
+describe(`always`, () => {
+ beforeEach(() => {
+ cy.visit(`/`).waitForRouteChange()
+ })
+ it(`page-creator without slash`, () => {
+ cy.getTestElement(`page-creator-without`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2/`)
+ })
+ it(`page-creator with slash`, () => {
+ cy.getTestElement(`page-creator-with`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2/`)
+ })
+ it(`create-page with slash`, () => {
+ cy.getTestElement(`create-page-with`).click()
+ cy.waitForRouteChange().assertRoute(`/create-page/with/`)
+ })
+ it(`create-page without slash`, () => {
+ cy.getTestElement(`create-page-without`).click()
+ cy.waitForRouteChange().assertRoute(`/create-page/without/`)
+ })
+ it(`fs-api with slash`, () => {
+ cy.getTestElement(`fs-api-with`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/with/`)
+ })
+ it(`fs-api without slash`, () => {
+ cy.getTestElement(`fs-api-without`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/without/`)
+ })
+ it(`fs-api client only splat without slash`, () => {
+ cy.getTestElement(`fs-api-client-only-without`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/without/without/`)
+ cy.getTestElement(`title`).should(`have.text`, `without`)
+ })
+ it(`fs-api client only splat with slash`, () => {
+ cy.getTestElement(`fs-api-client-only-with`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/with/with/`)
+ cy.getTestElement(`title`).should(`have.text`, `with`)
+ })
+ it(`fs-api-simple with slash`, () => {
+ cy.getTestElement(`fs-api-simple-with`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api-simple/with/`)
+ })
+ it(`fs-api-simple without slash`, () => {
+ cy.getTestElement(`fs-api-simple-without`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api-simple/without/`)
+ })
+ it(`gatsbyPath works`, () => {
+ cy.getTestElement(`gatsby-path-1`).should(
+ "have.attr",
+ "href",
+ "/fs-api-simple/with/"
+ )
+ cy.getTestElement(`gatsby-path-2`).should(
+ "have.attr",
+ "href",
+ "/fs-api-simple/without/"
+ )
+ })
+ it(`hash`, () => {
+ cy.getTestElement(`hash`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2/#anchor`)
+ })
+ it(`hash trailing`, () => {
+ cy.getTestElement(`hash-trailing`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2/#anchor`)
+ })
+ it(`query-param`, () => {
+ cy.getTestElement(`query-param`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2/?query_param=hello`)
+ })
+ it(`query-param-hash`, () => {
+ cy.getTestElement(`query-param-hash`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2/?query_param=hello#anchor`)
+ })
+ it(`client-only without slash`, () => {
+ cy.getTestElement(`client-only-simple-without`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only/without/`)
+ cy.getTestElement(`title`).should(`have.text`, `without`)
+ })
+ it(`client-only with slash`, () => {
+ cy.getTestElement(`client-only-simple-with`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only/with/`)
+ cy.getTestElement(`title`).should(`have.text`, `with`)
+ })
+ it(`client-only-splat without slash`, () => {
+ cy.getTestElement(`client-only-splat-without`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only-splat/without/without/`)
+ cy.getTestElement(`title`).should(`have.text`, `without/without`)
+ })
+ it(`client-only-splat with slash`, () => {
+ cy.getTestElement(`client-only-splat-with`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only-splat/with/with/`)
+ cy.getTestElement(`title`).should(`have.text`, `with/with`)
+ })
+})
+
+describe(`always (direct visits)`, () => {
+ beforeEach(() => {
+ cy.visit(`/`).waitForRouteChange()
+ })
+
+ it(`page-creator`, () => {
+ assertPageVisits([
+ { path: "/page-2/", status: 200 },
+ { path: "/page-2", status: 301, destinationPath: "/page-2/" },
+ ])
+
+ cy.visit(`/page-2`).waitForRouteChange().assertRoute(`/page-2/`)
+ })
+
+ it(`create-page with`, () => {
+ assertPageVisits([{ path: "/create-page/with/", status: 200 }])
+
+ cy.visit(`/create-page/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/create-page/with/`)
+ })
+
+ it(`create-page without`, () => {
+ assertPageVisits([
+ {
+ path: "/create-page/without",
+ status: 301,
+ destinationPath: "/create-page/without",
+ },
+ ])
+
+ cy.visit(`/create-page/without`)
+ .waitForRouteChange()
+ .assertRoute(`/create-page/without/`)
+ })
+
+ it(`fs-api-simple with`, () => {
+ assertPageVisits([{ path: "/fs-api-simple/with/", status: 200 }])
+
+ cy.visit(`/fs-api-simple/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api-simple/with/`)
+ })
+
+ it(`fs-api-simple without`, () => {
+ assertPageVisits([
+ {
+ path: "/fs-api-simple/without",
+ status: 301,
+ destinationPath: "/fs-api-simple/without/",
+ },
+ ])
+
+ cy.visit(`/fs-api-simple/without`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api-simple/without/`)
+ })
+
+ it(`fs-api client only splat with`, () => {
+ assertPageVisits([{ path: "/fs-api/with/with/", status: 200 }])
+
+ cy.visit(`/fs-api/with/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api/with/with/`)
+ })
+
+ it(`fs-api client only splat without`, () => {
+ assertPageVisits([
+ {
+ path: "`/fs-api/without/without",
+ status: 301,
+ destinationPath: "`/fs-api/without/without/",
+ },
+ ])
+
+ cy.visit(`/fs-api/without/without`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api/without/without/`)
+ })
+
+ it(`client-only with`, () => {
+ assertPageVisits([{ path: "/create-page/with/", status: 200 }])
+
+ cy.visit(`/client-only/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only/with/`)
+ })
+
+ it(`client-only without`, () => {
+ assertPageVisits([
+ {
+ path: "/client-only/without",
+ status: 301,
+ destinationPath: "/client-only/without",
+ },
+ ])
+
+ cy.visit(`/client-only/without`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only/without/`)
+ })
+
+ it(`client-only-splat with`, () => {
+ assertPageVisits([{ path: "/client-only-splat/with/with/", status: 200 }])
+
+ cy.visit(`/client-only-splat/with/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only-splat/with/with/`)
+ })
+
+ it(`client-only-splat without`, () => {
+ assertPageVisits([
+ {
+ path: "`/client-only-splat/without/without",
+ status: 301,
+ destinationPath: "`/client-only-splat/without/without/",
+ },
+ ])
+
+ cy.visit(`/client-only-splat/without/without`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only-splat/without/without/`)
+ })
+
+ it(`query-param-hash with`, () => {
+ assertPageVisits([
+ { path: "/page-2/?query_param=hello#anchor", status: 200 },
+ ])
+
+ cy.visit(`/page-2/?query_param=hello#anchor`)
+ .waitForRouteChange()
+ .assertRoute(`/page-2/?query_param=hello#anchor`)
+ })
+
+ it(`query-param-hash without`, () => {
+ assertPageVisits([{ path: "/page-2?query_param=hello#anchor", status: 200 }])
+
+ cy.visit(`/page-2?query_param=hello#anchor`)
+ .waitForRouteChange()
+ .assertRoute(`/page-2/?query_param=hello#anchor`)
+ })
+})
diff --git a/e2e-tests/trailing-slash/cypress/integration/functions.js b/e2e-tests/trailing-slash/cypress/integration/functions.js
new file mode 100644
index 0000000000000..bef063b0429b8
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress/integration/functions.js
@@ -0,0 +1,35 @@
+import { assertPageVisits } from "../support/utils/trailing-slash"
+
+describe(`functions`, () => {
+ describe(`src/api/test.js`, () => {
+ it(`functions are always accessible without trailing slash`, () => {
+ assertPageVisits([{ path: "/api/test", status: 200 }])
+
+ cy.visit(`/api/test`).assertRoute(`/api/test`)
+ })
+
+ it(`functions 404 with trailing slash`, () => {
+ assertPageVisits([{ path: "/api/test/", status: 404 }])
+
+ cy.visit(`/api/test/`, { failOnStatusCode: false }).assertRoute(
+ `/api/test/`
+ )
+ })
+ })
+
+ describe(`src/api/nested/index.js`, () => {
+ it(`functions are always accessible without trailing slash`, () => {
+ assertPageVisits([{ path: "/api/nested", status: 200 }])
+
+ cy.visit(`/api/nested`).assertRoute(`/api/nested`)
+ })
+
+ it(`functions 404 with trailing slash`, () => {
+ assertPageVisits([{ path: "/api/nested/", status: 404 }])
+
+ cy.visit(`/api/nested/`, { failOnStatusCode: false }).assertRoute(
+ `/api/nested/`
+ )
+ })
+ })
+})
diff --git a/e2e-tests/trailing-slash/cypress/integration/ignore.js b/e2e-tests/trailing-slash/cypress/integration/ignore.js
new file mode 100644
index 0000000000000..7a9497a23efb6
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress/integration/ignore.js
@@ -0,0 +1,224 @@
+import { assertPageVisits } from "../support/utils/trailing-slash"
+
+describe(`ignore`, () => {
+ beforeEach(() => {
+ cy.visit(`/`).waitForRouteChange()
+ })
+ it(`page-creator without slash`, () => {
+ cy.getTestElement(`page-creator-without`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2`)
+ })
+ it(`page-creator with slash`, () => {
+ cy.getTestElement(`page-creator-with`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2/`)
+ })
+ it(`create-page with slash`, () => {
+ cy.getTestElement(`create-page-with`).click()
+ cy.waitForRouteChange().assertRoute(`/create-page/with/`)
+ })
+ it(`create-page without slash`, () => {
+ cy.getTestElement(`create-page-without`).click()
+ cy.waitForRouteChange().assertRoute(`/create-page/without`)
+ })
+ it(`fs-api with slash`, () => {
+ cy.getTestElement(`fs-api-with`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/with/`)
+ })
+ it(`fs-api without slash`, () => {
+ cy.getTestElement(`fs-api-without`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/without`)
+ })
+ it(`fs-api client only splat without slash`, () => {
+ cy.getTestElement(`fs-api-client-only-without`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/without/without`)
+ cy.getTestElement(`title`).should(`have.text`, `without`)
+ })
+ it(`fs-api client only splat with slash`, () => {
+ cy.getTestElement(`fs-api-client-only-with`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/with/with/`)
+ cy.getTestElement(`title`).should(`have.text`, `with`)
+ })
+ it(`fs-api-simple with slash`, () => {
+ cy.getTestElement(`fs-api-simple-with`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api-simple/with/`)
+ })
+ it(`fs-api-simple without slash`, () => {
+ cy.getTestElement(`fs-api-simple-without`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api-simple/without`)
+ })
+ it(`gatsbyPath works`, () => {
+ cy.getTestElement(`gatsby-path-1`).should(
+ "have.attr",
+ "href",
+ "/fs-api-simple/with/"
+ )
+ cy.getTestElement(`gatsby-path-2`).should(
+ "have.attr",
+ "href",
+ "/fs-api-simple/without"
+ )
+ })
+ it(`hash`, () => {
+ cy.getTestElement(`hash`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2#anchor`)
+ })
+ it(`hash trailing`, () => {
+ cy.getTestElement(`hash-trailing`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2/#anchor`)
+ })
+ it(`query-param`, () => {
+ cy.getTestElement(`query-param`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2?query_param=hello`)
+ })
+ it(`query-param-hash`, () => {
+ cy.getTestElement(`query-param-hash`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2?query_param=hello#anchor`)
+ })
+ it(`client-only without slash`, () => {
+ cy.getTestElement(`client-only-simple-without`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only/without`)
+ cy.getTestElement(`title`).should(`have.text`, `without`)
+ })
+ it(`client-only with slash`, () => {
+ cy.getTestElement(`client-only-simple-with`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only/with/`)
+ cy.getTestElement(`title`).should(`have.text`, `with`)
+ })
+ it(`client-only-splat without slash`, () => {
+ cy.getTestElement(`client-only-splat-without`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only-splat/without/without`)
+ cy.getTestElement(`title`).should(`have.text`, `without/without`)
+ })
+ it(`client-only-splat with slash`, () => {
+ cy.getTestElement(`client-only-splat-with`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only-splat/with/with/`)
+ cy.getTestElement(`title`).should(`have.text`, `with/with`)
+ })
+})
+
+const IS_BUILD = Cypress.env(`IS_BUILD`)
+
+describe(`ignore (direct visits)`, () => {
+ beforeEach(() => {
+ cy.visit(`/`).waitForRouteChange()
+ })
+
+ //Fix
+ it(`page-creator`, () => {
+ assertPageVisits([
+ {
+ path: "/page-2",
+ destinationPath: IS_BUILD ? `/page-2/` : false,
+ status: IS_BUILD ? 301 : 200,
+ },
+ ])
+
+ cy.visit(`/page-2`)
+ .waitForRouteChange()
+ // TODO(v5): Should behave like "always"
+ .assertRoute(IS_BUILD ? `/page-2/` : `/page-2`)
+ })
+
+ it(`create-page with`, () => {
+ assertPageVisits([{ path: "/create-page/with/", status: 200 }])
+
+ cy.visit(`/create-page/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/create-page/with/`)
+ })
+
+ it(`create-page without`, () => {
+ assertPageVisits([{ path: "/create-page/without", status: 200 }])
+
+ cy.visit(`/create-page/without`)
+ .waitForRouteChange()
+ .assertRoute(`/create-page/without`)
+ })
+
+ it(`fs-api-simple with`, () => {
+ assertPageVisits([{ path: "/fs-api-simple/with/", status: 200 }])
+
+ cy.visit(`/fs-api-simple/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api-simple/with/`)
+ })
+
+ it(`fs-api-simple without`, () => {
+ assertPageVisits([{ path: "/fs-api-simple/without", status: 200 }])
+
+ cy.visit(`/fs-api-simple/without`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api-simple/without`)
+ })
+
+ it(`fs-api client only splat with`, () => {
+ assertPageVisits([{ path: "/fs-api/with/with/", status: 200 }])
+
+ cy.visit(`/fs-api/with/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api/with/with/`)
+ })
+
+ it(`fs-api client only splat without`, () => {
+ assertPageVisits([{ path: "/fs-api/without/without", status: 200 }])
+
+ cy.visit(`/fs-api/without/without`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api/without/without`)
+ })
+
+ it(`client-only with`, () => {
+ assertPageVisits([{ path: "/client-only/with/", status: 200 }])
+
+ cy.visit(`/client-only/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only/with/`)
+ })
+ it(`client-only without`, () => {
+ assertPageVisits([{ path: "/create-page/without", status: 200 }])
+
+ cy.visit(`/client-only/without`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only/without`)
+ })
+ it(`client-only-splat with`, () => {
+ assertPageVisits([{ path: "/create-page/without", status: 200 }])
+
+ cy.visit(`/client-only-splat/with/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only-splat/with/with/`)
+ })
+ it(`client-only-splat without`, () => {
+ assertPageVisits([{ path: "/create-page/without", status: 200 }])
+
+ cy.visit(`/client-only-splat/without/without`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only-splat/without/without`)
+ })
+ it(`query-param-hash with`, () => {
+ assertPageVisits([{ path: "/create-page/without", status: 200 }])
+
+ cy.visit(`/page-2/?query_param=hello#anchor`)
+ .waitForRouteChange()
+ .assertRoute(`/page-2/?query_param=hello#anchor`)
+ })
+ it(`query-param-hash without`, () => {
+ assertPageVisits([
+ {
+ path: "/page-2?query_param=hello#anchor",
+ status: IS_BUILD ? 301 : 200,
+ destinationPath: IS_BUILD
+ ? "/page-2/?query_param=hello#anchor"
+ : false,
+ },
+ ])
+
+ cy.visit(`/page-2?query_param=hello#anchor`)
+ .waitForRouteChange()
+ .assertRoute(
+ IS_BUILD
+ ? `/page-2/?query_param=hello#anchor`
+ : `/page-2?query_param=hello#anchor`
+ )
+ })
+})
diff --git a/e2e-tests/trailing-slash/cypress/integration/legacy.js b/e2e-tests/trailing-slash/cypress/integration/legacy.js
new file mode 100644
index 0000000000000..1a9090fc43af4
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress/integration/legacy.js
@@ -0,0 +1,274 @@
+import { assertPageVisits } from "../support/utils/trailing-slash"
+
+describe(`legacy`, () => {
+ beforeEach(() => {
+ cy.visit(`/`).waitForRouteChange()
+ })
+ it(`page-creator without slash`, () => {
+ cy.getTestElement(`page-creator-without`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2`)
+ })
+ it(`page-creator with slash`, () => {
+ cy.getTestElement(`page-creator-with`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2/`)
+ })
+ it(`create-page with slash`, () => {
+ cy.getTestElement(`create-page-with`).click()
+ cy.waitForRouteChange().assertRoute(`/create-page/with/`)
+ })
+ it(`create-page without slash`, () => {
+ cy.getTestElement(`create-page-without`).click()
+ cy.waitForRouteChange().assertRoute(`/create-page/without`)
+ })
+ it(`fs-api with slash`, () => {
+ cy.getTestElement(`fs-api-with`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/with/`)
+ })
+ it(`fs-api without slash`, () => {
+ cy.getTestElement(`fs-api-without`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/without`)
+ })
+ it(`fs-api client only splat without slash`, () => {
+ cy.getTestElement(`fs-api-client-only-without`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/without/without`)
+ cy.getTestElement(`title`).should(`have.text`, `without`)
+ })
+ it(`fs-api client only splat with slash`, () => {
+ cy.getTestElement(`fs-api-client-only-with`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/with/with/`)
+ cy.getTestElement(`title`).should(`have.text`, `with`)
+ })
+ it(`fs-api-simple with slash`, () => {
+ cy.getTestElement(`fs-api-simple-with`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api-simple/with/`)
+ })
+ it(`fs-api-simple without slash`, () => {
+ cy.getTestElement(`fs-api-simple-without`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api-simple/without`)
+ })
+ it(`gatsbyPath works`, () => {
+ cy.getTestElement(`gatsby-path-1`).should(
+ "have.attr",
+ "href",
+ "/fs-api-simple/with/"
+ )
+ cy.getTestElement(`gatsby-path-2`).should(
+ "have.attr",
+ "href",
+ "/fs-api-simple/without/"
+ )
+ })
+ it(`hash`, () => {
+ cy.getTestElement(`hash`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2#anchor`)
+ })
+ it(`hash trailing`, () => {
+ cy.getTestElement(`hash-trailing`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2/#anchor`)
+ })
+ it(`query-param`, () => {
+ cy.getTestElement(`query-param`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2?query_param=hello`)
+ })
+ it(`query-param-hash`, () => {
+ cy.getTestElement(`query-param-hash`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2?query_param=hello#anchor`)
+ })
+ it(`client-only without slash`, () => {
+ cy.getTestElement(`client-only-simple-without`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only/without`)
+ cy.getTestElement(`title`).should(`have.text`, `without`)
+ })
+ it(`client-only with slash`, () => {
+ cy.getTestElement(`client-only-simple-with`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only/with/`)
+ cy.getTestElement(`title`).should(`have.text`, `with`)
+ })
+ it(`client-only-splat without slash`, () => {
+ cy.getTestElement(`client-only-splat-without`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only-splat/without/without`)
+ cy.getTestElement(`title`).should(`have.text`, `without/without`)
+ })
+ it(`client-only-splat with slash`, () => {
+ cy.getTestElement(`client-only-splat-with`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only-splat/with/with/`)
+ cy.getTestElement(`title`).should(`have.text`, `with/with`)
+ })
+})
+
+const IS_BUILD = Cypress.env(`IS_BUILD`)
+
+describe(`legacy (direct visits)`, () => {
+ beforeEach(() => {
+ cy.visit(`/`).waitForRouteChange()
+ })
+
+ it(`page-creator`, () => {
+ assertPageVisits([
+ {
+ path: "/page-2/",
+ destinationPath: IS_BUILD ? false : `/page-2`,
+ status: IS_BUILD ? 200 : 301,
+ },
+ ])
+
+ cy.visit(`/page-2`)
+ .waitForRouteChange()
+ .assertRoute(IS_BUILD ? `/page-2/` : `/page-2`)
+ })
+
+ it(`create-page with`, () => {
+ assertPageVisits([
+ {
+ path: "/create-page/with/",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/create-page/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/create-page/with/`)
+ })
+
+ it(`create-page without`, () => {
+ assertPageVisits([
+ {
+ path: "/create-page/without",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/create-page/without`)
+ .waitForRouteChange()
+ .assertRoute(`/create-page/without`)
+ })
+ it(`fs-api-simple with`, () => {
+ assertPageVisits([
+ {
+ path: "/fs-api-simple/with/",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/fs-api-simple/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api-simple/with/`)
+ })
+ it(`fs-api-simple without`, () => {
+ assertPageVisits([
+ {
+ path: "/fs-api-simple/without",
+ status: IS_BUILD ? 301 : 200,
+ destinationPath: IS_BUILD ? `/fs-api-simple/without/` : false,
+ },
+ ])
+
+ cy.visit(`/fs-api-simple/without`)
+ .waitForRouteChange()
+ .assertRoute(
+ IS_BUILD ? `/fs-api-simple/without/` : `/fs-api-simple/without`
+ )
+ })
+ it(`fs-api client only splat with`, () => {
+ assertPageVisits([
+ {
+ path: "/fs-api/with/with/",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/fs-api/with/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api/with/with/`)
+ })
+ it(`fs-api client only splat without`, () => {
+ assertPageVisits([
+ {
+ path: "/fs-api/without/without",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/fs-api/without/without`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api/without/without`)
+ })
+ it(`client-only with`, () => {
+ assertPageVisits([
+ {
+ path: "/client-only/with/",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/client-only/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only/with/`)
+ })
+ it(`client-only without`, () => {
+ assertPageVisits([
+ {
+ path: "/client-only/without",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/client-only/without`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only/without`)
+ })
+ it(`client-only-splat with`, () => {
+ assertPageVisits([
+ {
+ path: `/client-only-splat/with/with/`,
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/client-only-splat/with/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only-splat/with/with/`)
+ })
+ it(`client-only-splat without`, () => {
+ assertPageVisits([
+ {
+ path: "/client-only-splat/without/without",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/client-only-splat/without/without`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only-splat/without/without`)
+ })
+ it(`query-param-hash with`, () => {
+ assertPageVisits([
+ {
+ path: "/page-2/?query_param=hello#anchor",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/page-2/?query_param=hello#anchor`)
+ .waitForRouteChange()
+ .assertRoute(`/page-2/?query_param=hello#anchor`)
+ })
+
+ it(`query-param-hash without`, () => {
+ assertPageVisits([
+ {
+ path: "/page-2?query_param=hello#anchor",
+ status: IS_BUILD ? 301 : 200,
+ destinationPath: IS_BUILD ? `/page-2?query_param=hello#anchor` : false,
+ },
+ ])
+
+ cy.visit(`/page-2?query_param=hello#anchor`)
+ .waitForRouteChange()
+ .assertRoute(
+ IS_BUILD
+ ? `/page-2/?query_param=hello#anchor`
+ : `/page-2?query_param=hello#anchor`
+ )
+ })
+})
diff --git a/e2e-tests/trailing-slash/cypress/integration/never.js b/e2e-tests/trailing-slash/cypress/integration/never.js
new file mode 100644
index 0000000000000..77caf305947cf
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress/integration/never.js
@@ -0,0 +1,284 @@
+import { assertPageVisits } from "../support/utils/trailing-slash"
+
+describe(`never`, () => {
+ beforeEach(() => {
+ cy.visit(`/`).waitForRouteChange()
+ })
+ it(`page-creator without slash`, () => {
+ cy.getTestElement(`page-creator-without`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2`)
+ })
+ it(`page-creator with slash`, () => {
+ cy.getTestElement(`page-creator-with`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2`)
+ })
+ it(`create-page with slash`, () => {
+ cy.getTestElement(`create-page-with`).click()
+ cy.waitForRouteChange().assertRoute(`/create-page/with`)
+ })
+ it(`create-page without slash`, () => {
+ cy.getTestElement(`create-page-without`).click()
+ cy.waitForRouteChange().assertRoute(`/create-page/without`)
+ })
+ it(`fs-api with slash`, () => {
+ cy.getTestElement(`fs-api-with`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/with`)
+ })
+ it(`fs-api without slash`, () => {
+ cy.getTestElement(`fs-api-without`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/without`)
+ })
+ it(`fs-api client only splat without slash`, () => {
+ cy.getTestElement(`fs-api-client-only-without`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/without/without`)
+ cy.getTestElement(`title`).should(`have.text`, `without`)
+ })
+ it(`fs-api client only splat with slash`, () => {
+ cy.getTestElement(`fs-api-client-only-with`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api/with/with`)
+ cy.getTestElement(`title`).should(`have.text`, `with`)
+ })
+ it(`fs-api-simple with slash`, () => {
+ cy.getTestElement(`fs-api-simple-with`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api-simple/with`)
+ })
+ it(`fs-api-simple without slash`, () => {
+ cy.getTestElement(`fs-api-simple-without`).click()
+ cy.waitForRouteChange().assertRoute(`/fs-api-simple/without`)
+ })
+ it(`gatsbyPath works`, () => {
+ cy.getTestElement(`gatsby-path-1`).should(
+ "have.attr",
+ "href",
+ "/fs-api-simple/with"
+ )
+ cy.getTestElement(`gatsby-path-2`).should(
+ "have.attr",
+ "href",
+ "/fs-api-simple/without"
+ )
+ })
+ it(`hash`, () => {
+ cy.getTestElement(`hash`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2#anchor`)
+ })
+ it(`hash trailing`, () => {
+ cy.getTestElement(`hash-trailing`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2#anchor`)
+ })
+ it(`query-param`, () => {
+ cy.getTestElement(`query-param`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2?query_param=hello`)
+ })
+ it(`query-param-hash`, () => {
+ cy.getTestElement(`query-param-hash`).click()
+ cy.waitForRouteChange().assertRoute(`/page-2?query_param=hello#anchor`)
+ })
+ it(`client-only without slash`, () => {
+ cy.getTestElement(`client-only-simple-without`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only/without`)
+ cy.getTestElement(`title`).should(`have.text`, `without`)
+ })
+ it(`client-only with slash`, () => {
+ cy.getTestElement(`client-only-simple-with`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only/with`)
+ cy.getTestElement(`title`).should(`have.text`, `with`)
+ })
+ it(`client-only-splat without slash`, () => {
+ cy.getTestElement(`client-only-splat-without`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only-splat/without/without`)
+ cy.getTestElement(`title`).should(`have.text`, `without/without`)
+ })
+ it(`client-only-splat with slash`, () => {
+ cy.getTestElement(`client-only-splat-with`).click()
+ cy.waitForRouteChange().assertRoute(`/client-only-splat/with/with`)
+ cy.getTestElement(`title`).should(`have.text`, `with/with`)
+ })
+})
+
+describe(`never (direct visits)`, () => {
+ beforeEach(() => {
+ cy.visit(`/`).waitForRouteChange()
+ })
+
+ it(`page-creator`, () => {
+ assertPageVisits([
+ {
+ path: "/page-2",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/page-2`).waitForRouteChange().assertRoute(`/page-2`)
+ })
+
+ it(`create-page with`, () => {
+ assertPageVisits([
+ {
+ path: "/create-page/with/",
+ status: 301,
+ destinationPath: "/create-page/with",
+ },
+ ])
+
+ cy.visit(`/create-page/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/create-page/with`)
+ })
+
+ it(`create-page without`, () => {
+ assertPageVisits([
+ {
+ path: "/create-page/without",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/create-page/without`)
+ .waitForRouteChange()
+ .assertRoute(`/create-page/without`)
+ })
+
+ it(`fs-api-simple with`, () => {
+ assertPageVisits([
+ {
+ path: "/fs-api-simple/with/",
+ status: 301,
+ destinationPath: "/fs-api-simple/with",
+ },
+ {
+ path: "/fs-api-simple/without",
+ status: 301,
+ destinationPath: "/fs-api-simple/without",
+ },
+ ])
+
+ cy.visit(`/fs-api-simple/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api-simple/with`)
+ cy.visit(`/fs-api-simple/without`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api-simple/without`)
+ })
+
+ it(`fs-api-simple without`, () => {
+ assertPageVisits([
+ {
+ path: "/fs-api-simple/without",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/fs-api-simple/without`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api-simple/without`)
+ })
+
+ it(`fs-api client only splat with`, () => {
+ assertPageVisits([
+ {
+ path: "/fs-api/with/with/",
+ status: 301,
+ destinationPath: "/fs-api/with/with",
+ },
+ ])
+
+ cy.visit(`/fs-api/with/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api/with/with`)
+ })
+
+ it(`fs-api client only splat without`, () => {
+ assertPageVisits([
+ {
+ path: "/fs-api/without/without",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/fs-api/without/without`)
+ .waitForRouteChange()
+ .assertRoute(`/fs-api/without/without`)
+ })
+
+ it(`client-only with`, () => {
+ assertPageVisits([
+ {
+ path: "/client-only/with/",
+ status: 301,
+ destinationPath: "/client-only/with",
+ },
+ ])
+
+ cy.visit(`/client-only/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only/with`)
+ })
+
+ it(`client-only without`, () => {
+ assertPageVisits([
+ {
+ path: "/client-only/without",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/client-only/without`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only/without`)
+ })
+
+ it(`client-only-splat with`, () => {
+ assertPageVisits([
+ {
+ path: "/client-only-splat/with/with/",
+ status: 301,
+ destinationPath: "/client-only-splat/with/with",
+ },
+ ])
+
+ cy.visit(`/client-only-splat/with/with/`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only-splat/with/with`)
+ })
+
+ it(`client-only-splat without`, () => {
+ assertPageVisits([
+ {
+ path: "/client-only-splat/without/without",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/client-only-splat/without/without`)
+ .waitForRouteChange()
+ .assertRoute(`/client-only-splat/without/without`)
+ })
+
+ it(`query-param-hash with`, () => {
+ assertPageVisits([
+ {
+ path: "/page-2/?query_param=hello#anchor",
+ status: 301,
+ destinationPath: "/page-2?query_param=hello#anchor",
+ },
+ ])
+
+ cy.visit(`/page-2/?query_param=hello#anchor`)
+ .waitForRouteChange()
+ .assertRoute(`/page-2?query_param=hello#anchor`)
+ })
+
+ it(`query-param-hash without`, () => {
+ assertPageVisits([
+ {
+ path: "/page-2?query_param=hello#anchor",
+ status: 200,
+ },
+ ])
+
+ cy.visit(`/page-2?query_param=hello#anchor`)
+ .waitForRouteChange()
+ .assertRoute(`/page-2?query_param=hello#anchor`)
+ })
+})
diff --git a/e2e-tests/trailing-slash/cypress/integration/static.js b/e2e-tests/trailing-slash/cypress/integration/static.js
new file mode 100644
index 0000000000000..5c42e3ef74c38
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress/integration/static.js
@@ -0,0 +1,41 @@
+import { assertPageVisits } from "../support/utils/trailing-slash"
+
+const IS_BUILD = Cypress.env(`IS_BUILD`)
+
+const itWhenIsBuild = IS_BUILD ? it : it.skip
+
+describe(`static directory`, () => {
+ describe(`static/something.html`, () => {
+ itWhenIsBuild(`visiting directly result in 200`, () => {
+ assertPageVisits([{ path: "/static/something.html", status: 200 }])
+
+ cy.visit(`/something.html`).assertRoute(`/something.html`)
+ })
+
+ itWhenIsBuild(`adding trailing slash result in 404`, () => {
+ // works for build+serve, doesn't work for develop
+ assertPageVisits([{ path: "/something.html/", status: 404 }])
+
+ cy.visit(`/something.html/`, {
+ failOnStatusCode: false,
+ }).assertRoute(`/something.html/`)
+ })
+ })
+
+ describe(`static/nested/index.html`, () => {
+ itWhenIsBuild(
+ `visiting without trailing slash redirects to trailing slash`,
+ () => {
+ assertPageVisits([{ path: "/nested", status: 200 }])
+
+ cy.visit(`/nested`).assertRoute(`/nested/`)
+ }
+ )
+
+ it(`visiting with trailing slash returns 404`, () => {
+ assertPageVisits([{ path: "/nested/", status: 404 }])
+
+ cy.visit(`/nested/`, { failOnStatusCode: false }).assertRoute(`/nested/`)
+ })
+ })
+})
diff --git a/e2e-tests/trailing-slash/cypress/plugins/index.js b/e2e-tests/trailing-slash/cypress/plugins/index.js
new file mode 100644
index 0000000000000..fd170fba6912b
--- /dev/null
+++ b/e2e-tests/trailing-slash/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/trailing-slash/cypress/support/commands.js b/e2e-tests/trailing-slash/cypress/support/commands.js
new file mode 100644
index 0000000000000..a13f2cb8f7884
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress/support/commands.js
@@ -0,0 +1,3 @@
+Cypress.Commands.add(`assertRoute`, route => {
+ cy.url().should(`equal`, `${window.location.origin}${route}`)
+})
diff --git a/e2e-tests/trailing-slash/cypress/support/index.js b/e2e-tests/trailing-slash/cypress/support/index.js
new file mode 100644
index 0000000000000..83237f7c18da2
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress/support/index.js
@@ -0,0 +1,2 @@
+import "gatsby-cypress"
+import "./commands"
diff --git a/e2e-tests/trailing-slash/cypress/support/utils/trailing-slash.js b/e2e-tests/trailing-slash/cypress/support/utils/trailing-slash.js
new file mode 100644
index 0000000000000..50b59854dd8dc
--- /dev/null
+++ b/e2e-tests/trailing-slash/cypress/support/utils/trailing-slash.js
@@ -0,0 +1,16 @@
+export function assertPageVisits(pages) {
+ for (let i = 0; i < pages; i++) {
+ const page = pages[i]
+
+ cy.intercept(new RegExp(`^${page.path}$`), req => {
+ req.continue(res => {
+ expect(res.statusCode).to.equal(page.status)
+ if (page.destinationPath) {
+ expect(res.headers.location).to.equal(page.destinationPath)
+ } else {
+ expect(res.headers.location).toBeUndefined()
+ }
+ })
+ })
+ }
+}
diff --git a/e2e-tests/trailing-slash/gatsby-browser.js b/e2e-tests/trailing-slash/gatsby-browser.js
new file mode 100644
index 0000000000000..be552c7f5970e
--- /dev/null
+++ b/e2e-tests/trailing-slash/gatsby-browser.js
@@ -0,0 +1 @@
+import "./global.css"
diff --git a/e2e-tests/trailing-slash/gatsby-config.js b/e2e-tests/trailing-slash/gatsby-config.js
new file mode 100644
index 0000000000000..01a4fb4540838
--- /dev/null
+++ b/e2e-tests/trailing-slash/gatsby-config.js
@@ -0,0 +1,12 @@
+const trailingSlash = process.env.TRAILING_SLASH || `legacy`
+console.info(`TrailingSlash: ${trailingSlash}`)
+
+module.exports = {
+ trailingSlash,
+ siteMetadata: {
+ siteMetadata: {
+ siteUrl: `https://www.domain.tld`,
+ title: `Trailing Slash`,
+ },
+ },
+}
diff --git a/e2e-tests/trailing-slash/gatsby-node.js b/e2e-tests/trailing-slash/gatsby-node.js
new file mode 100644
index 0000000000000..36a7821c020d8
--- /dev/null
+++ b/e2e-tests/trailing-slash/gatsby-node.js
@@ -0,0 +1,78 @@
+const posts = [
+ {
+ id: 1,
+ slug: `/with/`,
+ title: `With Trailing Slash`,
+ content: `With Trailing Slash`,
+ },
+ {
+ id: 2,
+ slug: `/without`,
+ title: `Without Trailing Slash`,
+ content: `Without Trailing Slash`,
+ },
+ {
+ id: 3,
+ slug: `/`,
+ title: `Index page`,
+ content: `This is an index page`,
+ },
+]
+
+exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => {
+ const { createNode } = actions
+
+ posts.forEach(post => {
+ createNode({
+ ...post,
+ id: createNodeId(`post-${post.id}`),
+ _id: post.id,
+ parent: null,
+ children: [],
+ internal: {
+ type: `Post`,
+ content: JSON.stringify(post),
+ contentDigest: createContentDigest(post),
+ },
+ })
+ })
+}
+
+exports.createSchemaCustomization = ({ actions }) => {
+ const { createTypes } = actions
+
+ createTypes(`#graphql
+ type Post implements Node {
+ id: ID!
+ slug: String!
+ title: String!
+ content: String!
+ }
+ `)
+}
+
+const templatePath = require.resolve(`./src/templates/template.js`)
+
+exports.createPages = async ({ graphql, actions }) => {
+ const { createPage } = actions
+
+ const result = await graphql(`
+ {
+ allPost {
+ nodes {
+ slug
+ }
+ }
+ }
+ `)
+
+ result.data.allPost.nodes.forEach(node => {
+ createPage({
+ path: `/create-page${node.slug}`,
+ component: templatePath,
+ context: {
+ slug: node.slug,
+ },
+ })
+ })
+}
diff --git a/e2e-tests/trailing-slash/global.css b/e2e-tests/trailing-slash/global.css
new file mode 100644
index 0000000000000..2369598c9758b
--- /dev/null
+++ b/e2e-tests/trailing-slash/global.css
@@ -0,0 +1,69 @@
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+* {
+ margin: 0;
+}
+html,
+body {
+ height: 100%;
+}
+body {
+ line-height: 1.5;
+ -webkit-font-smoothing: antialiased;
+}
+img,
+picture,
+video,
+canvas,
+svg {
+ display: block;
+ max-width: 100%;
+}
+input,
+button,
+textarea,
+select {
+ font: inherit;
+}
+p,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ overflow-wrap: break-word;
+}
+#__gatsby {
+ isolation: isolate;
+}
+
+:root {
+ --light-gray: #e2e8f0;
+ --dark-gray: #1d2739;
+ --body-bg: var(--light-gray);
+ --body-color: var(--dark-gray);
+ --link-color: #000;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --body-bg: var(--dark-gray);
+ --body-color: var(--light-gray);
+ --link-color: #fff;
+ }
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
+ Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+ background: var(--body-bg);
+ color: var(--body-color);
+}
+
+a {
+ color: var(--link-color);
+}
diff --git a/e2e-tests/trailing-slash/package.json b/e2e-tests/trailing-slash/package.json
new file mode 100644
index 0000000000000..652ecf1ec1ddf
--- /dev/null
+++ b/e2e-tests/trailing-slash/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "trailing-slash",
+ "description": "E2E Tests for trailingSlash config option",
+ "version": "1.0.0",
+ "author": "LekoArts",
+ "dependencies": {
+ "cypress": "^9.1.1",
+ "gatsby": "next",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
+ },
+ "license": "MIT",
+ "scripts": {
+ "develop": "cross-env CYPRESS_SUPPORT=y gatsby develop",
+ "build": "cross-env CYPRESS_SUPPORT=y gatsby build",
+ "clean": "gatsby clean",
+ "serve": "gatsby serve",
+ "format": "prettier --write '**/*.js' --ignore-path .gitignore",
+ "cy:open:develop": "cypress open --config baseUrl=http://localhost:8000",
+ "cy:open:build": "cypress open --config baseUrl=http://localhost:9000 --env IS_BUILD=y",
+ "debug:develop": "start-server-and-test develop http://localhost:8000 cy:open:develop",
+ "debug:build": "start-server-and-test serve http://localhost:9000 cy:open:build",
+ "cy:develop:option": "cross-env-shell node ../../scripts/cypress-run-with-conditional-record-flag.js --browser chrome --config-file \"cypress-$OPTION.json\" --config baseUrl=http://localhost:8000",
+ "cy:build:option": "cross-env-shell node ../../scripts/cypress-run-with-conditional-record-flag.js --browser chrome --config-file \"cypress-$OPTION.json\" --config baseUrl=http://localhost:9000 --env IS_BUILD=y",
+ "develop:option": "cross-env-shell CYPRESS_SUPPORT=y TRAILING_SLASH=$OPTION gatsby develop",
+ "build:option": "cross-env-shell CYPRESS_SUPPORT=y TRAILING_SLASH=$OPTION gatsby build",
+ "t:opt:develop": "cross-env-shell OPTION=$OPTION start-server-and-test develop:option http://localhost:8000 cy:develop:option",
+ "t:opt:build": "cross-env-shell OPTION=$OPTION TRAILING_SLASH=$OPTION start-server-and-test serve http://localhost:9000 cy:build:option",
+ "test:always": "cross-env OPTION=always npm run build:option && cross-env OPTION=always npm run t:opt:build && npm run clean && cross-env OPTION=always npm run t:opt:develop",
+ "test:never": "cross-env OPTION=never npm run build:option && cross-env OPTION=never npm run t:opt:build && npm run clean && cross-env OPTION=never npm run t:opt:develop",
+ "test:ignore": "cross-env OPTION=ignore npm run build:option && cross-env OPTION=ignore npm run t:opt:build && npm run clean && cross-env OPTION=ignore npm run t:opt:develop",
+ "test:legacy": "cross-env OPTION=legacy npm run build:option && cross-env OPTION=legacy npm run t:opt:build && npm run clean && cross-env OPTION=legacy npm run t:opt:develop",
+ "test": "npm-run-all -c -s test:always test:never test:ignore test:legacy"
+ },
+ "devDependencies": {
+ "cross-env": "^7.0.3",
+ "gatsby-cypress": "^2.4.0",
+ "npm-run-all": "^4.1.5",
+ "prettier": "^2.5.1",
+ "start-server-and-test": "^1.14.0"
+ }
+}
diff --git a/e2e-tests/trailing-slash/src/api/nested/index.js b/e2e-tests/trailing-slash/src/api/nested/index.js
new file mode 100644
index 0000000000000..4c22214ed5443
--- /dev/null
+++ b/e2e-tests/trailing-slash/src/api/nested/index.js
@@ -0,0 +1,4 @@
+module.exports = (req, res) => {
+ res.send(`hello`)
+}
+
diff --git a/e2e-tests/trailing-slash/src/api/test.js b/e2e-tests/trailing-slash/src/api/test.js
new file mode 100644
index 0000000000000..a7137d46d07c4
--- /dev/null
+++ b/e2e-tests/trailing-slash/src/api/test.js
@@ -0,0 +1,3 @@
+module.exports = (req, res) => {
+ res.send(`hello`)
+}
\ No newline at end of file
diff --git a/e2e-tests/trailing-slash/src/pages/client-only-splat/[...name].js b/e2e-tests/trailing-slash/src/pages/client-only-splat/[...name].js
new file mode 100644
index 0000000000000..58087626e61ef
--- /dev/null
+++ b/e2e-tests/trailing-slash/src/pages/client-only-splat/[...name].js
@@ -0,0 +1,14 @@
+import * as React from "react"
+
+const ClientOnlySplatNamePage = ({ params }) => {
+ return (
+
+ {params.name}
+
+ {JSON.stringify(params, null, 2)}
+
+
+ )
+}
+
+export default ClientOnlySplatNamePage
diff --git a/e2e-tests/trailing-slash/src/pages/client-only/[name].js b/e2e-tests/trailing-slash/src/pages/client-only/[name].js
new file mode 100644
index 0000000000000..e8a4c7d19cbf3
--- /dev/null
+++ b/e2e-tests/trailing-slash/src/pages/client-only/[name].js
@@ -0,0 +1,14 @@
+import * as React from "react"
+
+const ClientOnlyNamePage = ({ params }) => {
+ return (
+
+ {params.name}
+
+ {JSON.stringify(params, null, 2)}
+
+
+ )
+}
+
+export default ClientOnlyNamePage
diff --git a/e2e-tests/trailing-slash/src/pages/fs-api-simple/{Post.slug}.js b/e2e-tests/trailing-slash/src/pages/fs-api-simple/{Post.slug}.js
new file mode 100644
index 0000000000000..5e816cc0affcd
--- /dev/null
+++ b/e2e-tests/trailing-slash/src/pages/fs-api-simple/{Post.slug}.js
@@ -0,0 +1,18 @@
+import * as React from "react"
+import { Link } from "gatsby"
+
+const FSApiSimplePage = ({ pageContext }) => {
+ return (
+
+ {pageContext.slug}
+
+ Go Back
+
+
+ {JSON.stringify(pageContext, null, 2)}
+
+
+ )
+}
+
+export default FSApiSimplePage
diff --git a/e2e-tests/trailing-slash/src/pages/fs-api/{Post.slug}/[...name].js b/e2e-tests/trailing-slash/src/pages/fs-api/{Post.slug}/[...name].js
new file mode 100644
index 0000000000000..66d7201f743b7
--- /dev/null
+++ b/e2e-tests/trailing-slash/src/pages/fs-api/{Post.slug}/[...name].js
@@ -0,0 +1,14 @@
+import * as React from "react"
+
+const FSApiClientOnlySplatNamePage = ({ params }) => {
+ return (
+
+ {params.name}
+
+ {JSON.stringify(params, null, 2)}
+
+
+ )
+}
+
+export default FSApiClientOnlySplatNamePage
diff --git a/e2e-tests/trailing-slash/src/pages/fs-api/{Post.slug}/index.js b/e2e-tests/trailing-slash/src/pages/fs-api/{Post.slug}/index.js
new file mode 100644
index 0000000000000..6eccb0ae3eef2
--- /dev/null
+++ b/e2e-tests/trailing-slash/src/pages/fs-api/{Post.slug}/index.js
@@ -0,0 +1,18 @@
+import * as React from "react"
+import { Link } from "gatsby"
+
+const FSApiPage = ({ pageContext }) => {
+ return (
+
+ {pageContext.slug}
+
+ Go Back
+
+
+ {JSON.stringify(pageContext, null, 2)}
+
+
+ )
+}
+
+export default FSApiPage
diff --git a/e2e-tests/trailing-slash/src/pages/index.js b/e2e-tests/trailing-slash/src/pages/index.js
new file mode 100644
index 0000000000000..6583ee69ee27c
--- /dev/null
+++ b/e2e-tests/trailing-slash/src/pages/index.js
@@ -0,0 +1,141 @@
+import * as React from "react"
+import { Link, graphql } from "gatsby"
+
+const IndexPage = ({ data }) => {
+ const {
+ allPost: { nodes: posts },
+ } = data
+ return (
+
+ Trailing Slash Testing
+
+ -
+
+ Page Creator Without Trailing Slash
+
+
+ -
+
+ Page Creator With Trailing Slash
+
+
+ -
+
+ Create Page With Trailing Slash
+
+
+ -
+
+ Create Page Without Trailing Slash
+
+
+ -
+
+ FS API With Trailing Slash
+
+
+ -
+
+ FS API Without Trailing Slash
+
+
+ -
+
+ FS API Client-Only Without Trailing Slash
+
+
+ -
+
+ FS API Client-Only With Trailing Slash
+
+
+ -
+
+ FS API Simple With Trailing Slash
+
+
+ -
+
+ FS API Simple Without Trailing Slash
+
+
+ -
+
+ Go to page-2 with hash
+
+
+ -
+
+ Go to page-2 with hash With Trailing Slash
+
+
+ -
+
+ Go to page-2 with query param
+
+
+ -
+
+ Go to page-2 with query param and hash
+
+
+ -
+
+ Client-Only Simple Without Trailing Slash
+
+
+ -
+
+ Client-Only Simple With Trailing Slash
+
+
+ -
+
+ Client-Only-Splat Without Trailing Slash
+
+
+ -
+
+ Client-Only-Splat With Trailing Slash
+
+
+ {posts.map(post => (
+ -
+
+ Go to {post.slug} from gatsbyPath
+
+
+ ))}
+
+
+ )
+}
+
+export default IndexPage
+
+export const query = graphql`
+ {
+ allPost {
+ nodes {
+ _id
+ slug
+ gatsbyPath(filePath: "/fs-api-simple/{Post.slug}")
+ }
+ }
+ }
+`
diff --git a/e2e-tests/trailing-slash/src/pages/page-2.js b/e2e-tests/trailing-slash/src/pages/page-2.js
new file mode 100644
index 0000000000000..72dcfe568d8a7
--- /dev/null
+++ b/e2e-tests/trailing-slash/src/pages/page-2.js
@@ -0,0 +1,64 @@
+import * as React from "react"
+import { Link } from "gatsby"
+
+const PageTwo = () => {
+ return (
+
+ Page Two
+
+ Go Back
+
+
+ Tergeo mobilicorpus mortis nox tarantallegra mobilicorpus felicis
+ locomotor unction. Sonorus evanesco riddikulus lumos sonorus curse.
+ Mobilicorpus mortis leviosa lumos dissendium funnunculus. Imperio
+ reducio cruciatus portus evanesco imperio crucio inflamarae. Rictusempra
+ immobilus incarcerous ennervate muffliato evanesco. Engorgio locomotor
+ stupefy mobilicorpus. Locomotor homorphus leviosa accio incantartem
+ totalus sonorus sectumsempra lumos protego aparecium. Impedimenta
+ incarcerous petrificus patronum exume impedimenta accio immobilus
+ aparecium tarantallegra vipera. Arania arania quietus patronum
+ funnunculus. Wingardium leviosa felicis nox tarantallegra expecto
+ quietus jelly-legs. Mortis ennervate patronum serpensortia expecto
+ mobilicorpus waddiwasi. Legilimens legilimens protego inflamarae
+ specialis leviosa portus diffindo tarantallegra immobilus. Impedimenta
+ momentum me jelly-legs. Sonorus aresto densaugeo confundus immobilus
+ accio quodpot evanesco imperio totalus patronum. Reducto leviosa nox
+ portus funnunculus confundus cruciatus. Incarcerous portus sonorus
+ babbling impedimenta. Finite evanesco wingardium kedavra momentum
+ bulbadox lumos evanesco cushioning arania. Locomotor unction sonorus
+ wingardium expelliarumus dissendium aresto. Legilimens sonorus
+ imperturbable mobilicorpus lumos incarcerous mobilicorpus. Langlock
+ banishing unctuous expelliarmus. Avis locomotor immobilus leviosa finite
+ serpensortia imperio.
+
+
+ Tergeo mobilicorpus mortis nox tarantallegra mobilicorpus felicis
+ locomotor unction. Sonorus evanesco riddikulus lumos sonorus curse.
+ Mobilicorpus mortis leviosa lumos dissendium funnunculus. Imperio
+ reducio cruciatus portus evanesco imperio crucio inflamarae. Rictusempra
+ immobilus incarcerous ennervate muffliato evanesco. Engorgio locomotor
+ stupefy mobilicorpus. Locomotor homorphus leviosa accio incantartem
+ totalus sonorus sectumsempra lumos protego aparecium. Impedimenta
+ incarcerous petrificus patronum exume impedimenta accio immobilus
+ aparecium tarantallegra vipera. Arania arania quietus patronum
+ funnunculus. Wingardium leviosa felicis nox tarantallegra expecto
+ quietus jelly-legs. Mortis ennervate patronum serpensortia expecto
+ mobilicorpus waddiwasi. Legilimens legilimens protego inflamarae
+ specialis leviosa portus diffindo tarantallegra immobilus. Impedimenta
+ momentum me jelly-legs. Sonorus aresto densaugeo confundus immobilus
+ accio quodpot evanesco imperio totalus patronum. Reducto leviosa nox
+ portus funnunculus confundus cruciatus. Incarcerous portus sonorus
+ babbling impedimenta. Finite evanesco wingardium kedavra momentum
+ bulbadox lumos evanesco cushioning arania. Locomotor unction sonorus
+ wingardium expelliarumus dissendium aresto. Legilimens sonorus
+ imperturbable mobilicorpus lumos incarcerous mobilicorpus. Langlock
+ banishing unctuous expelliarmus. Avis locomotor immobilus leviosa finite
+ serpensortia imperio.
+
+ Anchor
+
+ )
+}
+
+export default PageTwo
diff --git a/e2e-tests/trailing-slash/src/templates/template.js b/e2e-tests/trailing-slash/src/templates/template.js
new file mode 100644
index 0000000000000..d56afb3a40c4e
--- /dev/null
+++ b/e2e-tests/trailing-slash/src/templates/template.js
@@ -0,0 +1,18 @@
+import * as React from "react"
+import { Link } from "gatsby"
+
+const Template = ({ pageContext }) => {
+ return (
+
+ {pageContext.title}
+
+ Go Back
+
+
+ {JSON.stringify(pageContext, null, 2)}
+
+
+ )
+}
+
+export default Template
diff --git a/e2e-tests/trailing-slash/static/nested/index.html b/e2e-tests/trailing-slash/static/nested/index.html
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/e2e-tests/trailing-slash/static/something.html b/e2e-tests/trailing-slash/static/something.html
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/examples/route-api/src/pages/index.js b/examples/route-api/src/pages/index.js
index 6425f9bc69da4..ad4d19491db8f 100644
--- a/examples/route-api/src/pages/index.js
+++ b/examples/route-api/src/pages/index.js
@@ -96,7 +96,7 @@ function Index({ data }) {
Client-Only routes
As shortly mentioned for the "Collection routes" the{` `}
- [name].js file inside src/pages/products is alreay a
+ [name].js file inside src/pages/products is already a
client-only page. But you can do even more with those! See the example
below:
diff --git a/examples/using-jest/jest.config.js b/examples/using-jest/jest.config.js
index 6c407b0cf3583..588b2b06ec5b1 100644
--- a/examples/using-jest/jest.config.js
+++ b/examples/using-jest/jest.config.js
@@ -5,6 +5,8 @@ module.exports = {
moduleNameMapper: {
".+\\.(css|styl|less|sass|scss)$": `identity-obj-proxy`,
".+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": `/__mocks__/file-mock.js`,
+ "^gatsby-page-utils/(.*)$": `gatsby-page-utils/dist/$1`, // Workaround for https://github.com/facebook/jest/issues/9771
+ "^gatsby-core-utils/(.*)$": `gatsby-core-utils/dist/$1`, // Workaround for https://github.com/facebook/jest/issues/9771
},
testPathIgnorePatterns: [`node_modules`, `.cache`],
transformIgnorePatterns: [`node_modules/(?!(gatsby)/)`],
diff --git a/examples/using-vanilla-extract/README.md b/examples/using-vanilla-extract/README.md
new file mode 100644
index 0000000000000..9f113fa3ee10f
--- /dev/null
+++ b/examples/using-vanilla-extract/README.md
@@ -0,0 +1,3 @@
+# Using vanilla-extract
+
+Example site that demonstrates [vanilla-extract](https://vanilla-extract.style/) using the plugin [`gatsby-plugin-vanilla-extract`](https://github.com/gatsby-uc/plugins/tree/main/packages/gatsby-plugin-vanilla-extract).
diff --git a/examples/using-vanilla-extract/gatsby-config.js b/examples/using-vanilla-extract/gatsby-config.js
new file mode 100644
index 0000000000000..22b60e2453aac
--- /dev/null
+++ b/examples/using-vanilla-extract/gatsby-config.js
@@ -0,0 +1,7 @@
+module.exports = {
+ siteMetadata: {
+ title: `using-vanilla-extract`,
+ siteUrl: `https://www.yourdomain.tld`,
+ },
+ plugins: [`gatsby-plugin-vanilla-extract`]
+}
\ No newline at end of file
diff --git a/examples/using-vanilla-extract/package.json b/examples/using-vanilla-extract/package.json
new file mode 100644
index 0000000000000..a78f39f0c31fa
--- /dev/null
+++ b/examples/using-vanilla-extract/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "using-vanilla-extract",
+ "version": "1.0.0",
+ "private": true,
+ "description": "using-vanilla-extract",
+ "author": "Jude Agboola",
+ "keywords": [
+ "gatsby"
+ ],
+ "scripts": {
+ "develop": "gatsby develop",
+ "start": "gatsby develop",
+ "build": "gatsby build",
+ "serve": "gatsby serve",
+ "clean": "gatsby clean"
+ },
+ "dependencies": {
+ "@vanilla-extract/babel-plugin": "^1.1.4",
+ "@vanilla-extract/css": "^1.6.8",
+ "@vanilla-extract/webpack-plugin": "^2.1.5",
+ "gatsby": "next",
+ "gatsby-plugin-vanilla-extract": "^2.0.0",
+ "react": "^17.0.1",
+ "react-dom": "^17.0.1"
+ }
+}
diff --git a/examples/using-vanilla-extract/src/components/ColorModeToggle.tsx b/examples/using-vanilla-extract/src/components/ColorModeToggle.tsx
new file mode 100644
index 0000000000000..c41d11d048aac
--- /dev/null
+++ b/examples/using-vanilla-extract/src/components/ColorModeToggle.tsx
@@ -0,0 +1,56 @@
+import * as React from 'react';
+import * as styles from "./color-mode-button.css"
+
+type ColorMode = 'dark' | 'light';
+export const themeKey = 'using-vanilla-extract-pref';
+
+interface ColorModeContextValues {
+ colorMode: ColorMode | null;
+ setColorMode: (colorMode: ColorMode) => void;
+}
+
+export const ColorModeContext = React.createContext({
+ colorMode: null,
+ setColorMode: () => {},
+});
+
+export function ColorModeProvider({ children }: { children: React.ReactNode }) {
+ const [colorMode, setColorMode] = React.useState(null);
+
+ React.useEffect(() => {
+ setColorMode(
+ document.documentElement.classList.contains('dark') ? 'dark' : 'light',
+ );
+ }, []);
+
+ const setter = (c: ColorMode) => {
+ setColorMode(c);
+
+ document.documentElement.classList.remove('light', 'dark');
+ document.documentElement.classList.add(c);
+
+ try {
+ localStorage.setItem(themeKey, c);
+ } catch (e) {}
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const ColorModeToggle = () => {
+ const { colorMode, setColorMode } = React.useContext(ColorModeContext);
+ const mode = colorMode === 'light' ? 'dark' : 'light'
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/examples/using-vanilla-extract/src/components/color-mode-button.css.ts b/examples/using-vanilla-extract/src/components/color-mode-button.css.ts
new file mode 100644
index 0000000000000..eef5a46e9eb20
--- /dev/null
+++ b/examples/using-vanilla-extract/src/components/color-mode-button.css.ts
@@ -0,0 +1,34 @@
+import { createVar, style } from '@vanilla-extract/css';
+import { rootColors } from '../styles/global.css';
+
+const initial = createVar()
+const dark = createVar()
+
+export const root = style({
+ outline: `none`,
+ borderWidth: `1px`,
+ borderStyle: `solid`,
+ borderColor: dark,
+ borderRadius: `0.25rem`,
+ background: `transparent`,
+ padding: `0.15rem 0.5rem`,
+ transition: `all 0.3s ease-in-out`,
+ color: dark,
+ ':hover': {
+ background: dark,
+ color: initial,
+ cursor: `pointer`
+ },
+ vars: {
+ [initial]: rootColors.light.bg,
+ [dark]: rootColors.dark.bg,
+ },
+ selectors: {
+ [`.dark &`]: {
+ vars: {
+ [initial]: rootColors.dark.bg,
+ [dark]: rootColors.light.bg,
+ }
+ }
+ }
+})
\ No newline at end of file
diff --git a/examples/using-vanilla-extract/src/icons/vanilla-extract.tsx b/examples/using-vanilla-extract/src/icons/vanilla-extract.tsx
new file mode 100644
index 0000000000000..d9c30a2a1614e
--- /dev/null
+++ b/examples/using-vanilla-extract/src/icons/vanilla-extract.tsx
@@ -0,0 +1,194 @@
+import * as React from "react"
+
+function VanillaExtractIcon() {
+ return (
+
+ )
+}
+export default VanillaExtractIcon
diff --git a/examples/using-vanilla-extract/src/pages/404.tsx b/examples/using-vanilla-extract/src/pages/404.tsx
new file mode 100644
index 0000000000000..c066a81c62002
--- /dev/null
+++ b/examples/using-vanilla-extract/src/pages/404.tsx
@@ -0,0 +1,10 @@
+import * as React from "react"
+import * as styles from "../styles/404.css"
+
+const NotFoundPage = () => {
+ return (
+ 404
+ )
+}
+
+export default NotFoundPage
diff --git a/examples/using-vanilla-extract/src/pages/index.tsx b/examples/using-vanilla-extract/src/pages/index.tsx
new file mode 100644
index 0000000000000..f35a599409536
--- /dev/null
+++ b/examples/using-vanilla-extract/src/pages/index.tsx
@@ -0,0 +1,33 @@
+import * as React from "react"
+import VanillaExtractIcon from "../icons/vanilla-extract"
+import * as styles from "../styles/index.css"
+import "../styles/global.css"
+import { ColorModeProvider, ColorModeToggle } from "../components/ColorModeToggle"
+
+class IndexPage extends React.Component {
+ render() {
+ return (
+
+
+
+ )
+ }
+}
+
+export default IndexPage
diff --git a/examples/using-vanilla-extract/src/styles/404.css.ts b/examples/using-vanilla-extract/src/styles/404.css.ts
new file mode 100644
index 0000000000000..738a55c0dea48
--- /dev/null
+++ b/examples/using-vanilla-extract/src/styles/404.css.ts
@@ -0,0 +1,11 @@
+import { style } from "@vanilla-extract/css"
+
+export const wrapper = style({
+ color: "rebeccapurple",
+ height: "100vh",
+ width: "100vw",
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ fontSize: "10rem",
+})
diff --git a/examples/using-vanilla-extract/src/styles/global.css.ts b/examples/using-vanilla-extract/src/styles/global.css.ts
new file mode 100644
index 0000000000000..aea4fb106669a
--- /dev/null
+++ b/examples/using-vanilla-extract/src/styles/global.css.ts
@@ -0,0 +1,55 @@
+import { globalStyle } from "@vanilla-extract/css"
+
+export const rootColors = {
+ light: {
+ bg: `white`,
+ color: `black`,
+ },
+ dark: {
+ bg: `#0B1222`,
+ color: `#CBD5E1`
+ }
+}
+
+globalStyle(`*`, {
+ boxSizing: `border-box`,
+ margin: 0,
+})
+
+globalStyle(`.dark`, {
+ background: rootColors.dark.bg,
+ color: rootColors.dark.color
+})
+
+globalStyle(`html`, {
+ background: rootColors.light.bg,
+ color: rootColors.light.color
+})
+
+globalStyle(`html, body`, {
+ height: `100%`,
+ fontSize: `18px`,
+ fontFamily: `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`,
+})
+
+globalStyle(`body`, {
+ lineHeight: 1.5,
+ WebkitFontSmoothing: `antialiased`,
+})
+
+globalStyle(`img, picture, video, canvas, svg`, {
+ display: `block`,
+ maxWidth: `100%`,
+})
+
+globalStyle(`input, button, textare, select`, {
+ font: `inherit`,
+})
+
+globalStyle(`p, h1, h2, h3, h4, h5, h6`, {
+ overflowWrap: `break-word`,
+})
+
+globalStyle(`___gatsby`, {
+ isolation: `isolate`,
+})
diff --git a/examples/using-vanilla-extract/src/styles/index.css.ts b/examples/using-vanilla-extract/src/styles/index.css.ts
new file mode 100644
index 0000000000000..76f816c8102bc
--- /dev/null
+++ b/examples/using-vanilla-extract/src/styles/index.css.ts
@@ -0,0 +1,64 @@
+import { style, createVar } from "@vanilla-extract/css"
+
+const shadowColor = createVar()
+
+export const wrapper = style({
+ height: "100vh",
+ width: "100vw",
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+})
+
+export const container = style({
+ maxWidth: `800px`,
+ textAlign: `right`
+})
+
+export const spacer = style({
+ width: `1px`,
+ height: `2rem`,
+ display: `block`,
+ minHeight: `2rem`,
+ minWidth: `1px`
+})
+
+export const title = style({
+ fontSize: `1.5rem`,
+ marginTop: `0.5rem`,
+ lineHeight: 1.5,
+ color: `rebeccapurple`,
+})
+
+export const content = style({
+ padding: `4em`,
+ background: `#ccfbf1`,
+ textAlign: `left`,
+ borderRadius: `0.25rem`,
+ color: `black`,
+ vars: {
+ [shadowColor]: `168deg 34% 56%`
+ },
+ boxShadow: `0px 0.7px 0.8px hsl(${shadowColor} / 0.34),
+ 0px 4.3px 4.8px -0.4px hsl(${shadowColor} / 0.34),
+ 0px 8.1px 9.1px -0.7px hsl(${shadowColor} / 0.34),
+ 0.1px 13.3px 15px -1.1px hsl(${shadowColor} / 0.34),
+ 0.1px 21.3px 24px -1.4px hsl(${shadowColor} / 0.34),
+ 0.2px 33.2px 37.4px -1.8px hsl(${shadowColor} / 0.34),
+ 0.2px 50.5px 56.8px -2.1px hsl(${shadowColor} / 0.34),
+ 0.4px 74.4px 83.7px -2.5px hsl(${shadowColor} / 0.34)`
+})
+
+export const button = style({
+ background: `rebeccapurple`,
+ textDecoration: `none`,
+ color: `white`,
+ padding: `0.5rem 0.75rem`,
+ border: `none`,
+ borderRadius: `0.25rem`,
+ fontWeight: `bold`,
+ transition: `background 0.3s ease-in-out`,
+ ":hover": {
+ background: `#8c53c6`
+ }
+})
diff --git a/integration-tests/cache-resilience/gatsby-node.js b/integration-tests/cache-resilience/gatsby-node.js
index 21ab949eb533a..3847cc1170211 100644
--- a/integration-tests/cache-resilience/gatsby-node.js
+++ b/integration-tests/cache-resilience/gatsby-node.js
@@ -3,7 +3,7 @@ const v8 = require(`v8`)
const glob = require(`glob`)
const path = require(`path`)
const _ = require(`lodash`)
-const { open } = require(`lmdb-store`)
+const { open } = require(`lmdb`)
const { saveState } = require(`gatsby/dist/redux/save-state`)
diff --git a/integration-tests/gatsby-cli/__tests__/build.js b/integration-tests/gatsby-cli/__tests__/build.js
index e31a54dee070a..f1a0a648df841 100644
--- a/integration-tests/gatsby-cli/__tests__/build.js
+++ b/integration-tests/gatsby-cli/__tests__/build.js
@@ -12,9 +12,8 @@ describe(`gatsby build`, () => {
it(`creates a built gatsby site`, () => {
const [code, logs] = GatsbyCLI.from(cwd).invoke(`build`)
- logs.should.contain(
- `success open and validate gatsby-configs, load plugins`
- )
+ logs.should.contain(`success load gatsby config`)
+ logs.should.contain(`success load plugins`)
logs.should.contain(`success onPreInit`)
logs.should.contain(`success initialize cache`)
logs.should.contain(`success copy gatsby files`)
diff --git a/integration-tests/gatsby-cli/__tests__/develop.js b/integration-tests/gatsby-cli/__tests__/develop.js
index db41335abe2f7..591b83aabb577 100644
--- a/integration-tests/gatsby-cli/__tests__/develop.js
+++ b/integration-tests/gatsby-cli/__tests__/develop.js
@@ -26,9 +26,8 @@ describe(`gatsby develop`, () => {
// 3. Make sure logs for the user contain expected results
const logs = getLogs()
- logs.should.contain(
- `success open and validate gatsby-configs, load plugins`
- )
+ logs.should.contain(`success load gatsby config`)
+ logs.should.contain(`success load plugins`)
logs.should.contain(`success onPreInit`)
logs.should.contain(`success initialize cache`)
logs.should.contain(`success copy gatsby files`)
diff --git a/integration-tests/gatsby-cli/__tests__/repl.js b/integration-tests/gatsby-cli/__tests__/repl.js
index 9eec28120ea7f..55ff0a8dfc0ad 100644
--- a/integration-tests/gatsby-cli/__tests__/repl.js
+++ b/integration-tests/gatsby-cli/__tests__/repl.js
@@ -21,9 +21,8 @@ describe(`gatsby repl`, () => {
// 3. Make assertions
const logs = getLogs()
- logs.should.contain(
- `success open and validate gatsby-configs, load plugins`
- )
+ logs.should.contain(`success load gatsby config`)
+ logs.should.contain(`success load plugins`)
logs.should.contain(`success onPreInit`)
logs.should.contain(`success initialize cache`)
logs.should.contain(`success copy gatsby files`)
diff --git a/integration-tests/node-manifest/__tests__/create-node-manifest.test.js b/integration-tests/node-manifest/__tests__/create-node-manifest.test.js
index d9967cf1dbdf4..c9416bb78f896 100644
--- a/integration-tests/node-manifest/__tests__/create-node-manifest.test.js
+++ b/integration-tests/node-manifest/__tests__/create-node-manifest.test.js
@@ -70,6 +70,14 @@ describe(`Node Manifest API in "gatsby ${gatsbyCommandName}"`, () => {
expect(manifestFileContents.foundPageBy).toBe(`context.id`)
})
+ it(`Creates an accurate node manifest when ownerNodeId isn't present but there's a matching "slug" in pageContext`, async () => {
+ const manifestFileContents = await getManifestContents(5)
+
+ expect(manifestFileContents.node.id).toBe(`5`)
+ expect(manifestFileContents.page.path).toBe(`/slug-test-path`)
+ expect(manifestFileContents.foundPageBy).toBe(`context.slug`)
+ })
+
if (gatsbyCommandName === `build`) {
// this doesn't work in gatsby develop since query tracking
// only runs when visiting a page in browser.
diff --git a/integration-tests/node-manifest/gatsby-node.js b/integration-tests/node-manifest/gatsby-node.js
index 6277c558dc425..8a9c03c34b46d 100644
--- a/integration-tests/node-manifest/gatsby-node.js
+++ b/integration-tests/node-manifest/gatsby-node.js
@@ -4,7 +4,7 @@ const createManifestId = nodeId => `${commandName}-${nodeId}`
exports.sourceNodes = ({ actions }) => {
// template nodes
- for (let id = 1; id < 5; id++) {
+ for (let id = 1; id < 6; id++) {
const node = {
id: `${id}`,
internal: {
@@ -13,6 +13,10 @@ exports.sourceNodes = ({ actions }) => {
},
}
+ if (id === 5) {
+ node.slug = `test-slug`
+ }
+
actions.createNode(node)
actions.unstable_createNodeManifest({
@@ -108,4 +112,12 @@ exports.createPages = ({ actions }) => {
path: `three-alternative`,
component: require.resolve(`./src/templates/three.js`),
})
+
+ actions.createPage({
+ path: `slug-test-path`,
+ context: {
+ slug: `test-slug`,
+ },
+ component: require.resolve(`./src/templates/four.js`),
+ })
}
diff --git a/integration-tests/node-manifest/src/templates/four.js b/integration-tests/node-manifest/src/templates/four.js
new file mode 100644
index 0000000000000..1874dfccbc20f
--- /dev/null
+++ b/integration-tests/node-manifest/src/templates/four.js
@@ -0,0 +1,17 @@
+import { graphql } from "gatsby"
+import React from "react"
+
+export default function Four({ data }) {
+ return Template 4. Node by slug {data.testNode.slug}
+}
+
+export const query = graphql`
+ query SLUG_TEST($slug: String) {
+ testNode(slug: { eq: $slug }) {
+ id
+ }
+ otherNode: testNode(id: { eq: "2" }) {
+ id
+ }
+ }
+`
diff --git a/integration-tests/ssr/__tests__/ssr.js b/integration-tests/ssr/__tests__/ssr.js
index 5e9e33b77bbea..5f2d96abf613d 100644
--- a/integration-tests/ssr/__tests__/ssr.js
+++ b/integration-tests/ssr/__tests__/ssr.js
@@ -33,7 +33,7 @@ describe(`SSR`, () => {
expect(String(childProcess.stdout)).toContain(
`testing these paths for differences between dev & prod outputs`
)
- }, 30000)
+ }, 60000)
test(`it generates an error page correctly`, async () => {
const src = path.join(__dirname, `/fixtures/bad-page.js`)
diff --git a/integration-tests/ssr/test-output.js b/integration-tests/ssr/test-output.js
index c3a205bebb92f..9b590f900b009 100644
--- a/integration-tests/ssr/test-output.js
+++ b/integration-tests/ssr/test-output.js
@@ -47,10 +47,10 @@ async function run() {
// Fetch once to trigger re-compilation.
await fetch(`${devSiteBasePath}/${path}`)
- // Then wait for a second to ensure it's ready to go.
+ // Then wait for six seconds to ensure it's ready to go.
// Otherwise, tests are flaky depending on the speed of the testing machine.
await new Promise(resolve => {
- setTimeout(() => resolve(), 1000)
+ setTimeout(() => resolve(), 6000)
})
let devStatus = 200
diff --git a/integration-tests/structured-logging/__tests__/to-do.js b/integration-tests/structured-logging/__tests__/to-do.js
index d7277b807709a..5de5df637e747 100644
--- a/integration-tests/structured-logging/__tests__/to-do.js
+++ b/integration-tests/structured-logging/__tests__/to-do.js
@@ -136,6 +136,12 @@ const commonAssertions = events => {
timestamp: joi.string().required(),
}),
+ joi.object({
+ type: joi.string().required().valid(`GATSBY_CONFIG_KEYS`),
+ payload: joi.object().required(),
+ timestamp: joi.string().required(),
+ }),
+
joi.object({
type: joi.string().required().valid(`RENDER_PAGE_TREE`),
payload: joi.object(),
diff --git a/jest-transformer.js b/jest-transformer.js
index 02167a152534c..94411b7b1d426 100644
--- a/jest-transformer.js
+++ b/jest-transformer.js
@@ -2,4 +2,11 @@ const babelJest = require(`babel-jest`)
module.exports = babelJest.default.createTransformer({
presets: [`babel-preset-gatsby-package`],
+ babelrcRoots: [
+ // Keep the root as a root
+ `.`,
+
+ // Also consider monorepo packages "root" and load their .babelrc files.
+ `./packages/*`,
+ ],
})
diff --git a/jest.config.js b/jest.config.js
index 7638a3b00a513..f30f5b8353b19 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -46,6 +46,8 @@ module.exports = {
"^weak-lru-cache$": `/node_modules/weak-lru-cache/dist/index.cjs`,
"^ordered-binary$": `/node_modules/ordered-binary/dist/index.cjs`,
"^msgpackr$": `/node_modules/msgpackr/dist/node.cjs`,
+ "^gatsby-page-utils/(.*)$": `gatsby-page-utils/dist/$1`, // Workaround for https://github.com/facebook/jest/issues/9771
+ "^gatsby-core-utils/(.*)$": `gatsby-core-utils/dist/$1`, // Workaround for https://github.com/facebook/jest/issues/9771
},
snapshotSerializers: [`jest-serializer-path`],
collectCoverageFrom: coverageDirs,
diff --git a/package.json b/package.json
index 04f979e8d68c7..62e3ca73ac777 100644
--- a/package.json
+++ b/package.json
@@ -85,7 +85,7 @@
"retext-syntax-urls": "^2.0.0",
"rimraf": "^3.0.2",
"svgo": "1.3.2",
- "typescript": "^4.5.4",
+ "typescript": "^4.5.5",
"unified": "^9.2.0",
"yargs": "^15.4.1"
},
diff --git a/packages/babel-plugin-remove-graphql-queries/CHANGELOG.md b/packages/babel-plugin-remove-graphql-queries/CHANGELOG.md
index b7c296c4cf7ef..42b04b4b936fe 100644
--- a/packages/babel-plugin-remove-graphql-queries/CHANGELOG.md
+++ b/packages/babel-plugin-remove-graphql-queries/CHANGELOG.md
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [4.7.0](https://github.com/gatsbyjs/gatsby/commits/babel-plugin-remove-graphql-queries@4.7.0/packages/babel-plugin-remove-graphql-queries) (2022-02-08)
+
+[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.7)
+
+**Note:** Version bump only for package babel-plugin-remove-graphql-queries
+
+## [4.6.0](https://github.com/gatsbyjs/gatsby/commits/babel-plugin-remove-graphql-queries@4.6.0/packages/babel-plugin-remove-graphql-queries) (2022-01-25)
+
+[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.6)
+
+**Note:** Version bump only for package babel-plugin-remove-graphql-queries
+
+### [4.5.2](https://github.com/gatsbyjs/gatsby/commits/babel-plugin-remove-graphql-queries@4.5.2/packages/babel-plugin-remove-graphql-queries) (2022-01-17)
+
+**Note:** Version bump only for package babel-plugin-remove-graphql-queries
+
### [4.5.1](https://github.com/gatsbyjs/gatsby/commits/babel-plugin-remove-graphql-queries@4.5.1/packages/babel-plugin-remove-graphql-queries) (2022-01-12)
**Note:** Version bump only for package babel-plugin-remove-graphql-queries
diff --git a/packages/babel-plugin-remove-graphql-queries/package.json b/packages/babel-plugin-remove-graphql-queries/package.json
index 74820bd522343..a51687b0b0c85 100644
--- a/packages/babel-plugin-remove-graphql-queries/package.json
+++ b/packages/babel-plugin-remove-graphql-queries/package.json
@@ -1,6 +1,6 @@
{
"name": "babel-plugin-remove-graphql-queries",
- "version": "4.6.0-next.1",
+ "version": "4.8.0-next.1",
"author": "Jason Quense ",
"repository": {
"type": "git",
@@ -10,12 +10,12 @@
"homepage": "https://github.com/gatsbyjs/gatsby/tree/master/packages/babel-plugin-remove-graphql-queries#readme",
"dependencies": {
"@babel/runtime": "^7.15.4",
- "gatsby-core-utils": "^3.6.0-next.1"
+ "gatsby-core-utils": "^3.8.0-next.1"
},
"devDependencies": {
"@babel/cli": "^7.15.4",
"@babel/core": "^7.15.5",
- "babel-preset-gatsby-package": "^2.6.0-next.0",
+ "babel-preset-gatsby-package": "^2.8.0-next.0",
"cross-env": "^7.0.3"
},
"peerDependencies": {
diff --git a/packages/babel-preset-gatsby-package/CHANGELOG.md b/packages/babel-preset-gatsby-package/CHANGELOG.md
index df335e820e0f5..8817d43da696f 100644
--- a/packages/babel-preset-gatsby-package/CHANGELOG.md
+++ b/packages/babel-preset-gatsby-package/CHANGELOG.md
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [2.7.0](https://github.com/gatsbyjs/gatsby/commits/babel-preset-gatsby-package@2.7.0/packages/babel-preset-gatsby-package) (2022-02-08)
+
+[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.7)
+
+**Note:** Version bump only for package babel-preset-gatsby-package
+
+## [2.6.0](https://github.com/gatsbyjs/gatsby/commits/babel-preset-gatsby-package@2.6.0/packages/babel-preset-gatsby-package) (2022-01-25)
+
+[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.6)
+
+**Note:** Version bump only for package babel-preset-gatsby-package
+
## [2.5.0](https://github.com/gatsbyjs/gatsby/commits/babel-preset-gatsby-package@2.5.0/packages/babel-preset-gatsby-package) (2022-01-11)
[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.5)
diff --git a/packages/babel-preset-gatsby-package/package.json b/packages/babel-preset-gatsby-package/package.json
index aeb3b1d93fb78..dc9eccd2d54d5 100644
--- a/packages/babel-preset-gatsby-package/package.json
+++ b/packages/babel-preset-gatsby-package/package.json
@@ -1,6 +1,6 @@
{
"name": "babel-preset-gatsby-package",
- "version": "2.6.0-next.0",
+ "version": "2.8.0-next.0",
"author": "Philipp Spiess ",
"repository": {
"type": "git",
diff --git a/packages/babel-preset-gatsby/CHANGELOG.md b/packages/babel-preset-gatsby/CHANGELOG.md
index 6bfc11efd8001..65f0355d4bde6 100644
--- a/packages/babel-preset-gatsby/CHANGELOG.md
+++ b/packages/babel-preset-gatsby/CHANGELOG.md
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [2.7.0](https://github.com/gatsbyjs/gatsby/commits/babel-preset-gatsby@2.7.0/packages/babel-preset-gatsby) (2022-02-08)
+
+[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.7)
+
+**Note:** Version bump only for package babel-preset-gatsby
+
+## [2.6.0](https://github.com/gatsbyjs/gatsby/commits/babel-preset-gatsby@2.6.0/packages/babel-preset-gatsby) (2022-01-25)
+
+[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.6)
+
+**Note:** Version bump only for package babel-preset-gatsby
+
+### [2.5.2](https://github.com/gatsbyjs/gatsby/commits/babel-preset-gatsby@2.5.2/packages/babel-preset-gatsby) (2022-01-17)
+
+**Note:** Version bump only for package babel-preset-gatsby
+
### [2.5.1](https://github.com/gatsbyjs/gatsby/commits/babel-preset-gatsby@2.5.1/packages/babel-preset-gatsby) (2022-01-12)
**Note:** Version bump only for package babel-preset-gatsby
diff --git a/packages/babel-preset-gatsby/package.json b/packages/babel-preset-gatsby/package.json
index 27b1f0dd55f43..193bf4c93c44b 100644
--- a/packages/babel-preset-gatsby/package.json
+++ b/packages/babel-preset-gatsby/package.json
@@ -1,6 +1,6 @@
{
"name": "babel-preset-gatsby",
- "version": "2.6.0-next.1",
+ "version": "2.8.0-next.1",
"author": "Philipp Spiess ",
"repository": {
"type": "git",
@@ -22,8 +22,8 @@
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-macros": "^2.8.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
- "gatsby-core-utils": "^3.6.0-next.1",
- "gatsby-legacy-polyfills": "^2.6.0-next.0"
+ "gatsby-core-utils": "^3.8.0-next.1",
+ "gatsby-legacy-polyfills": "^2.8.0-next.0"
},
"peerDependencies": {
"@babel/core": "^7.11.6",
@@ -38,7 +38,7 @@
},
"devDependencies": {
"@babel/cli": "^7.15.4",
- "babel-preset-gatsby-package": "^2.6.0-next.0",
+ "babel-preset-gatsby-package": "^2.8.0-next.0",
"cross-env": "^7.0.3",
"slash": "^3.0.0"
},
diff --git a/packages/create-gatsby/CHANGELOG.md b/packages/create-gatsby/CHANGELOG.md
index 3c8815440c736..4827fe6358712 100644
--- a/packages/create-gatsby/CHANGELOG.md
+++ b/packages/create-gatsby/CHANGELOG.md
@@ -3,6 +3,40 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [2.7.0](https://github.com/gatsbyjs/gatsby/commits/create-gatsby@2.7.0/packages/create-gatsby) (2022-02-08)
+
+[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.7)
+
+#### Bug Fixes
+
+- Use MDX v1 [#34710](https://github.com/gatsbyjs/gatsby/issues/34710) ([03a1863](https://github.com/gatsbyjs/gatsby/commit/03a18632e2764e3f1b4b9c80e050282a92e0834c))
+
+#### Refactoring
+
+- Decouple package functions [#34606](https://github.com/gatsbyjs/gatsby/issues/34606) ([dcbdc0c](https://github.com/gatsbyjs/gatsby/commit/dcbdc0cb46713cc90157882e3f7c23b5f03a27c3))
+
+### [2.6.1](https://github.com/gatsbyjs/gatsby/commits/create-gatsby@2.6.1/packages/create-gatsby) (2022-02-04)
+
+#### Bug Fixes
+
+- Use MDX v1 [#34710](https://github.com/gatsbyjs/gatsby/issues/34710) [#34718](https://github.com/gatsbyjs/gatsby/issues/34718) ([8b7b0e1](https://github.com/gatsbyjs/gatsby/commit/8b7b0e17624b01748f721426071660f6479f6e09))
+
+## [2.6.0](https://github.com/gatsbyjs/gatsby/commits/create-gatsby@2.6.0/packages/create-gatsby) (2022-01-25)
+
+[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.6)
+
+#### Bug Fixes
+
+- Respect telemetry disable [#34495](https://github.com/gatsbyjs/gatsby/issues/34495) ([44b2ef5](https://github.com/gatsbyjs/gatsby/commit/44b2ef5905801d1b40a15313966867bd3d410be7))
+- Re-Add plugin-add functionality [#34482](https://github.com/gatsbyjs/gatsby/issues/34482) ([618b32b](https://github.com/gatsbyjs/gatsby/commit/618b32b17751c76ea1b1a6f4fbc91da928bd18c1))
+
+### [2.5.1](https://github.com/gatsbyjs/gatsby/commits/create-gatsby@2.5.1/packages/create-gatsby) (2022-01-17)
+
+#### Bug Fixes
+
+- Respect telemetry disable [#34495](https://github.com/gatsbyjs/gatsby/issues/34495) [#34511](https://github.com/gatsbyjs/gatsby/issues/34511) ([9f9cabf](https://github.com/gatsbyjs/gatsby/commit/9f9cabfe05ba89f7e2e94fe29957dcdd610f4a43))
+- Re-Add plugin-add functionality [#34482](https://github.com/gatsbyjs/gatsby/issues/34482) [#34510](https://github.com/gatsbyjs/gatsby/issues/34510) ([0f5f7e4](https://github.com/gatsbyjs/gatsby/commit/0f5f7e46ca4e803a1f43059e5de984ce8cd150f3))
+
## [2.5.0](https://github.com/gatsbyjs/gatsby/commits/create-gatsby@2.5.0/packages/create-gatsby) (2022-01-11)
[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.5)
diff --git a/packages/create-gatsby/README.md b/packages/create-gatsby/README.md
index ee6030686f8c5..1d6fcbfb4c446 100644
--- a/packages/create-gatsby/README.md
+++ b/packages/create-gatsby/README.md
@@ -62,5 +62,5 @@ Open another terminal window and go to a folder where you can easily delete the
cd
# Run the create-gatsby script
-node /packages/create-gatsby/cli.js
```
diff --git a/packages/create-gatsby/package.json b/packages/create-gatsby/package.json
index 3a82b8818fe6d..57985daa98308 100644
--- a/packages/create-gatsby/package.json
+++ b/packages/create-gatsby/package.json
@@ -1,6 +1,6 @@
{
"name": "create-gatsby",
- "version": "2.6.0-next.2",
+ "version": "2.8.0-next.2",
"main": "lib/index.js",
"bin": "cli.js",
"license": "MIT",
@@ -28,7 +28,7 @@
"eslint": "^7.32.0",
"execa": "^5.1.1",
"fs-extra": "^10.0.0",
- "gatsby-plugin-utils": "^3.0.0-next.0",
+ "gatsby-plugin-utils": "^3.2.0-next.1",
"joi": "^17.4.2",
"microbundle": "^0.14.2",
"node-fetch": "^2.6.6",
diff --git a/packages/create-gatsby/src/__tests__/init-starter.ts b/packages/create-gatsby/src/__tests__/init-starter.ts
index d2bc51369ceef..714ff65837867 100644
--- a/packages/create-gatsby/src/__tests__/init-starter.ts
+++ b/packages/create-gatsby/src/__tests__/init-starter.ts
@@ -4,20 +4,21 @@ import execa from "execa"
import fs from "fs-extra"
import path from "path"
import { initStarter } from "../init-starter"
-import { reporter } from "../reporter"
+import { reporter } from "../utils/reporter"
jest.mock(`tiny-spin`, () => {
return {
spin: (): (() => void) => jest.fn(),
}
})
-jest.mock(`../utils`)
+jest.mock(`../utils/clear-line`)
+jest.mock(`../utils/make-npm-safe`)
jest.mock(`execa`)
jest.mock(`child_process`)
jest.mock(`fs-extra`)
jest.mock(`path`)
-jest.mock(`../reporter`)
-jest.mock(`../get-config-store`, () => {
+jest.mock(`../utils/reporter`)
+jest.mock(`../utils/get-config-store`, () => {
return {
getConfigStore: (): unknown => {
return {
diff --git a/packages/create-gatsby/src/__tests__/install-plugins.ts b/packages/create-gatsby/src/__tests__/install-plugins.ts
index 41f6ffe22339d..6db6c1356bcf3 100644
--- a/packages/create-gatsby/src/__tests__/install-plugins.ts
+++ b/packages/create-gatsby/src/__tests__/install-plugins.ts
@@ -1,9 +1,9 @@
import { installPlugins } from "../install-plugins"
-import { reporter } from "../reporter"
-import { requireResolve } from "../require-utils"
+import { reporter } from "../utils/reporter"
+import { requireResolve } from "../utils/require-utils"
-jest.mock(`../require-utils`)
-jest.mock(`../reporter`)
+jest.mock(`../utils/require-utils`)
+jest.mock(`../utils/reporter`)
jest.mock(
`somewhere-virtually-existing`,
diff --git a/packages/create-gatsby/src/__tests__/run.ts b/packages/create-gatsby/src/__tests__/run.ts
new file mode 100644
index 0000000000000..48d17db23ec5b
--- /dev/null
+++ b/packages/create-gatsby/src/__tests__/run.ts
@@ -0,0 +1,255 @@
+import { reporter } from "../utils/reporter"
+import { initStarter } from "../init-starter"
+import { trackCli } from "../tracking"
+import { run, DEFAULT_STARTERS } from "../index"
+
+jest.mock(`../utils/parse-args`)
+jest.mock(`enquirer`, () => {
+ const OriginalEnquirer = jest.requireActual(`enquirer`)
+
+ class MockedEnquirer extends OriginalEnquirer {
+ constructor() {
+ super()
+ // Turns waiting for user input off and autofills with answers
+ this.options = { show: false, autofill: true }
+
+ // Mock answers
+ this.answers = {
+ // First prompt answer
+ name: `hello-world`,
+
+ // Main question set answers
+ project: `hello-world`,
+ language: `js`,
+ cms: `none`,
+ styling: `none`,
+ features: [],
+
+ // Confirmation prompt answer
+ confirm: true,
+ }
+ }
+ }
+ return MockedEnquirer
+})
+jest.mock(`../utils/reporter`)
+jest.mock(`../tracking`, () => {
+ return {
+ trackCli: jest.fn(),
+ }
+})
+jest.mock(`../init-starter`, () => {
+ return {
+ initStarter: jest.fn(),
+ getPackageManager: jest.fn(),
+ gitSetup: jest.fn(),
+ }
+})
+jest.mock(`../install-plugins`, () => {
+ return {
+ installPlugins: jest.fn(),
+ }
+})
+jest.mock(`../utils/site-metadata`, () => {
+ return {
+ setSiteMetadata: jest.fn(),
+ }
+})
+jest.mock(`../utils/hash`, () => {
+ return {
+ sha256: jest.fn(args => args),
+ md5: jest.fn(args => args),
+ }
+})
+jest.mock(`../utils/question-helpers`, () => {
+ const originalQuestionHelpers = jest.requireActual(
+ `../utils/question-helpers`
+ )
+ return {
+ ...originalQuestionHelpers,
+ validateProjectName: jest.fn(() => true),
+ }
+})
+jest.mock(`../components/utils`, () => {
+ return {
+ center: jest.fn(args => args),
+ wrap: jest.fn(args => args),
+ }
+})
+
+const dirName = `hello-world`
+let parseArgsMock
+
+describe(`run`, () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ parseArgsMock = require(`../utils/parse-args`).parseArgs
+ })
+
+ describe(`no skip flag`, () => {
+ beforeEach(() => {
+ parseArgsMock.mockReturnValueOnce({
+ flags: { yes: false },
+ dirName,
+ })
+ })
+
+ it(`should welcome the user`, async () => {
+ await run()
+ expect(reporter.info).toHaveBeenCalledWith(
+ expect.stringContaining(`Welcome to Gatsby!`)
+ )
+ })
+ it(`should communicate setup questions will be asked`, async () => {
+ await run()
+ expect(reporter.info).toHaveBeenCalledWith(
+ expect.stringContaining(
+ `This command will generate a new Gatsby site for you`
+ )
+ )
+ })
+ it(`should confirm actions`, async () => {
+ await run()
+ expect(reporter.info).toHaveBeenCalledWith(
+ expect.stringContaining(`Thanks! Here's what we'll now do`)
+ )
+ })
+ it(`should notify of successful site creation`, async () => {
+ await run()
+ expect(reporter.success).toHaveBeenCalledWith(
+ expect.stringContaining(`Created site`)
+ )
+ })
+ })
+
+ describe(`skip flag`, () => {
+ beforeEach(() => {
+ parseArgsMock.mockReturnValueOnce({
+ flags: { yes: true },
+ dirName,
+ })
+ })
+
+ it(`should welcome the user`, async () => {
+ await run()
+ expect(reporter.info).toHaveBeenCalledWith(
+ expect.stringContaining(`Welcome to Gatsby!`)
+ )
+ })
+ it(`should not communicate setup questions`, async () => {
+ await run()
+ expect(reporter.info).not.toHaveBeenCalledWith(
+ expect.stringContaining(
+ `This command will generate a new Gatsby site for you`
+ )
+ )
+ })
+ it(`should not confirm actions`, async () => {
+ await run()
+ expect(reporter.info).not.toHaveBeenCalledWith(
+ expect.stringContaining(`Thanks! Here's what we'll now do`)
+ )
+ })
+ it(`should notify of successful site creation`, async () => {
+ await run()
+ expect(reporter.success).toHaveBeenCalledWith(
+ expect.stringContaining(`Created site`)
+ )
+ })
+ it(`should use the JS starter by default`, async () => {
+ await run()
+ expect(initStarter).toHaveBeenCalledWith(
+ DEFAULT_STARTERS.js,
+ dirName,
+ [],
+ dirName
+ )
+ })
+ it(`should track JS was selected as language`, async () => {
+ await run()
+ expect(trackCli).toHaveBeenCalledWith(`CREATE_GATSBY_SELECT_OPTION`, {
+ name: `LANGUAGE`,
+ valueString: `js`,
+ })
+ })
+ })
+
+ describe(`no ts flag`, () => {
+ beforeEach(() => {
+ parseArgsMock.mockReturnValueOnce({
+ flags: { ts: false },
+ dirName,
+ })
+ })
+
+ it(`should use the JS starter`, async () => {
+ await run()
+ expect(initStarter).toHaveBeenCalledWith(
+ DEFAULT_STARTERS.js,
+ dirName,
+ [],
+ dirName
+ )
+ })
+ it(`should track JS was selected as language`, async () => {
+ await run()
+ expect(trackCli).toHaveBeenCalledWith(`CREATE_GATSBY_SELECT_OPTION`, {
+ name: `LANGUAGE`,
+ valueString: `js`,
+ })
+ })
+ })
+
+ describe(`ts flag`, () => {
+ beforeEach(() => {
+ parseArgsMock.mockReturnValueOnce({
+ flags: { ts: true },
+ dirName,
+ })
+ })
+
+ it(`should use the TS starter`, async () => {
+ await run()
+ expect(initStarter).toHaveBeenCalledWith(
+ DEFAULT_STARTERS.ts,
+ dirName,
+ [],
+ dirName
+ )
+ })
+
+ it(`should track TS was selected as language`, async () => {
+ await run()
+ expect(trackCli).toHaveBeenCalledWith(`CREATE_GATSBY_SELECT_OPTION`, {
+ name: `LANGUAGE`,
+ valueString: `ts`,
+ })
+ })
+ })
+})
+
+describe(`skip and ts flag`, () => {
+ beforeEach(() => {
+ parseArgsMock.mockReturnValueOnce({
+ flags: { yes: true, ts: true },
+ dirName,
+ })
+ })
+
+ it(`should use the TS starter`, async () => {
+ await run()
+ expect(initStarter).toHaveBeenCalledWith(
+ DEFAULT_STARTERS.ts,
+ dirName,
+ [],
+ dirName
+ )
+ })
+ it(`should track TS was selected as language`, async () => {
+ await run()
+ expect(trackCli).toHaveBeenCalledWith(`CREATE_GATSBY_SELECT_OPTION`, {
+ name: `LANGUAGE`,
+ valueString: `ts`,
+ })
+ })
+})
diff --git a/packages/create-gatsby/src/__tests__/tracking.ts b/packages/create-gatsby/src/__tests__/tracking.ts
new file mode 100644
index 0000000000000..04b66679b83b3
--- /dev/null
+++ b/packages/create-gatsby/src/__tests__/tracking.ts
@@ -0,0 +1,61 @@
+let isTrackingEnabled: () => boolean
+
+const get = jest.fn()
+const set = jest.fn()
+
+jest.doMock(`../utils/get-config-store`, () => {
+ return {
+ getConfigStore: (): unknown => {
+ return {
+ get,
+ set,
+ }
+ },
+ }
+})
+
+describe(`isTrackingEnabled`, () => {
+ beforeEach(() => {
+ jest.resetModules()
+ isTrackingEnabled = require(`../tracking`).isTrackingEnabled
+ })
+
+ it(`is enabled by default`, () => {
+ const enabled = isTrackingEnabled()
+ expect(enabled).toBeTrue()
+ })
+
+ it(`respects the setting of the config store`, () => {
+ get.mockImplementationOnce(key => {
+ if (key === `telemetry.enabled`) {
+ return false
+ } else {
+ return true
+ }
+ })
+
+ const enabled = isTrackingEnabled()
+ expect(enabled).toBeFalse()
+
+ const cachedEnabled = isTrackingEnabled()
+ expect(cachedEnabled).toBeFalse()
+ })
+
+ describe(`process.env.GATSBY_TELEMETRY_DISABLED`, () => {
+ beforeAll(() => {
+ process.env.GATSBY_TELEMETRY_DISABLED = `true`
+ })
+
+ it(`respects the setting of the environment variable`, () => {
+ const enabled = isTrackingEnabled()
+ expect(enabled).toBeFalse()
+
+ const cachedEnabled = isTrackingEnabled()
+ expect(cachedEnabled).toBeFalse()
+ })
+
+ afterAll(() => {
+ process.env.GATSBY_TELEMETRY_DISABLED = undefined
+ })
+ })
+})
diff --git a/packages/create-gatsby/src/__tests__/utils.ts b/packages/create-gatsby/src/__tests__/utils/make-npm-safe.ts
similarity index 93%
rename from packages/create-gatsby/src/__tests__/utils.ts
rename to packages/create-gatsby/src/__tests__/utils/make-npm-safe.ts
index f806336ea176e..80e27402a0fcb 100644
--- a/packages/create-gatsby/src/__tests__/utils.ts
+++ b/packages/create-gatsby/src/__tests__/utils/make-npm-safe.ts
@@ -1,4 +1,4 @@
-import { makeNpmSafe } from "../utils"
+import { makeNpmSafe } from "../../utils/make-npm-safe"
const tests = [
[`A gatsby SiteHere`, `a-gatsby-site-here`],
diff --git a/packages/create-gatsby/src/__tests__/utils/parse-args.ts b/packages/create-gatsby/src/__tests__/utils/parse-args.ts
new file mode 100644
index 0000000000000..21f086b0c3d20
--- /dev/null
+++ b/packages/create-gatsby/src/__tests__/utils/parse-args.ts
@@ -0,0 +1,52 @@
+import { parseArgs } from "../../utils/parse-args"
+import { reporter } from "../../utils/reporter"
+
+const dirNameArg = `hello-world`
+
+jest.mock(`../../utils/reporter`)
+
+describe(`parseArgs`, () => {
+ it(`should parse without flags and dir name`, () => {
+ const { flags, dirName } = parseArgs([])
+ expect(flags.yes).toBeFalsy()
+ expect(flags.ts).toBeFalsy()
+ expect(dirName).toEqual(``)
+ })
+ it(`should parse with dir name without flags`, () => {
+ const { flags, dirName } = parseArgs([dirNameArg])
+ expect(flags.yes).toBeFalsy()
+ expect(flags.ts).toBeFalsy()
+ expect(dirName).toEqual(dirNameArg)
+ })
+ it(`should parse with flags before dir name`, () => {
+ const { flags, dirName } = parseArgs([`-y`, `-ts`, dirNameArg])
+ expect(flags.yes).toBeTruthy()
+ expect(flags.ts).toBeTruthy()
+ expect(dirName).toEqual(dirNameArg)
+ })
+ it(`should parse with flags after dir name`, () => {
+ const { flags, dirName } = parseArgs([dirNameArg, `-y`, `-ts`])
+ expect(flags.yes).toBeTruthy()
+ expect(flags.ts).toBeTruthy()
+ expect(dirName).toEqual(dirNameArg)
+ })
+ it(`should parse with flags before and after dir name`, () => {
+ const { flags, dirName } = parseArgs([`-y`, dirNameArg, `-ts`])
+ expect(flags.yes).toBeTruthy()
+ expect(flags.ts).toBeTruthy()
+ expect(dirName).toEqual(dirNameArg)
+ })
+ it(`should warn if unknown flags are used`, () => {
+ const unknownFlag = `-unknown`
+ const { flags, dirName } = parseArgs([dirNameArg, unknownFlag])
+ expect(reporter.warn).toBeCalledTimes(1)
+ expect(reporter.warn).toBeCalledWith(
+ expect.stringContaining(
+ `Found unknown argument "${unknownFlag}", ignoring. Known arguments are: -y, -ts`
+ )
+ )
+ expect(flags.yes).toBeFalsy()
+ expect(flags.ts).toBeFalsy()
+ expect(dirName).toEqual(dirNameArg)
+ })
+})
diff --git a/packages/create-gatsby/src/__tests__/utils/question-helpers.ts b/packages/create-gatsby/src/__tests__/utils/question-helpers.ts
new file mode 100644
index 0000000000000..95911844b07c6
--- /dev/null
+++ b/packages/create-gatsby/src/__tests__/utils/question-helpers.ts
@@ -0,0 +1,143 @@
+import fs from "fs"
+import { reporter } from "../../utils/reporter"
+import {
+ makeChoices,
+ validateProjectName,
+ generateQuestions,
+} from "../../utils/question-helpers"
+
+jest.mock(`fs`)
+jest.mock(`../../utils/reporter`)
+
+describe(`question-helpers`, () => {
+ describe(`makeChoices`, () => {
+ it(`should return a select none option by default`, () => {
+ const options = {
+ init: {
+ message: `hello world`,
+ },
+ }
+ const choices = makeChoices(options)
+ const [none] = choices
+ expect(none).toMatchObject({
+ message: `No (or I'll add it later)`,
+ })
+ })
+
+ it(`should return no select none option if must select indicated`, () => {
+ const name = `init`
+ const message = `hello world`
+ const options = {
+ [name]: {
+ message,
+ },
+ }
+ const choices = makeChoices(options, true)
+ const [option] = choices
+ expect(option).toMatchObject({
+ message,
+ name,
+ })
+ })
+ })
+
+ describe(`validateProjectName`, () => {
+ it(`should warn if no dir name`, () => {
+ const valid = validateProjectName(``)
+ expect(valid).toBeFalsy()
+ expect(reporter.warn).toBeCalledWith(
+ expect.stringContaining(
+ `You have not provided a directory name for your site. Please do so when running with the 'y' flag.`
+ )
+ )
+ })
+
+ it(`should warn if dir name has special character`, () => {
+ const name = ` {
+ jest.spyOn(fs, `existsSync`).mockReturnValueOnce(true)
+ const name = `hello-world`
+ const valid = validateProjectName(name)
+ expect(valid).toBeFalsy()
+ expect(reporter.warn).toBeCalledWith(
+ expect.stringContaining(
+ `The destination "${name}" already exists. Please choose a different name`
+ )
+ )
+ })
+
+ it(`should return true if the dir name meets all conditions`, () => {
+ const valid = validateProjectName(`hello-world`)
+ expect(valid).toBeTruthy()
+ })
+
+ describe(`windows`, () => {
+ const originalPlatform = process.platform
+
+ beforeEach(() => {
+ Object.defineProperty(process, `platform`, { value: `win32` })
+ })
+
+ afterEach(() => {
+ Object.defineProperty(process, `platform`, { value: originalPlatform })
+ })
+
+ it(`should warn if dir name has invalid patterns`, () => {
+ const name = `aux`
+ const valid = validateProjectName(name)
+ expect(valid).toBeFalsy()
+ expect(reporter.warn).toBeCalledWith(
+ expect.stringContaining(
+ `The destination "${name}" is not a valid Windows filename. Please try another name`
+ )
+ )
+ })
+ })
+ })
+
+ describe(`generateQuestions`, () => {
+ it(`should return one question if the skip flag is passed`, () => {
+ const question = generateQuestions(`hello-world`, {
+ yes: true,
+ ts: false,
+ })
+ expect(question.name).toEqual(`project`)
+ })
+
+ it(`should return all questions if no skip flag is passed`, () => {
+ const questions = generateQuestions(`hello-world`, {
+ yes: false,
+ ts: false,
+ })
+ const [first, second, third, fourth, fifth] = questions
+ expect(questions).toHaveLength(5)
+ expect(first.name).toEqual(`project`)
+ expect(second.name).toEqual(`language`)
+ expect(third.name).toEqual(`cms`)
+ expect(fourth.name).toEqual(`styling`)
+ expect(fifth.name).toEqual(`features`)
+ })
+
+ it(`should return all questions except for language if ts flag is passed`, () => {
+ const questions = generateQuestions(`hello-world`, {
+ yes: false,
+ ts: true,
+ })
+ const [first, second, third, fourth] = questions
+ expect(questions).toHaveLength(4)
+ expect(first.name).toEqual(`project`)
+ expect(second.name).toEqual(`cms`)
+ expect(third.name).toEqual(`styling`)
+ expect(fourth.name).toEqual(`features`)
+ })
+ })
+})
diff --git a/packages/create-gatsby/src/cmses.json.d.ts b/packages/create-gatsby/src/cmses.json.d.ts
deleted file mode 100644
index be0d2fb824369..0000000000000
--- a/packages/create-gatsby/src/cmses.json.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import {PluginMap} from "."
-
-declare const cmses: PluginMap
-
-export default cmses
\ No newline at end of file
diff --git a/packages/create-gatsby/src/features.json.d.ts b/packages/create-gatsby/src/features.json.d.ts
deleted file mode 100644
index 97521924b34ad..0000000000000
--- a/packages/create-gatsby/src/features.json.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import {PluginMap} from "."
-
-declare const features: PluginMap
-
-export default features
\ No newline at end of file
diff --git a/packages/create-gatsby/src/index.ts b/packages/create-gatsby/src/index.ts
index bf487285a215f..be25487a8bba5 100644
--- a/packages/create-gatsby/src/index.ts
+++ b/packages/create-gatsby/src/index.ts
@@ -1,114 +1,37 @@
import Enquirer from "enquirer"
-import cmses from "./cmses.json"
-import styles from "./styles.json"
-import features from "./features.json"
+import cmses from "./questions/cmses.json"
+import styles from "./questions/styles.json"
+import features from "./questions/features.json"
+import languages from "./questions/languages.json"
import { initStarter, getPackageManager, gitSetup } from "./init-starter"
import { installPlugins } from "./install-plugins"
-import c from "ansi-colors"
+import colors from "ansi-colors"
import path from "path"
-import fs from "fs"
import { plugin } from "./components/plugin"
import { makePluginConfigQuestions } from "./plugin-options-form"
import { center, wrap } from "./components/utils"
import { stripIndent } from "common-tags"
import { trackCli } from "./tracking"
-import crypto from "crypto"
-import { reporter } from "./reporter"
-import { setSiteMetadata } from "./site-metadata"
-import { makeNpmSafe } from "./utils"
-
-const sha256 = (str: string): string =>
- crypto.createHash(`sha256`).update(str).digest(`hex`)
-
-const md5 = (str: string): string =>
- crypto.createHash(`md5`).update(str).digest(`hex`)
-
-/**
- * Hide string on windows (for emojis)
- */
-const w = (input: string): string => (process.platform === `win32` ? `` : input)
-
-// eslint-disable-next-line no-control-regex
-const INVALID_FILENAMES = /[<>:"/\\|?*\u0000-\u001F]/g
-const INVALID_WINDOWS = /^(con|prn|aux|nul|com\d|lpt\d)$/i
-
-const DEFAULT_STARTER = `https://github.com/gatsbyjs/gatsby-starter-minimal.git`
-
-const makeChoices = (
- options: Record }>,
- multi = false
-): Array<{ message: string; name: string; disabled?: boolean }> => {
- const entries = Object.entries(options).map(([name, message]) => {
- return { name, message: message.message }
- })
-
- if (multi) {
- return entries
- }
- const none = { name: `none`, message: `No (or I'll add it later)` }
- const divider = { name: `–`, role: `separator`, message: `–` }
-
- return [none, divider, ...entries]
-}
-
-export const validateProjectName = async (
- value: string
-): Promise => {
- if (!value) {
- return `You have not provided a directory name for your site. Please do so when running with the 'y' flag.`
- }
- value = value.trim()
- if (INVALID_FILENAMES.test(value)) {
- return `The destination "${value}" is not a valid filename. Please try again, avoiding special characters.`
- }
- if (process.platform === `win32` && INVALID_WINDOWS.test(value)) {
- return `The destination "${value}" is not a valid Windows filename. Please try another name`
- }
- if (fs.existsSync(path.resolve(value))) {
- return `The destination "${value}" already exists. Please choose a different name`
- }
- return true
+import { reporter } from "./utils/reporter"
+import { setSiteMetadata } from "./utils/site-metadata"
+import { makeNpmSafe } from "./utils/make-npm-safe"
+import {
+ generateQuestions,
+ validateProjectName,
+} from "./utils/question-helpers"
+import { sha256, md5 } from "./utils/hash"
+import { maybeUseEmoji } from "./utils/emoji"
+import { parseArgs } from "./utils/parse-args"
+
+export const DEFAULT_STARTERS: Record = {
+ js: `https://github.com/gatsbyjs/gatsby-starter-minimal.git`,
+ ts: `https://github.com/gatsbyjs/gatsby-starter-minimal-ts.git`,
}
-// The enquirer types are not accurate
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const questions = (initialFolderName: string, skip: boolean): any => [
- {
- type: `textinput`,
- name: `project`,
- message: `What would you like to name the folder where your site will be created?`,
- hint: path.basename(process.cwd()),
- separator: `/`,
- initial: initialFolderName,
- format: (value: string): string => c.cyan(value),
- validate: validateProjectName,
- skip,
- },
- {
- type: `selectinput`,
- name: `cms`,
- message: `Will you be using a CMS?`,
- hint: `(Single choice) Arrow keys to move, enter to confirm`,
- choices: makeChoices(cmses),
- },
- {
- type: `selectinput`,
- name: `styling`,
- message: `Would you like to install a styling system?`,
- hint: `(Single choice) Arrow keys to move, enter to confirm`,
- choices: makeChoices(styles),
- },
- {
- type: `multiselectinput`,
- name: `features`,
- message: `Would you like to install additional features with other plugins?`,
- hint: `(Multiple choice) Use arrow keys to move, spacebar to select, and confirm with an enter on "Done"`,
- choices: makeChoices(features, true),
- },
-]
interface IAnswers {
name: string
project: string
+ language: keyof typeof languages
styling?: keyof typeof styles
cms?: keyof typeof cmses
features?: Array
@@ -141,38 +64,33 @@ export type PluginMap = Record
export type PluginConfigMap = Record>
-const removeKey = (plugin: string): string => plugin.split(`:`)[0]
-
export async function run(): Promise {
- const [flag, siteDirectory] = process.argv.slice(2)
-
- let yesFlag = false
- if (flag === `-y`) {
- yesFlag = true
- }
+ const { flags, dirName } = parseArgs(process.argv.slice(2))
trackCli(`CREATE_GATSBY_START`)
const { version } = require(`../package.json`)
- reporter.info(c.grey(`create-gatsby version ${version}`))
+ reporter.info(colors.grey(`create-gatsby version ${version}`))
+ // Wecome message
reporter.info(
`
-${center(c.blueBright.bold.underline(`Welcome to Gatsby!`))}
+${center(colors.blueBright.bold.underline(`Welcome to Gatsby!`))}
`
)
- if (!yesFlag) {
+ // If we aren't skipping prompts, communicate we'll ask setup questions
+ if (!flags.yes) {
reporter.info(
wrap(
- `This command will generate a new Gatsby site for you in ${c.bold(
+ `This command will generate a new Gatsby site for you in ${colors.bold(
process.cwd()
- )} with the setup you select. ${c.white.bold(
+ )} with the setup you select. ${colors.white.bold(
`Let's answer some questions:\n\n`
)}`,
process.stdout.columns
@@ -181,55 +99,73 @@ ${center(c.blueBright.bold.underline(`Welcome to Gatsby!`))}
}
const enquirer = new Enquirer()
-
enquirer.use(plugin)
- let data
- let siteName
- if (!yesFlag) {
- ;({ name: siteName } = await enquirer.prompt({
+ // If we aren't skipping prompts, get a site name first to use as a default folder name
+ let npmSafeSiteName
+
+ if (!flags.yes) {
+ const { name } = await enquirer.prompt({
type: `textinput`,
name: `name`,
message: `What would you like to call your site?`,
initial: `My Gatsby Site`,
- format: (value: string): string => c.cyan(value),
- } as any))
+ format: (value: string): string => colors.cyan(value),
+ } as any)
- data = await enquirer.prompt(questions(makeNpmSafe(siteName), yesFlag))
+ npmSafeSiteName = makeNpmSafe(name)
} else {
- const warn = await validateProjectName(siteDirectory)
- if (typeof warn === `string`) {
- reporter.warn(warn)
+ const valid = validateProjectName(dirName)
+
+ if (!valid) {
return
}
- siteName = siteDirectory
- data = await enquirer.prompt(
- questions(makeNpmSafe(siteDirectory), yesFlag)[0]
- )
+
+ npmSafeSiteName = makeNpmSafe(dirName)
}
- data.project = data.project.trim()
+ // Prompt user with questions and gather answers
+ const questions = generateQuestions(npmSafeSiteName, flags)
+ const answers = await enquirer.prompt(questions)
+
+ answers.project = answers.project.trim()
+ // Language selection
+ if (flags.yes) {
+ answers.language = `js`
+ }
+ if (flags.ts) {
+ answers.language = `ts`
+ }
+
+ // Telemetry
trackCli(`CREATE_GATSBY_SELECT_OPTION`, {
name: `project_name`,
- valueString: sha256(data.project),
+ valueString: sha256(answers.project),
+ })
+ trackCli(`CREATE_GATSBY_SELECT_OPTION`, {
+ name: `LANGUAGE`,
+ valueString: answers.language,
})
trackCli(`CREATE_GATSBY_SELECT_OPTION`, {
name: `CMS`,
- valueString: data.cms || `none`,
+ valueString: answers.cms || `none`,
})
trackCli(`CREATE_GATSBY_SELECT_OPTION`, {
name: `CSS_TOOLS`,
- valueString: data.styling || `none`,
+ valueString: answers.styling || `none`,
})
trackCli(`CREATE_GATSBY_SELECT_OPTION`, {
name: `PLUGIN`,
- valueStringArray: data.features || [],
+ valueStringArray: answers.features || [],
})
+ // Collect a report of things we will do to present to the user once the questions are complete
const messages: Array = [
- `${w(`🛠 `)}Create a new Gatsby site in the folder ${c.magenta(
- data.project
+ `${maybeUseEmoji(
+ `🛠 `
+ )}Create a new Gatsby site in the folder ${colors.magenta(
+ answers.project
)}`,
]
@@ -237,47 +173,52 @@ ${center(c.blueBright.bold.underline(`Welcome to Gatsby!`))}
const packages: Array = []
let pluginConfig: PluginConfigMap = {}
- if (data.cms && data.cms !== `none`) {
+ // If a CMS is selected, ask CMS config questions after the main question set is complete
+ if (answers.cms && answers.cms !== `none`) {
messages.push(
- `${w(`📚 `)}Install and configure the plugin for ${c.magenta(
- cmses[data.cms].message
+ `${maybeUseEmoji(
+ `📚 `
+ )}Install and configure the plugin for ${colors.magenta(
+ cmses[answers.cms].message
)}`
)
- const extraPlugins = cmses[data.cms].plugins || []
- plugins.push(data.cms, ...extraPlugins)
+ const extraPlugins = cmses[answers.cms].plugins || []
+ plugins.push(answers.cms, ...extraPlugins)
packages.push(
- data.cms,
- ...(cmses[data.cms].dependencies || []),
+ answers.cms,
+ ...(cmses[answers.cms].dependencies || []),
...extraPlugins
)
- pluginConfig = { ...pluginConfig, ...cmses[data.cms].options }
+ pluginConfig = { ...pluginConfig, ...cmses[answers.cms].options }
}
- if (data.styling && data.styling !== `none`) {
+ // If a styling system is selected, ask styling config questions after the main question set is complete
+ if (answers.styling && answers.styling !== `none`) {
messages.push(
- `${w(`🎨 `)}Get you set up to use ${c.magenta(
- styles[data.styling].message
+ `${maybeUseEmoji(`🎨 `)}Get you set up to use ${colors.magenta(
+ styles[answers.styling].message
)} for styling your site`
)
- const extraPlugins = styles[data.styling].plugins || []
+ const extraPlugins = styles[answers.styling].plugins || []
- plugins.push(data.styling, ...extraPlugins)
+ plugins.push(answers.styling, ...extraPlugins)
packages.push(
- data.styling,
- ...(styles[data.styling].dependencies || []),
+ answers.styling,
+ ...(styles[answers.styling].dependencies || []),
...extraPlugins
)
- pluginConfig = { ...pluginConfig, ...styles[data.styling].options }
+ pluginConfig = { ...pluginConfig, ...styles[answers.styling].options }
}
- if (data.features?.length) {
+ // If additional features are selected, install required dependencies in install step
+ if (answers.features?.length) {
messages.push(
- `${w(`🔌 `)}Install ${data.features
- ?.map((feat: string) => c.magenta(feat))
+ `${maybeUseEmoji(`🔌 `)}Install ${answers.features
+ ?.map((feat: string) => colors.magenta(feat))
.join(`, `)}`
)
- plugins.push(...data.features)
- const featureDependencies = data.features?.map(featureKey => {
+ plugins.push(...answers.features)
+ const featureDependencies = answers.features?.map(featureKey => {
const extraPlugins = features[featureKey].plugins || []
plugins.push(...extraPlugins)
return [
@@ -292,14 +233,16 @@ ${center(c.blueBright.bold.underline(`Welcome to Gatsby!`))}
featureDependencies
) // here until we upgrade to node 11 and can use flatMap
- packages.push(...data.features, ...flattenedDependencies)
+ packages.push(...answers.features, ...flattenedDependencies)
// Merge plugin options
- pluginConfig = data.features.reduce((prev, key) => {
+ pluginConfig = answers.features.reduce((prev, key) => {
return { ...prev, ...features[key].options }
}, pluginConfig)
}
+ // Ask additional config questions if any
const config = makePluginConfigQuestions(plugins)
+
if (config.length) {
reporter.info(
`\nGreat! A few of the selections you made need to be configured. Please fill in the options for each plugin now:\n`
@@ -314,10 +257,12 @@ ${center(c.blueBright.bold.underline(`Welcome to Gatsby!`))}
trackCli(`CREATE_GATSBY_SET_PLUGINS_STOP`)
}
- if (!yesFlag) {
+
+ // If we're not skipping prompts, give the user a report of what we're about to do
+ if (!flags.yes) {
reporter.info(`
-${c.bold(`Thanks! Here's what we'll now do:`)}
+${colors.bold(`Thanks! Here's what we'll now do:`)}
${messages.join(`\n `)}
`)
@@ -327,7 +272,7 @@ ${c.bold(`Thanks! Here's what we'll now do:`)}
name: `confirm`,
initial: `Yes`,
message: `Shall we do this?`,
- format: value => (value ? c.greenBright(`Yes`) : c.red(`No`)),
+ format: value => (value ? colors.greenBright(`Yes`) : colors.red(`No`)),
})
if (!confirm) {
@@ -338,46 +283,50 @@ ${c.bold(`Thanks! Here's what we'll now do:`)}
}
}
+ // Decide starter
+ const starter = DEFAULT_STARTERS[answers.language || `js`]
+
+ // Do all the things
await initStarter(
- DEFAULT_STARTER,
- data.project,
- packages.map(removeKey),
- siteName
+ starter,
+ answers.project,
+ packages.map((plugin: string) => plugin.split(`:`)[0]),
+ npmSafeSiteName
)
- reporter.success(`Created site in ${c.green(data.project)}`)
+ reporter.success(`Created site in ${colors.green(answers.project)}`)
- const fullPath = path.resolve(data.project)
+ const fullPath = path.resolve(answers.project)
if (plugins.length) {
- reporter.info(`${w(`🔌 `)}Setting-up plugins...`)
+ reporter.info(`${maybeUseEmoji(`🔌 `)}Setting-up plugins...`)
await installPlugins(plugins, pluginConfig, fullPath, [])
}
- await setSiteMetadata(fullPath, `title`, siteName)
+ await setSiteMetadata(fullPath, `title`, dirName)
- await gitSetup(data.project)
+ await gitSetup(answers.project)
const pm = await getPackageManager()
const runCommand = pm === `npm` ? `npm run` : `yarn`
reporter.info(
stripIndent`
- ${w(`🎉 `)}Your new Gatsby site ${c.bold(
- siteName
+ ${maybeUseEmoji(`🎉 `)}Your new Gatsby site ${colors.bold(
+ dirName
)} has been successfully created
- at ${c.bold(fullPath)}.
+ at ${colors.bold(fullPath)}.
`
)
reporter.info(`Start by going to the directory with\n
- ${c.magenta(`cd ${data.project}`)}
+ ${colors.magenta(`cd ${answers.project}`)}
`)
reporter.info(`Start the local development server with\n
- ${c.magenta(`${runCommand} develop`)}
+ ${colors.magenta(`${runCommand} develop`)}
`)
reporter.info(`See all commands at\n
- ${c.blueBright(`https://www.gatsbyjs.com/docs/gatsby-cli/`)}
+ ${colors.blueBright(`https://www.gatsbyjs.com/docs/gatsby-cli/`)}
`)
const siteHash = md5(fullPath)
diff --git a/packages/create-gatsby/src/init-starter.ts b/packages/create-gatsby/src/init-starter.ts
index b74c6470db5fc..7b85684f0fe54 100644
--- a/packages/create-gatsby/src/init-starter.ts
+++ b/packages/create-gatsby/src/init-starter.ts
@@ -2,12 +2,12 @@ import { execSync } from "child_process"
import execa, { Options } from "execa"
import fs from "fs-extra"
import path from "path"
-import { reporter } from "./reporter"
+import { reporter } from "./utils/reporter"
import { spin } from "tiny-spin"
-import { getConfigStore } from "./get-config-store"
+import { getConfigStore } from "./utils/get-config-store"
type PackageManager = "yarn" | "npm"
-import c from "ansi-colors"
-import { clearLine, makeNpmSafe } from "./utils"
+import colors from "ansi-colors"
+import { clearLine } from "./utils/clear-line"
const packageManagerConfigKey = `cli.packageManager`
@@ -81,12 +81,12 @@ const createInitialGitCommit = async (rootPath: string): Promise => {
const setNameInPackage = async (
sitePath: string,
- name: string
+ npmSafeSiteName: string
): Promise => {
const packageJsonPath = path.join(sitePath, `package.json`)
const packageJson = await fs.readJSON(packageJsonPath)
- packageJson.name = makeNpmSafe(name)
- packageJson.description = name
+ packageJson.name = npmSafeSiteName
+ packageJson.description = npmSafeSiteName
delete packageJson.license
try {
const result = await execa(`git`, [`config`, `user.name`])
@@ -109,7 +109,9 @@ const install = async (
): Promise => {
const prevDir = process.cwd()
- reporter.info(`${c.blueBright(c.symbols.pointer)} Installing Gatsby...`)
+ reporter.info(
+ `${colors.blueBright(colors.symbols.pointer)} Installing Gatsby...`
+ )
process.chdir(rootPath)
@@ -137,7 +139,9 @@ const install = async (
await clearLine()
reporter.success(`Installed Gatsby`)
- reporter.info(`${c.blueBright(c.symbols.pointer)} Installing plugins...`)
+ reporter.info(
+ `${colors.blueBright(colors.symbols.pointer)} Installing plugins...`
+ )
await execa(
`npm`,
@@ -200,13 +204,13 @@ export async function initStarter(
starter: string,
rootPath: string,
packages: Array,
- siteName: string
+ npmSafeSiteName: string
): Promise {
const sitePath = path.resolve(rootPath)
await clone(starter, sitePath)
- await setNameInPackage(sitePath, siteName)
+ await setNameInPackage(sitePath, npmSafeSiteName)
await install(rootPath, packages)
diff --git a/packages/create-gatsby/src/install-plugins.ts b/packages/create-gatsby/src/install-plugins.ts
index 2f0b8838131e3..646d73e65ee53 100644
--- a/packages/create-gatsby/src/install-plugins.ts
+++ b/packages/create-gatsby/src/install-plugins.ts
@@ -1,7 +1,7 @@
-import { reporter } from "./reporter"
+import { reporter } from "./utils/reporter"
import path from "path"
import { PluginConfigMap } from "."
-import { requireResolve } from "./require-utils"
+import { requireResolve } from "./utils/require-utils"
const resolveGatsbyPath = (rootPath: string): string | never => {
try {
diff --git a/packages/create-gatsby/src/plugin-options-form.ts b/packages/create-gatsby/src/plugin-options-form.ts
index 9c63a215b8bfc..1cccfafa9b08b 100644
--- a/packages/create-gatsby/src/plugin-options-form.ts
+++ b/packages/create-gatsby/src/plugin-options-form.ts
@@ -2,9 +2,9 @@ import { stripIndent } from "common-tags"
import terminalLink from "terminal-link"
import Joi from "joi"
import pluginSchemas from "./plugin-schemas.json"
-import cmses from "./cmses.json"
-import styles from "./styles.json"
-import c from "ansi-colors"
+import cmses from "./questions/cmses.json"
+import styles from "./questions/styles.json"
+import colors from "ansi-colors"
const supportedOptionTypes = [`string`, `boolean`, `number`]
@@ -45,7 +45,7 @@ function getName(key: string): string | undefined {
}
function docsLink(pluginName: string): string {
- return c.blueBright(
+ return colors.blueBright(
terminalLink(
`the plugin docs`,
`https://www.gatsbyjs.com/plugins/${pluginName}/`,
@@ -101,7 +101,7 @@ export const makePluginConfigQuestions = (
See ${docsLink(pluginName)} for help.
${
choices.length > 1
- ? c.green(
+ ? colors.green(
`Use arrow keys to move between fields, and enter to finish`
)
: ``
diff --git a/packages/create-gatsby/src/cmses.json b/packages/create-gatsby/src/questions/cmses.json
similarity index 100%
rename from packages/create-gatsby/src/cmses.json
rename to packages/create-gatsby/src/questions/cmses.json
diff --git a/packages/create-gatsby/src/questions/cmses.json.d.ts b/packages/create-gatsby/src/questions/cmses.json.d.ts
new file mode 100644
index 0000000000000..42bc0c62001de
--- /dev/null
+++ b/packages/create-gatsby/src/questions/cmses.json.d.ts
@@ -0,0 +1,5 @@
+import { PluginMap } from "../index"
+
+declare const cmses: PluginMap
+
+export default cmses
diff --git a/packages/create-gatsby/src/features.json b/packages/create-gatsby/src/questions/features.json
similarity index 95%
rename from packages/create-gatsby/src/features.json
rename to packages/create-gatsby/src/questions/features.json
index 450291d76d74d..4ed7eb46b8440 100644
--- a/packages/create-gatsby/src/features.json
+++ b/packages/create-gatsby/src/questions/features.json
@@ -30,7 +30,7 @@
"gatsby-plugin-mdx": {
"message": "Add Markdown and MDX support",
"plugins": ["gatsby-source-filesystem:pages"],
- "dependencies": ["@mdx-js/react", "@mdx-js/mdx"],
+ "dependencies": ["@mdx-js/react@v1", "@mdx-js/mdx@v1"],
"options": {
"gatsby-source-filesystem:pages": {
"name": "pages",
diff --git a/packages/create-gatsby/src/questions/features.json.d.ts b/packages/create-gatsby/src/questions/features.json.d.ts
new file mode 100644
index 0000000000000..03a45605c42f3
--- /dev/null
+++ b/packages/create-gatsby/src/questions/features.json.d.ts
@@ -0,0 +1,5 @@
+import { PluginMap } from "../index"
+
+declare const features: PluginMap
+
+export default features
diff --git a/packages/create-gatsby/src/questions/languages.json b/packages/create-gatsby/src/questions/languages.json
new file mode 100644
index 0000000000000..6d3734ae9e296
--- /dev/null
+++ b/packages/create-gatsby/src/questions/languages.json
@@ -0,0 +1,8 @@
+{
+ "js": {
+ "message": "JavaScript"
+ },
+ "ts": {
+ "message": "TypeScript"
+ }
+}
\ No newline at end of file
diff --git a/packages/create-gatsby/src/questions/languages.json.d.ts b/packages/create-gatsby/src/questions/languages.json.d.ts
new file mode 100644
index 0000000000000..92a15fe41b9e0
--- /dev/null
+++ b/packages/create-gatsby/src/questions/languages.json.d.ts
@@ -0,0 +1,5 @@
+import { PluginMap } from "../index"
+
+declare const language: PluginMap
+
+export default language
diff --git a/packages/create-gatsby/src/styles.json b/packages/create-gatsby/src/questions/styles.json
similarity index 71%
rename from packages/create-gatsby/src/styles.json
rename to packages/create-gatsby/src/questions/styles.json
index 16b01a0dc22ec..aeeb4fb4d0f80 100644
--- a/packages/create-gatsby/src/styles.json
+++ b/packages/create-gatsby/src/questions/styles.json
@@ -15,5 +15,13 @@
"gatsby-plugin-theme-ui": {
"message": "Theme UI",
"dependencies": ["theme-ui"]
+ },
+ "gatsby-plugin-vanilla-extract": {
+ "message": "vanilla-extract",
+ "dependencies": [
+ "@vanilla-extract/webpack-plugin",
+ "@vanilla-extract/css",
+ "@vanilla-extract/babel-plugin"
+ ]
}
}
diff --git a/packages/create-gatsby/src/questions/styles.json.d.ts b/packages/create-gatsby/src/questions/styles.json.d.ts
new file mode 100644
index 0000000000000..3cad7e19734bc
--- /dev/null
+++ b/packages/create-gatsby/src/questions/styles.json.d.ts
@@ -0,0 +1,5 @@
+import { PluginMap } from "../index"
+
+declare const styles: PluginMap
+
+export default styles
diff --git a/packages/create-gatsby/src/styles.json.d.ts b/packages/create-gatsby/src/styles.json.d.ts
deleted file mode 100644
index d723f32125043..0000000000000
--- a/packages/create-gatsby/src/styles.json.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import {PluginMap} from "."
-
-declare const styles: PluginMap
-
-export default styles
\ No newline at end of file
diff --git a/packages/create-gatsby/src/tracking.ts b/packages/create-gatsby/src/tracking.ts
index d0c2d60801895..e277af15e3167 100644
--- a/packages/create-gatsby/src/tracking.ts
+++ b/packages/create-gatsby/src/tracking.ts
@@ -1,11 +1,16 @@
import fetch from "node-fetch"
import { v4 as uuidv4 } from "@lukeed/uuid"
-import { getConfigStore } from "./get-config-store"
+import { getConfigStore } from "./utils/get-config-store"
+import { isTruthy } from "./utils/is-truthy"
const store = getConfigStore()
const gatsbyCliVersion = require(`../package.json`).version
const analyticsApi =
process.env.GATSBY_TELEMETRY_API || `https://analytics.gatsbyjs.com/events`
+let trackingEnabled: boolean | undefined
+const trackingDisabledFromEnvVar: boolean | undefined = isTruthy(
+ process.env.GATSBY_TELEMETRY_DISABLED
+)
const getMachineId = (): string => {
let machineId = store.get(`telemetry.machineId`)
@@ -28,7 +33,34 @@ export interface ITrackCliArgs {
const sessionId = uuidv4()
+// Adapted from gatsby-telemetry
+export function isTrackingEnabled(): boolean {
+ // Cache the result
+ if (trackingEnabled !== undefined) {
+ return trackingEnabled
+ }
+
+ let enabled = store.get(`telemetry.enabled`) as boolean | null
+
+ if (enabled === undefined || enabled === null) {
+ enabled = true
+ store.set(`telemetry.enabled`, enabled)
+ }
+
+ if (trackingDisabledFromEnvVar) {
+ enabled = false
+ }
+
+ trackingEnabled = enabled
+
+ return enabled
+}
+
export const trackCli = (eventType: string, args?: ITrackCliArgs): void => {
+ if (!isTrackingEnabled()) {
+ return
+ }
+
fetch(analyticsApi, {
method: `POST`,
headers: {
diff --git a/packages/create-gatsby/src/utils.ts b/packages/create-gatsby/src/utils/clear-line.ts
similarity index 51%
rename from packages/create-gatsby/src/utils.ts
rename to packages/create-gatsby/src/utils/clear-line.ts
index 26165037004b0..e013c089e7915 100644
--- a/packages/create-gatsby/src/utils.ts
+++ b/packages/create-gatsby/src/utils/clear-line.ts
@@ -10,14 +10,3 @@ export const clearLine = (count = 1): Promise =>
resolve()
})
})
-
-// Makes a string safe for using as a folder or npm package name
-export const makeNpmSafe = (str: string): string =>
- str
- // Replace camelcase with kebab
- .replace(/([a-z])([A-Z])/g, `$1-$2`)
- .toLowerCase()
- // Replace any number of consecutive illegal characters with a single dash
- .replace(/[^a-z0-9_.]+/g, `-`)
- // Remove trailing dots and dashes
- .replace(/^[_\-.]+|[_\-.]+$/g, ``)
diff --git a/packages/create-gatsby/src/utils/emoji.ts b/packages/create-gatsby/src/utils/emoji.ts
new file mode 100644
index 0000000000000..a0d7df4ccceb0
--- /dev/null
+++ b/packages/create-gatsby/src/utils/emoji.ts
@@ -0,0 +1,5 @@
+/**
+ * Hide string on windows (for emojis)
+ */
+export const maybeUseEmoji = (input: string): string =>
+ process.platform === `win32` ? `` : input
diff --git a/packages/create-gatsby/src/get-config-store.ts b/packages/create-gatsby/src/utils/get-config-store.ts
similarity index 100%
rename from packages/create-gatsby/src/get-config-store.ts
rename to packages/create-gatsby/src/utils/get-config-store.ts
diff --git a/packages/create-gatsby/src/utils/hash.ts b/packages/create-gatsby/src/utils/hash.ts
new file mode 100644
index 0000000000000..ced08a278b176
--- /dev/null
+++ b/packages/create-gatsby/src/utils/hash.ts
@@ -0,0 +1,6 @@
+import crypto from "crypto"
+
+export const sha256 = (str: string): string =>
+ crypto.createHash(`sha256`).update(str).digest(`hex`)
+export const md5 = (str: string): string =>
+ crypto.createHash(`md5`).update(str).digest(`hex`)
diff --git a/packages/create-gatsby/src/utils/is-truthy.ts b/packages/create-gatsby/src/utils/is-truthy.ts
new file mode 100644
index 0000000000000..25b0c67e77c18
--- /dev/null
+++ b/packages/create-gatsby/src/utils/is-truthy.ts
@@ -0,0 +1,23 @@
+// Copied from gatsby-core-utils to avoid depending on it, similar to get-config-store
+//
+// Returns true for `true`, true, positive numbers
+// Returns false for `false`, false, 0, negative integers and anything else
+export function isTruthy(value: any): boolean {
+ // Return if Boolean
+ if (typeof value === `boolean`) return value
+
+ // Return false if null or undefined
+ if (value === undefined || value === null) return false
+
+ // If the String is true or false
+ if (value.toLowerCase() === `true`) return true
+ if (value.toLowerCase() === `false`) return false
+
+ // Now check if it's a number
+ const number = parseInt(value, 10)
+ if (isNaN(number)) return false
+ if (number > 0) return true
+
+ // Default to false
+ return false
+}
diff --git a/packages/create-gatsby/src/utils/make-npm-safe.ts b/packages/create-gatsby/src/utils/make-npm-safe.ts
new file mode 100644
index 0000000000000..10a89fbc05468
--- /dev/null
+++ b/packages/create-gatsby/src/utils/make-npm-safe.ts
@@ -0,0 +1,10 @@
+// Makes a string safe for using as a folder or npm package name
+export const makeNpmSafe = (str: string): string =>
+ str
+ // Replace camelcase with kebab
+ .replace(/([a-z])([A-Z])/g, `$1-$2`)
+ .toLowerCase()
+ // Replace any number of consecutive illegal characters with a single dash
+ .replace(/[^a-z0-9_.]+/g, `-`)
+ // Remove trailing dots and dashes
+ .replace(/^[_\-.]+|[_\-.]+$/g, ``)
diff --git a/packages/create-gatsby/src/utils/parse-args.ts b/packages/create-gatsby/src/utils/parse-args.ts
new file mode 100644
index 0000000000000..8297c0ff1614a
--- /dev/null
+++ b/packages/create-gatsby/src/utils/parse-args.ts
@@ -0,0 +1,61 @@
+import { reporter } from "./reporter"
+
+enum Flag {
+ yes = `-y`, // Skip prompts
+ ts = `-ts`, // Use TypeScript
+}
+
+export interface IFlags {
+ yes: boolean
+ ts: boolean
+}
+
+interface IArgs {
+ flags: IFlags
+ dirName: string
+}
+
+/**
+ * Parse arguments without considering position. Both cases should work the same:
+ *
+ * - `npm init gatsby hello-world -y`
+ * - `npm init gatsby -y hello-world`
+ *
+ * We deliberately trade the edge case of a user attempting to create a directory name
+ * prepended with a dash (e.g. `-my-project`) for flags that work regardless of position.
+ */
+export function parseArgs(args: Array): IArgs {
+ const { flags, dirName } = args.reduce(
+ (sortedArgs, arg) => {
+ switch (arg) {
+ case Flag.yes:
+ sortedArgs.flags.yes = true
+ break
+ case Flag.ts:
+ sortedArgs.flags.ts = true
+ break
+ default:
+ if (arg.startsWith(`-`)) {
+ reporter.warn(
+ `Found unknown argument "${arg}", ignoring. Known arguments are: ${Flag.yes}, ${Flag.ts}`
+ )
+ break
+ }
+ sortedArgs.dirName = arg
+ }
+ return sortedArgs
+ },
+ {
+ flags: {
+ yes: false,
+ ts: false,
+ },
+ dirName: ``,
+ }
+ )
+
+ return {
+ flags,
+ dirName,
+ }
+}
diff --git a/packages/create-gatsby/src/utils/question-helpers.ts b/packages/create-gatsby/src/utils/question-helpers.ts
new file mode 100644
index 0000000000000..fde5df7cbf010
--- /dev/null
+++ b/packages/create-gatsby/src/utils/question-helpers.ts
@@ -0,0 +1,123 @@
+import fs from "fs"
+import path from "path"
+import languages from "../questions/languages.json"
+import cmses from "../questions/cmses.json"
+import styles from "../questions/styles.json"
+import features from "../questions/features.json"
+import colors from "ansi-colors"
+import { reporter } from "./reporter"
+import { IFlags } from "./parse-args"
+
+// eslint-disable-next-line no-control-regex
+const INVALID_FILENAMES = /[<>:"/\\|?*\u0000-\u001F]/g
+const INVALID_WINDOWS = /^(con|prn|aux|nul|com\d|lpt\d)$/i
+
+export const makeChoices = (
+ options: Record }>,
+ mustSelect = false
+): Array<{ message: string; name: string; disabled?: boolean }> => {
+ const entries = Object.entries(options).map(([name, message]) => {
+ return { name, message: message.message }
+ })
+
+ if (mustSelect) {
+ return entries
+ }
+
+ const none = { name: `none`, message: `No (or I'll add it later)` }
+ const divider = { name: `–`, role: `separator`, message: `–` }
+
+ return [none, divider, ...entries]
+}
+
+export function validateProjectName(value: string): boolean {
+ if (!value) {
+ reporter.warn(
+ `You have not provided a directory name for your site. Please do so when running with the 'y' flag.`
+ )
+ return false
+ }
+ value = value.trim()
+ if (INVALID_FILENAMES.test(value)) {
+ reporter.warn(
+ `The destination "${value}" is not a valid filename. Please try again, avoiding special characters.`
+ )
+ return false
+ }
+ if (process.platform === `win32` && INVALID_WINDOWS.test(value)) {
+ reporter.warn(
+ `The destination "${value}" is not a valid Windows filename. Please try another name`
+ )
+ return false
+ }
+ if (fs.existsSync(path.resolve(value))) {
+ reporter.warn(
+ `The destination "${value}" already exists. Please choose a different name`
+ )
+ return false
+ }
+ return true
+}
+
+// Enquirer types are not exported and are out of date
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const generateQuestions = (
+ initialFolderName: string,
+ flags: IFlags
+): any => {
+ const siteNameQuestion = {
+ type: `textinput`,
+ name: `project`,
+ message: `What would you like to name the folder where your site will be created?`,
+ hint: path.basename(process.cwd()),
+ separator: `/`,
+ initial: initialFolderName,
+ format: (value: string): string => colors.cyan(value),
+ validate: validateProjectName,
+ skip: flags.yes,
+ }
+
+ const languageQuestion = {
+ type: `selectinput`,
+ name: `language`,
+ message: `Will you be using JavaScript or TypeScript?`,
+ hint: `(Single choice) Arrow keys to move, enter to confirm`,
+ choices: makeChoices(languages, true),
+ }
+
+ const otherQuestions = [
+ {
+ type: `selectinput`,
+ name: `cms`,
+ message: `Will you be using a CMS?`,
+ hint: `(Single choice) Arrow keys to move, enter to confirm`,
+ choices: makeChoices(cmses),
+ },
+ {
+ type: `selectinput`,
+ name: `styling`,
+ message: `Would you like to install a styling system?`,
+ hint: `(Single choice) Arrow keys to move, enter to confirm`,
+ choices: makeChoices(styles),
+ },
+ {
+ type: `multiselectinput`,
+ name: `features`,
+ message: `Would you like to install additional features with other plugins?`,
+ hint: `(Multiple choice) Use arrow keys to move, spacebar to select, and confirm with an enter on "Done"`,
+ choices: makeChoices(features, true),
+ },
+ ]
+
+ // Skip all questions
+ if (flags.yes) {
+ return siteNameQuestion
+ }
+
+ // Skip language question
+ if (flags.ts) {
+ return [siteNameQuestion, ...otherQuestions]
+ }
+
+ return [siteNameQuestion, languageQuestion, ...otherQuestions]
+}
diff --git a/packages/create-gatsby/src/reporter.ts b/packages/create-gatsby/src/utils/reporter.ts
similarity index 64%
rename from packages/create-gatsby/src/reporter.ts
rename to packages/create-gatsby/src/utils/reporter.ts
index 001d8d8aa5497..c93855fdb5d87 100644
--- a/packages/create-gatsby/src/reporter.ts
+++ b/packages/create-gatsby/src/utils/reporter.ts
@@ -1,16 +1,17 @@
-import c from "ansi-colors"
+import colors from "ansi-colors"
+
// We don't want to depend on the whole of gatsby-cli, so we can't use reporter
export const reporter = {
info: (message: string): void => console.log(message),
verbose: (message: string): void => console.log(message),
log: (message: string): void => console.log(message),
success: (message: string): void =>
- console.log(c.green(c.symbols.check + ` `) + message),
+ console.log(colors.green(colors.symbols.check + ` `) + message),
error: (message: string): void =>
- console.error(c.red(c.symbols.cross + ` `) + message),
+ console.error(colors.red(colors.symbols.cross + ` `) + message),
panic: (message: string): never => {
console.error(message)
process.exit(1)
},
- warn: (message: string): void => console.warn(message),
+ warn: (message: string): void => console.warn(colors.yellow(message)),
}
diff --git a/packages/create-gatsby/src/require-utils.ts b/packages/create-gatsby/src/utils/require-utils.ts
similarity index 100%
rename from packages/create-gatsby/src/require-utils.ts
rename to packages/create-gatsby/src/utils/require-utils.ts
diff --git a/packages/create-gatsby/src/site-metadata.ts b/packages/create-gatsby/src/utils/site-metadata.ts
similarity index 100%
rename from packages/create-gatsby/src/site-metadata.ts
rename to packages/create-gatsby/src/utils/site-metadata.ts
diff --git a/packages/gatsby-cli/CHANGELOG.md b/packages/gatsby-cli/CHANGELOG.md
index b2488185b6e5c..df492f37b2bfe 100644
--- a/packages/gatsby-cli/CHANGELOG.md
+++ b/packages/gatsby-cli/CHANGELOG.md
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [4.7.0](https://github.com/gatsbyjs/gatsby/commits/gatsby-cli@4.7.0/packages/gatsby-cli) (2022-02-08)
+
+[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.7)
+
+#### Bug Fixes
+
+- relax error location validation and ignore extra fields [#34559](https://github.com/gatsbyjs/gatsby/issues/34559) ([0d894f5](https://github.com/gatsbyjs/gatsby/commit/0d894f59bbcc9a27eb7680969df2d4a06752b857))
+
+#### Chores
+
+- update dependency rollup to ^2.66.1 for gatsby-cli [#34659](https://github.com/gatsbyjs/gatsby/issues/34659) ([0cc56b4](https://github.com/gatsbyjs/gatsby/commit/0cc56b474e9280dd2addd1138a9eed12b9732746))
+- update dependency typescript to ^4.5.5 [#34641](https://github.com/gatsbyjs/gatsby/issues/34641) ([f7a7e1f](https://github.com/gatsbyjs/gatsby/commit/f7a7e1f642d91babb397156ab37cb28dcde19737))
+
+### [4.6.1](https://github.com/gatsbyjs/gatsby/commits/gatsby-cli@4.6.1/packages/gatsby-cli) (2022-02-04)
+
+**Note:** Version bump only for package gatsby-cli
+
+## [4.6.0](https://github.com/gatsbyjs/gatsby/commits/gatsby-cli@4.6.0/packages/gatsby-cli) (2022-01-25)
+
+[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.6)
+
+#### Bug Fixes
+
+- relax error location validation and ignore extra fields [#34559](https://github.com/gatsbyjs/gatsby/issues/34559) [#34588](https://github.com/gatsbyjs/gatsby/issues/34588) ([ed1a9b5](https://github.com/gatsbyjs/gatsby/commit/ed1a9b5a2d42c4bb87825b424a61fe973f52efa7))
+- Re-Add plugin-add functionality [#34482](https://github.com/gatsbyjs/gatsby/issues/34482) ([618b32b](https://github.com/gatsbyjs/gatsby/commit/618b32b17751c76ea1b1a6f4fbc91da928bd18c1))
+
+#### Other Changes
+
+- Upgrade to strip-ansi ^6.0.1 [#34383](https://github.com/gatsbyjs/gatsby/issues/34383) ([73b4625](https://github.com/gatsbyjs/gatsby/commit/73b462591f1e97a5d84803c792868a8058e895ff))
+
+### [4.5.2](https://github.com/gatsbyjs/gatsby/commits/gatsby-cli@4.5.2/packages/gatsby-cli) (2022-01-17)
+
+#### Bug Fixes
+
+- Re-Add plugin-add functionality [#34482](https://github.com/gatsbyjs/gatsby/issues/34482) [#34510](https://github.com/gatsbyjs/gatsby/issues/34510) ([0f5f7e4](https://github.com/gatsbyjs/gatsby/commit/0f5f7e46ca4e803a1f43059e5de984ce8cd150f3))
+
### [4.5.1](https://github.com/gatsbyjs/gatsby/commits/gatsby-cli@4.5.1/packages/gatsby-cli) (2022-01-12)
**Note:** Version bump only for package gatsby-cli
diff --git a/packages/gatsby-cli/package.json b/packages/gatsby-cli/package.json
index 92727d8c9ce32..36a0c3ced8dde 100644
--- a/packages/gatsby-cli/package.json
+++ b/packages/gatsby-cli/package.json
@@ -1,7 +1,7 @@
{
"name": "gatsby-cli",
"description": "Gatsby command-line interface for creating new sites and running Gatsby commands",
- "version": "4.6.0-next.3",
+ "version": "4.8.0-next.3",
"author": "Kyle Mathews ",
"bin": {
"gatsby": "cli.js"
@@ -11,7 +11,12 @@
},
"dependencies": {
"@babel/code-frame": "^7.14.0",
+ "@babel/core": "^7.15.5",
+ "@babel/generator": "^7.16.8",
+ "@babel/helper-plugin-utils": "^7.16.7",
"@babel/runtime": "^7.15.4",
+ "@babel/template": "^7.16.7",
+ "@babel/types": "^7.16.8",
"@types/common-tags": "^1.8.1",
"better-opn": "^2.1.1",
"boxen": "^5.1.2",
@@ -20,13 +25,13 @@
"common-tags": "^1.8.2",
"configstore": "^5.0.1",
"convert-hrtime": "^3.0.0",
- "create-gatsby": "^2.6.0-next.2",
+ "create-gatsby": "^2.8.0-next.2",
"envinfo": "^7.8.1",
"execa": "^5.1.1",
"fs-exists-cached": "^1.0.0",
"fs-extra": "^10.0.0",
- "gatsby-core-utils": "^3.6.0-next.1",
- "gatsby-telemetry": "^3.6.0-next.1",
+ "gatsby-core-utils": "^3.8.0-next.1",
+ "gatsby-telemetry": "^3.8.0-next.1",
"hosted-git-info": "^3.0.8",
"is-valid-path": "^0.1.1",
"joi": "^17.4.2",
@@ -52,7 +57,6 @@
},
"devDependencies": {
"@babel/cli": "^7.15.4",
- "@babel/core": "^7.15.5",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-json": "^4.1.0",
@@ -60,17 +64,17 @@
"@rollup/plugin-replace": "^2.4.2",
"@types/hosted-git-info": "^3.0.2",
"@types/yargs": "^15.0.14",
- "babel-preset-gatsby-package": "^2.6.0-next.0",
+ "babel-preset-gatsby-package": "^2.8.0-next.0",
"cross-env": "^7.0.3",
"ink": "^3.2.0",
"ink-spinner": "^4.0.3",
"npm-run-all": "4.1.5",
"react": "^16.9.0",
"rimraf": "^3.0.2",
- "rollup": "^2.62.0",
+ "rollup": "^2.66.1",
"rollup-plugin-auto-external": "^2.0.0",
"rollup-plugin-internal": "^1.0.4",
- "typescript": "^4.5.4"
+ "typescript": "^4.5.5"
},
"files": [
"lib/",
diff --git a/packages/gatsby-cli/src/handlers/plugin-add-utils.ts b/packages/gatsby-cli/src/handlers/plugin-add-utils.ts
new file mode 100644
index 0000000000000..326f377619772
--- /dev/null
+++ b/packages/gatsby-cli/src/handlers/plugin-add-utils.ts
@@ -0,0 +1,193 @@
+import * as fs from "fs-extra"
+import execa from "execa"
+import _ from "lodash"
+import {
+ readConfigFile,
+ lock,
+ getConfigPath,
+ getConfigStore,
+} from "gatsby-core-utils"
+import { transform } from "@babel/core"
+import { BabelPluginAddPluginsToGatsbyConfig } from "./plugin-babel-utils"
+
+const addPluginToConfig = (
+ src: string,
+ {
+ name,
+ options,
+ key,
+ }: {
+ name: string
+ options: Record | undefined
+ key: string
+ }
+): string => {
+ const addPlugins = new BabelPluginAddPluginsToGatsbyConfig({
+ pluginOrThemeName: name,
+ options,
+ shouldAdd: true,
+ key,
+ })
+
+ // @ts-ignore - fix me
+ const { code } = transform(src, {
+ // @ts-ignore - fix me
+ plugins: [addPlugins.plugin],
+ configFile: false,
+ })
+
+ return code
+}
+
+interface IGatsbyPluginCreateInput {
+ root: string
+ name: string
+ options: Record | undefined
+ key: string
+}
+
+export const GatsbyPluginCreate = async ({
+ root,
+ name,
+ options,
+ key,
+}: IGatsbyPluginCreateInput): Promise => {
+ const release = await lock(`gatsby-config.js`)
+ const configSrc = await readConfigFile(root)
+
+ const code = addPluginToConfig(configSrc, { name, options, key })
+
+ await fs.writeFile(getConfigPath(root), code)
+ release()
+}
+
+const packageMangerConfigKey = `cli.packageManager`
+const PACKAGE_MANAGER = getConfigStore().get(packageMangerConfigKey) || `yarn`
+
+const getPackageNames = (
+ packages: Array<{ name: string; version: string }>
+): Array => packages.map(n => `${n.name}@${n.version}`)
+
+const generateClientCommands = ({
+ packageManager,
+ depType,
+ packageNames,
+}: {
+ packageManager: string
+ depType: string
+ packageNames: Array
+}): Array | undefined => {
+ const commands: Array = []
+ if (packageManager === `yarn`) {
+ commands.push(`add`)
+ // Needed for Yarn Workspaces and is a no-opt elsewhere.
+ commands.push(`-W`)
+ if (depType === `development`) {
+ commands.push(`--dev`)
+ }
+
+ return commands.concat(packageNames)
+ } else if (packageManager === `npm`) {
+ commands.push(`install`)
+ if (depType === `development`) {
+ commands.push(`--save-dev`)
+ }
+ return commands.concat(packageNames)
+ }
+
+ return undefined
+}
+
+let installs: Array<{
+ outsideResolve: any
+ outsideReject: any
+ resource: any
+}> = []
+const executeInstalls = async (root: string): Promise => {
+ // @ts-ignore - fix me
+ const types = _.groupBy(installs, c => c.resource.dependencyType)
+
+ // Grab the key of the first install & delete off installs these packages
+ // then run intall
+ // when done, check again & call executeInstalls again.
+ // @ts-ignore - fix me
+ const depType = installs[0].resource.dependencyType
+ const packagesToInstall = types[depType]
+ installs = installs.filter(
+ // @ts-ignore - fix me
+ i => !packagesToInstall.some(p => i.resource.name === p.resource.name)
+ )
+
+ // @ts-ignore - fix me
+ const pkgs = packagesToInstall.map(p => p.resource)
+ const packageNames = getPackageNames(pkgs)
+
+ const commands = generateClientCommands({
+ packageNames,
+ depType,
+ packageManager: PACKAGE_MANAGER,
+ })
+
+ const release = await lock(`package.json`)
+ try {
+ await execa(PACKAGE_MANAGER, commands, {
+ cwd: root,
+ })
+ } catch (e) {
+ // A package failed so call the rejects
+ return packagesToInstall.forEach(p => {
+ // @ts-ignore - fix me
+ p.outsideReject(
+ JSON.stringify({
+ message: e.shortMessage,
+ installationError: `Could not install package`,
+ })
+ )
+ })
+ }
+ release()
+
+ // @ts-ignore - fix me
+ packagesToInstall.forEach(p => p.outsideResolve())
+
+ // Run again if there's still more installs.
+ if (installs.length > 0) {
+ executeInstalls(root)
+ }
+
+ return undefined
+}
+
+const debouncedExecute = _.debounce(executeInstalls, 25)
+
+interface IPackageCreateInput {
+ root: string
+ name: string
+}
+
+const createInstall = async ({
+ root,
+ name,
+}: IPackageCreateInput): Promise => {
+ let outsideResolve
+ let outsideReject
+ const promise = new Promise((resolve, reject) => {
+ outsideResolve = resolve
+ outsideReject = reject
+ })
+ installs.push({
+ outsideResolve,
+ outsideReject,
+ resource: name,
+ })
+
+ debouncedExecute(root)
+ return promise
+}
+
+export const NPMPackageCreate = async ({
+ root,
+ name,
+}: IPackageCreateInput): Promise => {
+ await createInstall({ root, name })
+}
diff --git a/packages/gatsby-cli/src/handlers/plugin-add.ts b/packages/gatsby-cli/src/handlers/plugin-add.ts
new file mode 100644
index 0000000000000..335dbf3cf1aed
--- /dev/null
+++ b/packages/gatsby-cli/src/handlers/plugin-add.ts
@@ -0,0 +1,86 @@
+import reporter from "../reporter"
+import { GatsbyPluginCreate, NPMPackageCreate } from "./plugin-add-utils"
+
+const normalizePluginName = (plugin: string): string => {
+ if (plugin.startsWith(`gatsby-`)) {
+ return plugin
+ }
+ if (
+ plugin.startsWith(`source-`) ||
+ plugin.startsWith(`transformer-`) ||
+ plugin.startsWith(`plugin-`)
+ ) {
+ return `gatsby-${plugin}`
+ }
+ return `gatsby-plugin-${plugin}`
+}
+
+async function installPluginPackage(
+ plugin: string,
+ root: string
+): Promise {
+ const installTimer = reporter.activityTimer(`Installing ${plugin}`)
+
+ installTimer.start()
+ reporter.info(`Installing ${plugin}`)
+ try {
+ await NPMPackageCreate({ root, name: plugin })
+ reporter.info(`Installed NPM package ${plugin}`)
+ } catch (err) {
+ reporter.error(JSON.parse(err)?.message)
+ installTimer.setStatus(`FAILED`)
+ }
+ installTimer.end()
+}
+
+async function installPluginConfig(
+ plugin: string,
+ options: Record | undefined,
+ root: string
+): Promise {
+ // Plugins can optionally include a key, to allow duplicates
+ const [pluginName, pluginKey] = plugin.split(`:`)
+
+ const installTimer = reporter.activityTimer(
+ `Adding ${pluginName} ${pluginKey ? `(${pluginKey}) ` : ``}to gatsby-config`
+ )
+
+ installTimer.start()
+ reporter.info(`Adding ${pluginName}`)
+ try {
+ await GatsbyPluginCreate({
+ root,
+ name: pluginName,
+ options,
+ key: pluginKey,
+ })
+ reporter.info(`Installed ${pluginName || pluginKey} in gatsby-config.js`)
+ } catch (err) {
+ reporter.error(JSON.parse(err)?.message)
+ installTimer.setStatus(`FAILED`)
+ }
+ installTimer.end()
+}
+
+export async function addPlugins(
+ plugins: Array,
+ pluginOptions: Record>,
+ directory: string,
+ packages: Array = []
+): Promise {
+ if (!plugins?.length) {
+ reporter.error(`Please specify a plugin to install`)
+ return
+ }
+
+ const pluginList = plugins.map(normalizePluginName)
+
+ await Promise.all(
+ packages.map(plugin => installPluginPackage(plugin, directory))
+ )
+ await Promise.all(
+ pluginList.map(plugin =>
+ installPluginConfig(plugin, pluginOptions[plugin], directory)
+ )
+ )
+}
diff --git a/packages/gatsby-cli/src/handlers/plugin-babel-utils.ts b/packages/gatsby-cli/src/handlers/plugin-babel-utils.ts
new file mode 100644
index 0000000000000..7ab57e5debe32
--- /dev/null
+++ b/packages/gatsby-cli/src/handlers/plugin-babel-utils.ts
@@ -0,0 +1,319 @@
+import * as t from "@babel/types"
+import generate from "@babel/generator"
+import template from "@babel/template"
+import { declare } from "@babel/helper-plugin-utils"
+
+const getKeyNameFromAttribute = (node: any): any =>
+ node.key.name || node.key.value
+
+const unwrapTemplateLiteral = (str: string): string =>
+ str.trim().replace(/^`/, ``).replace(/`$/, ``)
+
+const isLiteral = (node: any): boolean =>
+ t.isLiteral(node) || t.isStringLiteral(node) || t.isNumericLiteral(node)
+
+const getObjectFromNode = (nodeValue: any): any => {
+ if (!nodeValue || !nodeValue.properties) {
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ return getValueFromNode(nodeValue)
+ }
+
+ const props = nodeValue.properties.reduce((acc, curr) => {
+ let value = null
+
+ if (curr.value) {
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ value = getValueFromNode(curr.value)
+ } else if (t.isObjectExpression(curr.value)) {
+ value = curr.value.expression.properties.reduce((acc, curr) => {
+ acc[getKeyNameFromAttribute(curr)] = getObjectFromNode(curr)
+ return acc
+ }, {})
+ } else {
+ throw new Error(`Did not recognize ${curr}`)
+ }
+
+ acc[getKeyNameFromAttribute(curr)] = value
+ return acc
+ }, {})
+
+ return props
+}
+
+const getValueFromNode = (node: any): any => {
+ if (t.isTemplateLiteral(node)) {
+ // @ts-ignore - fix me
+ delete node.leadingComments
+ // @ts-ignore - fix me
+ delete node.trailingComments
+ // @ts-ignore - fix me
+ const literalContents = generate(node).code
+ return unwrapTemplateLiteral(literalContents)
+ }
+
+ if (isLiteral(node)) {
+ return node.value
+ }
+
+ if (node.type === `ArrayExpression`) {
+ return node.elements.map(getObjectFromNode)
+ }
+
+ if (node.type === `ObjectExpression`) {
+ return getObjectFromNode(node)
+ }
+
+ return null
+}
+
+function isDefaultExport(node): boolean {
+ if (!node || !t.isMemberExpression(node)) {
+ return false
+ }
+
+ const { object, property } = node
+
+ if (!t.isIdentifier(object) || object.name !== `module`) return false
+ if (!t.isIdentifier(property) || property.name !== `exports`) return false
+
+ return true
+}
+
+const getOptionsForPlugin = (node: any): any => {
+ if (!t.isObjectExpression(node) && !t.isLogicalExpression(node)) {
+ return undefined
+ }
+
+ let options
+
+ // When a plugin is added conditionally with && {}
+ if (t.isLogicalExpression(node)) {
+ // @ts-ignore - fix me
+ options = node.right.properties.find(
+ property => property.key.name === `options`
+ )
+ } else {
+ // @ts-ignore - fix me
+ options = node.properties.find(property => property.key.name === `options`)
+ }
+
+ if (options) {
+ return getObjectFromNode(options.value)
+ }
+
+ return undefined
+}
+
+const getKeyForPlugin = (node: any): any => {
+ if (t.isObjectExpression(node)) {
+ // @ts-ignore - fix me
+ const key = node.properties.find(p => p.key.name === `__key`)
+
+ // @ts-ignore - fix me
+ return key ? getValueFromNode(key.value) : null
+ }
+
+ // When a plugin is added conditionally with && {}
+ if (t.isLogicalExpression(node)) {
+ // @ts-ignore - fix me
+ const key = node.right.properties.find(p => p.key.name === `__key`)
+
+ return key ? getValueFromNode(key.value) : null
+ }
+
+ return null
+}
+
+const getNameForPlugin = (node: any): any => {
+ if (t.isStringLiteral(node) || t.isTemplateLiteral(node)) {
+ return getValueFromNode(node)
+ }
+
+ if (t.isObjectExpression(node)) {
+ // @ts-ignore - fix me
+ const resolve = node.properties.find(p => p.key.name === `resolve`)
+
+ // @ts-ignore - fix me
+ return resolve ? getValueFromNode(resolve.value) : null
+ }
+
+ // When a plugin is added conditionally with && {}
+ if (t.isLogicalExpression(node)) {
+ // @ts-ignore - fix me
+ const resolve = node.right.properties.find(p => p.key.name === `resolve`)
+
+ return resolve ? getValueFromNode(resolve.value) : null
+ }
+
+ return null
+}
+
+const getPlugin = (node: any): any => {
+ const plugin = {
+ name: getNameForPlugin(node),
+ options: getOptionsForPlugin(node),
+ }
+
+ const key = getKeyForPlugin(node)
+
+ if (key) {
+ return { ...plugin, key }
+ }
+
+ return plugin
+}
+
+function buildPluginNode({ name, options, key }): any {
+ if (!options && !key) {
+ return t.stringLiteral(name)
+ }
+
+ const pluginWithOptions = template(
+ `
+ const foo = {
+ resolve: '${name}',
+ options: ${JSON.stringify(options, null, 2)},
+ ${key ? `__key: "` + key + `"` : ``}
+ }
+ `,
+ { placeholderPattern: false }
+ )()
+
+ // @ts-ignore - fix me
+ return pluginWithOptions.declarations[0].init
+}
+
+export class BabelPluginAddPluginsToGatsbyConfig {
+ constructor({ pluginOrThemeName, shouldAdd, options, key }) {
+ // @ts-ignore - fix me
+ this.plugin = declare(api => {
+ api.assertVersion(7)
+
+ return {
+ visitor: {
+ ExpressionStatement(path): void {
+ const { node } = path
+ const { left, right } = node.expression
+
+ if (!isDefaultExport(left)) {
+ return
+ }
+
+ const pluginNodes = right.properties.find(
+ p => p.key.name === `plugins`
+ )
+
+ if (shouldAdd) {
+ if (t.isCallExpression(pluginNodes.value)) {
+ const plugins =
+ pluginNodes.value.callee.object.elements.map(getPlugin)
+ const matches = plugins.filter(plugin => {
+ if (!key) {
+ return plugin.name === pluginOrThemeName
+ }
+
+ return plugin.key === key
+ })
+
+ if (!matches.length) {
+ const pluginNode = buildPluginNode({
+ name: pluginOrThemeName,
+ options,
+ key,
+ })
+
+ pluginNodes.value.callee.object.elements.push(pluginNode)
+ } else {
+ pluginNodes.value.callee.object.elements =
+ pluginNodes.value.callee.object.elements.map(node => {
+ const plugin = getPlugin(node)
+
+ if (plugin.key !== key) {
+ return node
+ }
+
+ if (!plugin.key && plugin.name !== pluginOrThemeName) {
+ return node
+ }
+
+ return buildPluginNode({
+ name: pluginOrThemeName,
+ options,
+ key,
+ })
+ })
+ }
+ } else {
+ const plugins = pluginNodes.value.elements.map(getPlugin)
+ const matches = plugins.filter(plugin => {
+ if (!key) {
+ return plugin.name === pluginOrThemeName
+ }
+
+ return plugin.key === key
+ })
+
+ if (!matches.length) {
+ const pluginNode = buildPluginNode({
+ name: pluginOrThemeName,
+ options,
+ key,
+ })
+
+ pluginNodes.value.elements.push(pluginNode)
+ } else {
+ pluginNodes.value.elements = pluginNodes.value.elements.map(
+ node => {
+ const plugin = getPlugin(node)
+
+ if (plugin.key !== key) {
+ return node
+ }
+
+ if (!plugin.key && plugin.name !== pluginOrThemeName) {
+ return node
+ }
+
+ return buildPluginNode({
+ name: pluginOrThemeName,
+ options,
+ key,
+ })
+ }
+ )
+ }
+ }
+ } else {
+ if (t.isCallExpression(pluginNodes.value)) {
+ pluginNodes.value.callee.object.elements =
+ pluginNodes.value.callee.object.elements.filter(node => {
+ const plugin = getPlugin(node)
+
+ if (key) {
+ return plugin.key !== key
+ }
+
+ return plugin.name !== pluginOrThemeName
+ })
+ } else {
+ pluginNodes.value.elements = pluginNodes.value.elements.filter(
+ node => {
+ const plugin = getPlugin(node)
+
+ if (key) {
+ return plugin.key !== key
+ }
+
+ return plugin.name !== pluginOrThemeName
+ }
+ )
+ }
+ }
+
+ path.stop()
+ },
+ },
+ }
+ })
+ }
+}
diff --git a/packages/gatsby-cli/src/structured-errors/error-map.ts b/packages/gatsby-cli/src/structured-errors/error-map.ts
index 6617a68ee2430..069bd571b48fb 100644
--- a/packages/gatsby-cli/src/structured-errors/error-map.ts
+++ b/packages/gatsby-cli/src/structured-errors/error-map.ts
@@ -627,31 +627,37 @@ const errors = {
/** Node Manifest warnings */
"11801": {
- // @todo add docs link to "using Preview" once it's updated with an explanation of ownerNodeId
text: ({ inputManifest }): string => `${getSharedNodeManifestWarning(
inputManifest
)} but Gatsby couldn't find a page for this node.
- If you want a manifest to be created for this node (for previews or other purposes), ensure that a page was created (and that a ownerNodeId is added to createPage() if you're not using the Filesystem Route API).\n`,
+ If you want a manifest to be created for this node (for previews or other purposes), ensure that a page was created (and that a ownerNodeId is added to createPage() if you're not using the Filesystem Route API). See https://www.gatsbyjs.com/docs/conceptual/content-sync for more info.\n`,
level: Level.WARNING,
category: ErrorCategory.USER,
},
"11802": {
- // @todo add docs link to "using Preview" once it's updated with an explanation of ownerNodeId
text: ({ inputManifest, pagePath }): string =>
`${getSharedNodeManifestWarning(
inputManifest
- )} but Gatsby didn't find a ownerNodeId for the page at ${pagePath}\nUsing the first page that was found with the node manifest id set in pageContext.id in createPage().\nThis may result in an inaccurate node manifest (for previews or other purposes).`,
+ )} but Gatsby didn't find an ownerNodeId for the page at ${pagePath}\nUsing the first page that was found with the node manifest id set in pageContext.id in createPage().\nThis may result in an inaccurate node manifest (for previews or other purposes). See https://www.gatsbyjs.com/docs/conceptual/content-sync for more info.`,
+ level: Level.WARNING,
+ category: ErrorCategory.USER,
+ },
+
+ "11805": {
+ text: ({ inputManifest, pagePath }): string =>
+ `${getSharedNodeManifestWarning(
+ inputManifest
+ )} but Gatsby didn't find an ownerNodeId for the page at ${pagePath}\nUsing the first page that was found with the node manifest id set in pageContext.slug in createPage().\nThis may result in an inaccurate node manifest (for previews or other purposes). See https://www.gatsbyjs.com/docs/conceptual/content-sync for more info.`,
level: Level.WARNING,
category: ErrorCategory.USER,
},
"11803": {
- // @todo add docs link to "using Preview" once it's updated with an explanation of ownerNodeId
text: ({ inputManifest, pagePath }): string =>
`${getSharedNodeManifestWarning(
inputManifest
- )} but Gatsby didn't find a ownerNodeId for the page at ${pagePath}\nUsing the first page where this node is queried.\nThis may result in an inaccurate node manifest (for previews or other purposes).`,
+ )} but Gatsby didn't find an ownerNodeId for the page at ${pagePath}\nUsing the first page where this node is queried.\nThis may result in an inaccurate node manifest (for previews or other purposes). See https://www.gatsbyjs.com/docs/conceptual/content-sync for more info.`,
level: Level.WARNING,
category: ErrorCategory.USER,
},
diff --git a/packages/gatsby-cli/src/structured-errors/error-schema.ts b/packages/gatsby-cli/src/structured-errors/error-schema.ts
index 306da6d31bdee..ec7dcdbb0e89c 100644
--- a/packages/gatsby-cli/src/structured-errors/error-schema.ts
+++ b/packages/gatsby-cli/src/structured-errors/error-schema.ts
@@ -1,10 +1,12 @@
import Joi from "joi"
import { ILocationPosition, IStructuredError } from "./types"
-export const Position: Joi.ObjectSchema = Joi.object().keys({
- line: Joi.number(),
- column: Joi.number(),
-})
+export const Position: Joi.ObjectSchema = Joi.object()
+ .keys({
+ line: Joi.number(),
+ column: Joi.number(),
+ })
+ .unknown()
export const errorSchema: Joi.ObjectSchema =
Joi.object().keys({
@@ -27,7 +29,7 @@ export const errorSchema: Joi.ObjectSchema =
location: Joi.object({
start: Position.required(),
end: Position,
- }),
+ }).unknown(),
docsUrl: Joi.string().uri({
allowRelative: false,
relativeOnly: false,
diff --git a/packages/gatsby-codemods/CHANGELOG.md b/packages/gatsby-codemods/CHANGELOG.md
index b7d4ae0cf91f4..444c188935530 100644
--- a/packages/gatsby-codemods/CHANGELOG.md
+++ b/packages/gatsby-codemods/CHANGELOG.md
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [3.7.0](https://github.com/gatsbyjs/gatsby/commits/gatsby-codemods@3.7.0/packages/gatsby-codemods) (2022-02-08)
+
+[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.7)
+
+**Note:** Version bump only for package gatsby-codemods
+
+## [3.6.0](https://github.com/gatsbyjs/gatsby/commits/gatsby-codemods@3.6.0/packages/gatsby-codemods) (2022-01-25)
+
+[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.6)
+
+**Note:** Version bump only for package gatsby-codemods
+
## [3.5.0](https://github.com/gatsbyjs/gatsby/commits/gatsby-codemods@3.5.0/packages/gatsby-codemods) (2022-01-11)
[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.5)
diff --git a/packages/gatsby-codemods/package.json b/packages/gatsby-codemods/package.json
index cb6aaa28f2068..c41d1d4cdf7e6 100644
--- a/packages/gatsby-codemods/package.json
+++ b/packages/gatsby-codemods/package.json
@@ -1,6 +1,6 @@
{
"name": "gatsby-codemods",
- "version": "3.6.0-next.0",
+ "version": "3.8.0-next.0",
"description": "A collection of codemod scripts for use with JSCodeshift that help migrate to newer versions of Gatsby.",
"main": "index.js",
"scripts": {
@@ -36,7 +36,7 @@
},
"devDependencies": {
"@babel/cli": "^7.15.4",
- "babel-preset-gatsby-package": "^2.6.0-next.0",
+ "babel-preset-gatsby-package": "^2.8.0-next.0",
"cross-env": "^7.0.3"
},
"engines": {
diff --git a/packages/gatsby-core-utils/CHANGELOG.md b/packages/gatsby-core-utils/CHANGELOG.md
index 77c7b00553418..b00f990e72eaf 100644
--- a/packages/gatsby-core-utils/CHANGELOG.md
+++ b/packages/gatsby-core-utils/CHANGELOG.md
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [3.7.0](https://github.com/gatsbyjs/gatsby/commits/gatsby-core-utils@3.7.0/packages/gatsby-core-utils) (2022-02-08)
+
+[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.7)
+
+#### Bug Fixes
+
+- ensure remote file downloads are queued in all cases [#34414](https://github.com/gatsbyjs/gatsby/issues/34414) ([6ac1ed6](https://github.com/gatsbyjs/gatsby/commit/6ac1ed6ad61b86094e3b57004e50981820390965))
+
+#### Chores
+
+- update dependency msw to ^0.36.8 for gatsby-core-utils [#34639](https://github.com/gatsbyjs/gatsby/issues/34639) ([741f30b](https://github.com/gatsbyjs/gatsby/commit/741f30b2d843207a78da297c0ce97323169b754c))
+- update dependency typescript to ^4.5.5 [#34641](https://github.com/gatsbyjs/gatsby/issues/34641) ([f7a7e1f](https://github.com/gatsbyjs/gatsby/commit/f7a7e1f642d91babb397156ab37cb28dcde19737))
+
+## [3.6.0](https://github.com/gatsbyjs/gatsby/commits/gatsby-core-utils@3.6.0/packages/gatsby-core-utils) (2022-01-25)
+
+[🧾 Release notes](https://www.gatsbyjs.com/docs/reference/release-notes/v4.6)
+
+#### Bug Fixes
+
+- Re-Add plugin-add functionality [#34482](https://github.com/gatsbyjs/gatsby/issues/34482) ([618b32b](https://github.com/gatsbyjs/gatsby/commit/618b32b17751c76ea1b1a6f4fbc91da928bd18c1))
+- Re-Export updateSiteMetadata [#34462](https://github.com/gatsbyjs/gatsby/issues/34462) ([d061b1c](https://github.com/gatsbyjs/gatsby/commit/d061b1c5b92553a95da32707cc6a29380ec365c2))
+
+### [3.5.2](https://github.com/gatsbyjs/gatsby/commits/gatsby-core-utils@3.5.2/packages/gatsby-core-utils) (2022-01-17)
+
+#### Bug Fixes
+
+- Re-Add plugin-add functionality [#34482](https://github.com/gatsbyjs/gatsby/issues/34482) [#34510](https://github.com/gatsbyjs/gatsby/issues/34510) ([0f5f7e4](https://github.com/gatsbyjs/gatsby/commit/0f5f7e46ca4e803a1f43059e5de984ce8cd150f3))
+
### [3.5.1](https://github.com/gatsbyjs/gatsby/commits/gatsby-core-utils@3.5.1/packages/gatsby-core-utils) (2022-01-12)
#### Bug Fixes
diff --git a/packages/gatsby-core-utils/README.md b/packages/gatsby-core-utils/README.md
index fb16a3b62bdad..30d512d527339 100644
--- a/packages/gatsby-core-utils/README.md
+++ b/packages/gatsby-core-utils/README.md
@@ -104,3 +104,20 @@ const requireUtil = createRequireFromPath("../src/utils/")
requireUtil("./some-tool")
// ...
```
+
+### Mutex
+
+When working inside workers or async operations you want some kind of concurrency control that a specific work load can only concurrent one at a time. This is what a [Mutex](https://en.wikipedia.org/wiki/Mutual_exclusion) does.
+
+By implementing the following code, the code is only executed one at a time and the other threads/async workloads are awaited until the current one is done. This is handy when writing to the same file to disk.
+
+```js
+const { createMutex } = require("gatsby-core-utils/mutex")
+
+const mutex = createMutex("my-custom-mutex-key")
+await mutex.acquire()
+
+await fs.writeFile("pathToFile", "my custom content")
+
+await mutex.release()
+```
diff --git a/packages/gatsby-core-utils/package.json b/packages/gatsby-core-utils/package.json
index ba656435f698a..7f8489979acb9 100644
--- a/packages/gatsby-core-utils/package.json
+++ b/packages/gatsby-core-utils/package.json
@@ -1,11 +1,23 @@
{
"name": "gatsby-core-utils",
- "version": "3.6.0-next.1",
+ "version": "3.8.0-next.1",
"description": "A collection of gatsby utils used in different gatsby packages",
"keywords": [
"gatsby",
"gatsby-core-utils"
],
+ "exports": {
+ ".": "./dist/index.js",
+ "./*": "./dist/*.js"
+ },
+ "typesVersions": {
+ "*": {
+ "*": [
+ "dist/*.d.ts",
+ "dist/index.d.ts"
+ ]
+ }
+ },
"author": "Ward Peeters ",
"homepage": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-core-utils#readme",
"license": "MIT",
@@ -32,12 +44,16 @@
"@babel/runtime": "^7.15.4",
"ci-info": "2.0.0",
"configstore": "^5.0.1",
+ "fastq": "^1.13.0",
"file-type": "^16.5.3",
"fs-extra": "^10.0.0",
"got": "^11.8.3",
+ "import-from": "^4.0.0",
+ "lmdb": "^2.1.7",
"lock": "^1.1.0",
"node-object-hash": "^2.3.10",
"proper-lockfile": "^4.1.2",
+ "resolve-from": "^5.0.0",
"tmp": "^0.2.1",
"xdg-basedir": "^4.0.0"
},
@@ -45,11 +61,11 @@
"@babel/cli": "^7.15.4",
"@babel/core": "^7.15.5",
"@types/ci-info": "2.0.0",
- "babel-preset-gatsby-package": "^2.6.0-next.0",
+ "babel-preset-gatsby-package": "^2.8.0-next.0",
"cross-env": "^7.0.3",
"is-uuid": "^1.0.2",
- "msw": "^0.36.3",
- "typescript": "^4.5.4"
+ "msw": "^0.36.8",
+ "typescript": "^4.5.5"
},
"engines": {
"node": ">=14.15.0"
diff --git a/packages/gatsby-core-utils/src/__tests__/mutex.ts b/packages/gatsby-core-utils/src/__tests__/mutex.ts
new file mode 100644
index 0000000000000..66b7f00acd5c4
--- /dev/null
+++ b/packages/gatsby-core-utils/src/__tests__/mutex.ts
@@ -0,0 +1,99 @@
+import path from "path"
+import { remove, mkdirp } from "fs-extra"
+import { createMutex } from "../mutex"
+import * as storage from "../utils/get-storage"
+
+jest.spyOn(storage, `getDatabaseDir`)
+
+function sleep(timeout = 100): Promise {
+ return new Promise(resolve => setTimeout(resolve, timeout))
+}
+
+async function doAsync(
+ mutex: ReturnType,
+ result: Array = [],
+ waitTime: number,
+ id: string
+): Promise> {
+ await mutex.acquire()
+ result.push(`start ${id}`)
+ await sleep(waitTime)
+ result.push(`stop ${id}`)
+ await mutex.release()
+
+ return result
+}
+
+describe(`mutex`, () => {
+ const cachePath = path.join(__dirname, `.cache`)
+ beforeAll(async () => {
+ await mkdirp(cachePath)
+ storage.getDatabaseDir.mockReturnValue(cachePath)
+ })
+
+ afterAll(async () => {
+ await storage.closeDatabase()
+ await remove(cachePath)
+ })
+
+ it(`should only allow one action go through at the same time`, async () => {
+ const mutex = createMutex(`test-key`, 300)
+
+ const result: Array = []
+
+ doAsync(mutex, result, 50, `1`)
+ await sleep(0)
+ await doAsync(mutex, result, 10, `2`)
+
+ expect(result).toMatchInlineSnapshot(`
+ Array [
+ "start 1",
+ "stop 1",
+ "start 2",
+ "stop 2",
+ ]
+ `)
+ })
+
+ it(`should generate the same mutex if key are identical`, async () => {
+ const mutex1 = createMutex(`test-key`, 300)
+ const mutex2 = createMutex(`test-key`, 300)
+
+ const result: Array = []
+
+ const mutexPromise = doAsync(mutex1, result, 50, `1`)
+ await sleep(0)
+ await doAsync(mutex2, result, 10, `2`)
+ await mutexPromise
+
+ expect(result).toMatchInlineSnapshot(`
+ Array [
+ "start 1",
+ "stop 1",
+ "start 2",
+ "stop 2",
+ ]
+ `)
+ })
+
+ it(`shouldn't wait if keys are different`, async () => {
+ const mutex1 = createMutex(`test-key`, 300)
+ const mutex2 = createMutex(`other-key`, 300)
+
+ const result: Array = []
+
+ const mutexPromise = doAsync(mutex1, result, 50, `1`)
+ await sleep(0)
+ await doAsync(mutex2, result, 10, `2`)
+ await mutexPromise
+
+ expect(result).toMatchInlineSnapshot(`
+ Array [
+ "start 1",
+ "start 2",
+ "stop 2",
+ "stop 1",
+ ]
+ `)
+ })
+})
diff --git a/packages/gatsby-core-utils/src/fetch-remote-file.ts b/packages/gatsby-core-utils/src/fetch-remote-file.ts
index 52e98d20cfcba..f739bc4104e4b 100644
--- a/packages/gatsby-core-utils/src/fetch-remote-file.ts
+++ b/packages/gatsby-core-utils/src/fetch-remote-file.ts
@@ -10,6 +10,8 @@ import {
} from "./filename-utils"
import type { IncomingMessage } from "http"
import type { GatsbyCache } from "gatsby"
+import Queue from "fastq"
+import type { queue, done } from "fastq"
export interface IFetchRemoteFileOptions {
url: string
@@ -72,9 +74,64 @@ const ERROR_CODES_TO_RETRY = [
`ERR_GOT_REQUEST_ERROR`,
]
+/********************
+ * Queue Management *
+ ********************/
+
+const GATSBY_CONCURRENT_DOWNLOAD = process.env.GATSBY_CONCURRENT_DOWNLOAD
+ ? parseInt(process.env.GATSBY_CONCURRENT_DOWNLOAD, 10) || 0
+ : 50
+
+const q: queue = Queue(
+ fetchWorker,
+ GATSBY_CONCURRENT_DOWNLOAD
+)
+
+/**
+ * fetchWorker
+ * --
+ * Handle fetch requests that are pushed in to the Queue
+ */
+async function fetchWorker(
+ task: IFetchRemoteFileOptions,
+ cb: done
+): Promise {
+ try {
+ const node = await fetchFile(task)
+ return void cb(null, node)
+ } catch (e) {
+ return void cb(e)
+ }
+}
+
+/**
+ * pushTask
+ * --
+ * pushes a task in to the Queue and the processing cache
+ *
+ * Promisfy a task in queue
+ * @param {CreateRemoteFileNodePayload} task
+ * @return {Promise