From 8291044020254b04eb47b021101e79743794243a Mon Sep 17 00:00:00 2001 From: Dustin Schau Date: Thu, 2 May 2019 14:08:22 -0500 Subject: [PATCH] feat(gatsby): add assetPrefix to support deploying assets separate from html (#12128) * chore: add validation schema, start tweaking webpack config * chore: keep iterating * chore: yadda * chore: keep working * keep doing stuff * chore: get mostly done (let's see!) * chore: remove unused package * chore: ensure url is normalized correctly * chore: try try again * chore: fix for base path * test: tests are important; fix them * chore: remove a silly change * chore: fix linter Note: this should've been fine * fix(gatsby-plugin-offline): hard fail if assetPrefix is used Note: very possible this may be reverted * refactor: add a publicPath helper * test: add some get public path tests * chore: use correct name * docs: add asset prefix doc, and tweak path prefix * chore: allow relative url for assetPrefix * test: add a few more unit tests * test: clean up test * chore: fix e2e-test Note: this is a bug, will fix the underlying bug too. pathPrefix should have no effect unless using --prefix-paths * fix: fall back to empty string, not slash * Update docs/docs/asset-prefix.md * fix: handle relative paths * feat: add withAssetPrefix helper for gatsby-link This should rarely be used--but should be exposed * fix: use withAssetPrefix (if available) for gatsby-plugin-manifest * Allow using gatsby-plugin-offline with assetPrefix * Add docs for using offline-plugin with asset prefix * clarify docs * feat(*): use withAssetPrefix helper from gatsby-link BREAKING CHANGE: this is a breaking change (as currently authored) for a few plugins (specified in this commit). I'll work on a fallback--but I think it might make sense to just fail here. We can specify a peerDependency in the package.json of each of these packages, too. * test: get tests passing * test: add a test for assetPrefix with nesting * Update docs/docs/path-prefix.md Co-Authored-By: DSchau * chore: fix up merge conflicts/get tests passing * chore: tweak version * fix(gatsby-plugin-sitemap): work with asset prefix * fix(gatsby): disallow both relative assetPrefix and pathPrefix * chore: fallback to withPathPrefix, bump peerDep * chore: remove caveat re: trailing slash * fix: gatsby-plugin-sitemap regression * chore: revert peer dep * chore: use basePath if it's defined * chore: remove eslint global comment * chore: ensure prefixPaths is set to enable pathPrefix * chore: fix read-only error (can't reassign imports ya dingus) * chore: actually fallback * Update docs/docs/asset-prefix.md Co-Authored-By: DSchau * Update docs/docs/path-prefix.md Co-Authored-By: DSchau * Update docs/docs/asset-prefix.md Co-Authored-By: DSchau * chore: simply/merely remove the easy term ;) * Update docs/docs/asset-prefix.md Co-Authored-By: DSchau * test: write e2e test for asset prefix Note: this very well may fail * chore: fix package json and make isURL test stricter * chore: fix yarn and stuff hopefully * chore: minor clean up * fix(gatsby): fix initial navigation not registering in history * chore: remove unneccessary dep * fix: use __BASE_PATH__ in development runtime too; add a test * chore: fix @pieh nit before he finds it --- .eslintrc.json | 4 +- .../index.md | 3 +- docs/docs/asset-prefix.md | 96 +++++++++++++++++++ docs/docs/gatsby-config.md | 1 - docs/docs/path-prefix.md | 86 ++++++++++++----- .../cypress/integration/navigation/linking.js | 10 ++ e2e-tests/gatsby-image/gatsby-config.js | 1 - e2e-tests/path-prefix/.gitignore | 3 +- .../cypress/integration/asset-prefix.js | 52 ++++++++++ .../cypress/integration/path-prefix.js | 4 +- .../path-prefix/cypress/plugins/index.js | 17 ++++ e2e-tests/path-prefix/gatsby-config.js | 44 +++++++++ e2e-tests/path-prefix/package.json | 12 ++- e2e-tests/path-prefix/scripts/serve.js | 28 ++++++ packages/gatsby-link/index.d.ts | 1 + packages/gatsby-link/src/__tests__/index.js | 39 ++++++-- packages/gatsby-link/src/index.js | 7 +- .../src/__tests__/catch-links.js | 8 +- packages/gatsby-plugin-feed/package.json | 2 +- .../src/__tests__/gatsby-ssr.js | 8 +- packages/gatsby-plugin-feed/src/gatsby-ssr.js | 5 +- packages/gatsby-plugin-manifest/package.json | 2 +- .../src/__tests__/gatsby-ssr.js | 1 + .../gatsby-plugin-manifest/src/gatsby-ssr.js | 10 +- packages/gatsby-plugin-offline/README.md | 2 + .../src/__tests__/gatsby-node.js | 7 +- .../src/__tests__/internals.js | 8 ++ .../gatsby-plugin-sitemap/src/gatsby-node.js | 12 +-- .../gatsby-plugin-sitemap/src/gatsby-ssr.js | 5 +- .../gatsby-plugin-sitemap/src/internals.js | 1 - .../cache-dir/__tests__/static-entry.js | 2 + .../gatsby/cache-dir/gatsby-browser-entry.js | 2 + packages/gatsby/cache-dir/loader.js | 2 +- packages/gatsby/cache-dir/production-app.js | 13 ++- .../cache-dir/register-service-worker.js | 2 +- packages/gatsby/cache-dir/root.js | 2 +- packages/gatsby/cache-dir/static-entry.js | 4 +- packages/gatsby/index.d.ts | 1 + packages/gatsby/src/commands/serve.js | 21 +++- .../__tests__/__snapshots__/joi.js.snap | 5 + .../gatsby/src/joi-schemas/__tests__/joi.js | 61 ++++++++++++ packages/gatsby/src/joi-schemas/joi.js | 70 ++++++++++---- .../src/utils/__tests__/get-public-path.js | 81 ++++++++++++++++ packages/gatsby/src/utils/api-runner-node.js | 12 ++- packages/gatsby/src/utils/eslint-config.js | 1 + packages/gatsby/src/utils/get-public-path.js | 21 ++++ packages/gatsby/src/utils/path.js | 4 + packages/gatsby/src/utils/webpack.config.js | 30 +++--- yarn.lock | 5 + 49 files changed, 694 insertions(+), 124 deletions(-) create mode 100644 docs/docs/asset-prefix.md create mode 100644 e2e-tests/path-prefix/cypress/integration/asset-prefix.js create mode 100644 e2e-tests/path-prefix/cypress/plugins/index.js create mode 100644 e2e-tests/path-prefix/scripts/serve.js create mode 100644 packages/gatsby/src/joi-schemas/__tests__/__snapshots__/joi.js.snap create mode 100644 packages/gatsby/src/joi-schemas/__tests__/joi.js create mode 100644 packages/gatsby/src/utils/__tests__/get-public-path.js create mode 100644 packages/gatsby/src/utils/get-public-path.js diff --git a/.eslintrc.json b/.eslintrc.json index b9b35292e3d64..78637b6889ed2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -26,7 +26,9 @@ "globals": { "before": true, "spyOn": true, - "__PATH_PREFIX__": true + "__PATH_PREFIX__": true, + "__BASE_PATH__": true, + "__ASSET_PREFIX__": true }, "rules": { "arrow-body-style": [ diff --git a/docs/blog/2019-02-08-government-open-data-site-with-gatsby/index.md b/docs/blog/2019-02-08-government-open-data-site-with-gatsby/index.md index 0b0ef5316b1b1..72553314dce94 100644 --- a/docs/blog/2019-02-08-government-open-data-site-with-gatsby/index.md +++ b/docs/blog/2019-02-08-government-open-data-site-with-gatsby/index.md @@ -223,8 +223,7 @@ We set a `BASEURL` environment variable in `gatsby-config.js` that resolves the const BASEURL = process.env.BASEURL || "" module.exports = { - // Note: it must *not* have a trailing slash. - // This is currently the realtive path in our Jekyll deployment. This path points to our Gatsby pages. + // This is currently the relative path in our Jekyll deployment. This path points to our Gatsby pages. // This prefix is prepended to load all our related images, code, and pages. pathPrefix: `${BASEURL}/gatsby-public`, } diff --git a/docs/docs/asset-prefix.md b/docs/docs/asset-prefix.md new file mode 100644 index 0000000000000..798ac773af26f --- /dev/null +++ b/docs/docs/asset-prefix.md @@ -0,0 +1,96 @@ +--- +title: Adding an Asset Prefix +--- + +Gatsby produces static content that can be hosted _anywhere_ at scale in a cost-effective manner. This static content is comprised of HTML files, JavaScript, CSS, images, and more that power your great Gatsby application. + +In some circumstances you may want to deploy _assets_ (non-HTML resources such as JavaScript, CSS, etc.) to a separate domain. Typically this is when you're required to use a dedicated CDN for assets or need to follow company-specific hosting policies. + +This `assetPrefix` functionality is available starting in gatsby@2.4.0, so that you can seamlessly use Gatsby with assets hosted from a separate domain. To use this functionality, ensure that your version of `gatsby` specified in `package.json` is at least `2.4.0`. + +## Usage + +### Adding to `gatsby-config.js` + +```js:title=gatsby-config.js +module.exports = { + assetPrefix: `https://cdn.example.com`, +} +``` + +One more step - when we build out this application, we need to add a flag so that Gatsby picks up this option. + +### The `--prefix-paths` flag + +When building with an `assetPrefix`, we require a `--prefix-paths` flag. If this flag is not specified, the build will ignore this option, and build out content as if it was hosted on the same domain. To ensure we build out successfully, use the following command: + +```shell +gatsby build --prefix-paths +``` + +That's it! We now have an application that is ready to have its assets deployed from a CDN and its core files (e.g. HTML files) can be hosted on a separate domain. + +## Building / Deploying + +Once your application is built out, all assets will be automatically prefixed by this asset prefix. For example, if we have a JavaScript file `app-common-1234.js`, the script tag will look something like: + +```html + +``` + +However - if we were to deploy our application as-is, those assets would not be available! We can do this in a few ways, but the general approach will be to deploy the contents of the `public` folder to _both_ your core domain, and the CDN/asset prefix location. + +### Using `onPostBuild` + +We expose an [`onPostBuild`](/docs/node-apis/#onPostBuild) API hook. This can be used to deploy your content to the CDN, like so: + +```js:title=gatsby-node.js +const assetsDirectory = `public` + +exports.onPostBuild = async function onPostBuild() { + // do something with public + // e.g. upload to S3 +} +``` + +### Using `package.json` scripts + +Additionally, we can use an npm script, which will let us use some command line interfaces/executables to perform some action, in this case, deploying our assets directory! + +In this example, I'll use the `aws-cli` and `s3` to sync the `public` folder (containing all our assets) to the `s3` bucket. + +```json:title=package.json +{ + "scripts": { + "build": "gatsby build --prefix-paths", + "postbuild": "aws s3 sync public s3://mybucket" + } +} +``` + +Now whenever the `build` script is invoked, e.g. `npm run build`, the `postbuild` script will be invoked _after_ the build completes, therefore making our assets available on a _separate_ domain after we have finished building out our application with prefixed assets. + +## Additional Considerations + +### Usage with `pathPrefix` + +The [`pathPrefix`](/docs/path-prefix/) feature can be thought of as semi-related to this feature. That feature allows _all_ your website content to be prefixed with some constant prefix, for example you may want your blog to be hosted from `/blog` rather than the project root. + +This feature works seamlessly with `pathPrefix`. Build out your application with the `--prefix-paths` flag and you'll be well on your way to hosting an application with its assets hosted on a CDN, and its core functionality available behind a path prefix. + +### Usage with `gatsby-plugin-offline` + +When using a custom asset prefix with `gatsby-plugin-offline`, your assets can still be cached offline. However, to ensure the plugin works correctly, there are a few things you need to do. + +1. Your asset server needs to have the following headers set: + + ``` + Access-Control-Allow-Origin: + Access-Control-Allow-Credentials: true + ``` + + Note that the origin needs to be specific, rather than using `*` to allow all origins. This is because Gatsby makes requests to fetch resources with `withCredentials` set to `true`, which disallows using `*` to match all origins. This is also why the second header is required. For local testing, use `http://localhost:9000` as the origin. + +2. Certain essential resources need to be available on your content server (i.e. the one used to serve pages). This includes `sw.js`, as well as resources to precache: the Webpack bundle, the app bundle, the manifest (and any icons referenced), and the resources for the offline plugin app shell. + + You can find most of these by looking for the `self.__precacheManifest` variable in your generated `sw.js`. Remember to also include `sw.js` itself, and any icons referenced in your `manifest.webmanifest` if you have one. To check your service worker is functioning as expected, look in Application → Service Workers in your browser dev tools, and check for any failed resources in the Console/Network tabs. diff --git a/docs/docs/gatsby-config.md b/docs/docs/gatsby-config.md index 22a81b6efb7b3..8aa8ffb0ce5f0 100644 --- a/docs/docs/gatsby-config.md +++ b/docs/docs/gatsby-config.md @@ -63,7 +63,6 @@ It's common for sites to be hosted somewhere other than the root of their domain ```javascript:title=gatsby-config.js module.exports = { - // Note: it must *not* have a trailing slash. pathPrefix: `/blog`, } ``` diff --git a/docs/docs/path-prefix.md b/docs/docs/path-prefix.md index ec76eb990e8f4..89a2893f43313 100644 --- a/docs/docs/path-prefix.md +++ b/docs/docs/path-prefix.md @@ -2,46 +2,82 @@ title: Adding a Path Prefix --- -Many sites are hosted at something other than the root of their domain. +Many applications are hosted at something other than the root (`/`) of their domain. -E.g. a Gatsby blog could live at `example.com/blog/` or a site could be hosted -on GitHub Pages at `example.github.io/my-gatsby-site/` +For example, a Gatsby blog could live at `example.com/blog/` or a site could be hosted on GitHub Pages at `example.github.io/my-gatsby-site/` Each of these sites need a prefix added to all paths on the site. So a link to `/my-sweet-blog-post/` should be rewritten to `/blog/my-sweet-blog-post`. -In addition links to various resources (JavaScript, images, CSS) need the same -prefix added (this is accomplished by setting the `publicPath` in webpack). +In addition links to various resources (JavaScript, CSS, images, and other static content) need the same prefix, so that the site continues to function and display correctly, even if served from this path prefix. -Luckily, for most sites, this work can be offloaded to Gatsby. Using -[Gatsby's Link component](/docs/gatsby-link/) for internal links ensures those links -will be prefixed correctly. Gatsby ensures that paths created internally and by -webpack are also correctly prefixed. +Let's get this functionality implemented. We'll add an option to our `gatsby-config.js`, and add a flag to our build command. -## Development +### Add to `gatsby-config.js` -During development, write paths as if there was no path prefix e.g. for a blog -hosted at `example.com/blog`, don't add `/blog` to your links. The prefix will -be added when you build for deployment. - -## Production build - -There are two steps for building a site with path prefixes. - -First define the prefix in your site's `gatsby-config.js`. - -```javascript:title=gatsby-config.js +```js:title=gatsby-config.js module.exports = { - // Note: it must *not* have a trailing slash. pathPrefix: `/blog`, } ``` -Then pass `--prefix-paths` cmd option to Gatsby. +### Build + +Once the `pathPrefix` is specified in `gastby-config.js`, we are well on our way to a prefixed app. The final step is to build out your application with a flag `--prefix-paths`, like so: ```shell gatsby build --prefix-paths ``` -NOTE: When running the command without the `--prefix-paths` flag, Gatsby ignores -your `pathPrefix`. +If this flag is not passed, Gatsby will ignore your `pathPrefix` and build out your site as if it were hosted from the root domain. + +### In-app linking + +As a developer using this feature, it should be seamless. We provide APIs and libraries to make using this functionality a breeze. Specifically, the [`Link`](/docs/gatsby-link/) component has built-in functionality to handle path prefixing. + +For example, if we want to link to our `/page-2` link (but the actual link will be prefixed, e.g. `/blog/page-2`) we don't want to hard code this path prefix in all of our links. We have your back! By using the `Link` component, we will automatically prefix your paths for you. If you later migrate off of `pathPrefix` your links will _still_ work seamlessly. + +Let's look at a quick example. + +```jsx:title=src/pages/index.js +import React from "react" +import { Link } from "gatsby" +import Layout from "../components/layout" + +function Index() { + return ( + + {/* highlight-next-line */} + Page 2 + + ) +} +``` + +Without doing _anything_ and merely using the `Link` component, this link will be prefixed with our specified `pathPrefix` in `gatsby-config.js`. Woo hoo! + +If we want to do programatic/dynamic navigation, totally possible too! We expose a `navigate` helper, and this too automatically handles path prefixing. + +```jsx:title=src/pages/index.js +import React from "react" +import { navigate } from "gatsby" +import Layout from "../components/layout" + +export default function Index() { + return ( + + {/* Note: this is an intentionally contrived example, but you get the idea! */} + {/* highlight-next-line */} + + + ) +} +``` + +### Additional Considerations + +The [`assetPrefix`](/docs/asset-prefix/) feature can be thought of as semi-related to this feature. That feature allows your assets (non-HTML files, e.g. images, JavaScript, etc.) to be hosted on a separate domain, for example a CDN. + +This feature works seamlessly with `assetPrefix`. Build out your application with the `--prefix-paths` flag and you'll be well on your way to hosting an application with its assets hosted on a CDN, and its core functionality available behind a path prefix. diff --git a/e2e-tests/development-runtime/cypress/integration/navigation/linking.js b/e2e-tests/development-runtime/cypress/integration/navigation/linking.js index b2f511ea4a2ee..8cf9c5fb0056d 100644 --- a/e2e-tests/development-runtime/cypress/integration/navigation/linking.js +++ b/e2e-tests/development-runtime/cypress/integration/navigation/linking.js @@ -25,6 +25,16 @@ describe(`navigation`, () => { cy.location(`pathname`).should(`equal`, `/`) }) + it(`can navigate back using history`, () => { + cy.getTestElement(`page-two`) + .click() + .waitForRouteChange() + + cy.go(`back`).waitForRouteChange() + + cy.location(`pathname`).should(`equal`, `/`) + }) + describe(`non-existant route`, () => { beforeEach(() => { cy.getTestElement(`broken-link`) diff --git a/e2e-tests/gatsby-image/gatsby-config.js b/e2e-tests/gatsby-image/gatsby-config.js index bbee39b3be5a0..a3d119648e60e 100644 --- a/e2e-tests/gatsby-image/gatsby-config.js +++ b/e2e-tests/gatsby-image/gatsby-config.js @@ -1,7 +1,6 @@ const path = require(`path`) module.exports = { - pathPrefix: `/blog`, siteMetadata: { title: `Gatsby Image e2e`, }, diff --git a/e2e-tests/path-prefix/.gitignore b/e2e-tests/path-prefix/.gitignore index 615f726febf63..3d1c1613bdf9d 100644 --- a/e2e-tests/path-prefix/.gitignore +++ b/e2e-tests/path-prefix/.gitignore @@ -3,9 +3,10 @@ node_modules yarn-error.log -# Build directory +# Build assets /public .DS_Store +/assets # Cypress output cypress/videos/ diff --git a/e2e-tests/path-prefix/cypress/integration/asset-prefix.js b/e2e-tests/path-prefix/cypress/integration/asset-prefix.js new file mode 100644 index 0000000000000..5272e165a47d7 --- /dev/null +++ b/e2e-tests/path-prefix/cypress/integration/asset-prefix.js @@ -0,0 +1,52 @@ +const { assetPrefix } = require(`../../gatsby-config`) + +const assetPrefixExpression = new RegExp(`^${assetPrefix}`) + +const assetPrefixMatcher = (chain, attr = `href`) => + chain.should(`have.attr`, attr).and(`matches`, assetPrefixExpression) + +describe(`assetPrefix`, () => { + beforeEach(() => { + cy.visit(`/`).waitForRouteChange() + }) + + describe(`runtime`, () => { + it(`prefixes preloads`, () => { + assetPrefixMatcher(cy.get(`head link[rel="preload"]`)) + }) + + it(`prefixes styles`, () => { + assetPrefixMatcher(cy.get(`head style[data-href]`), `data-href`) + }) + + it(`prefixes scripts`, () => { + assetPrefixMatcher(cy.get(`body script[src]`), `src`) + }) + }) + + describe(`gatsby-plugin-manifest`, () => { + it(`prefixes manifest`, () => { + assetPrefixMatcher(cy.get(`head link[rel="manifest"]`)) + }) + + it(`prefixes shortcut icon`, () => { + assetPrefixMatcher(cy.get(`head link[rel="shortcut icon"]`)) + }) + + it(`prefixes manifest icons`, () => { + assetPrefixMatcher(cy.get(`head link[rel="apple-touch-icon"]`)) + }) + }) + + describe(`gatsby-plugin-sitemap`, () => { + it(`prefixes sitemap`, () => { + assetPrefixMatcher(cy.get(`head link[rel="sitemap"]`)) + }) + }) + + describe(`gatsby-plugin-feed`, () => { + it(`prefixes RSS feed`, () => { + assetPrefixMatcher(cy.get(`head link[type="application/rss+xml"]`)) + }) + }) +}) diff --git a/e2e-tests/path-prefix/cypress/integration/path-prefix.js b/e2e-tests/path-prefix/cypress/integration/path-prefix.js index 4dfefc60d398d..00fb257b158da 100644 --- a/e2e-tests/path-prefix/cypress/integration/path-prefix.js +++ b/e2e-tests/path-prefix/cypress/integration/path-prefix.js @@ -39,8 +39,8 @@ describe(`Production pathPrefix`, () => { cy.getTestElement(`page-2-link`) .click() .waitForRouteChange() - - cy.go(`back`).waitForRouteChange() + .go(`back`) + .waitForRouteChange() cy.location(`pathname`).should(`eq`, withTrailingSlash(pathPrefix)) }) diff --git a/e2e-tests/path-prefix/cypress/plugins/index.js b/e2e-tests/path-prefix/cypress/plugins/index.js new file mode 100644 index 0000000000000..fd170fba6912b --- /dev/null +++ b/e2e-tests/path-prefix/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/path-prefix/gatsby-config.js b/e2e-tests/path-prefix/gatsby-config.js index e80b503e2601f..d97cbc4d4ee57 100644 --- a/e2e-tests/path-prefix/gatsby-config.js +++ b/e2e-tests/path-prefix/gatsby-config.js @@ -1,6 +1,10 @@ +const pathPrefix = `/blog` + module.exports = { + assetPrefix: `http://localhost:9001`, pathPrefix: `/blog`, siteMetadata: { + siteUrl: `http://localhost:9000`, title: `Gatsby Default Starter`, }, plugins: [ @@ -17,5 +21,45 @@ module.exports = { icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site. }, }, + `gatsby-plugin-sitemap`, + { + resolve: `gatsby-plugin-feed`, + options: { + query: ` + { + site { + siteMetadata { + siteUrl + site_url: siteUrl + } + } + } + `, + feeds: [ + { + query: ` + { + pages: allSitePage { + nodes { + path + } + } + } + `, + serialize({ query: { site, pages } }) { + return pages.nodes.map(node => { + return { + description: `A sample page hello world suh dude`, + date: `10-08-1990`, + url: `${site.siteMetadata.siteUrl}${pathPrefix}${node.path}`, + } + }) + }, + title: `assetPrefix + pathPrefix RSS Feed`, + output: `rss.xml`, + }, + ], + }, + }, ], } diff --git a/e2e-tests/path-prefix/package.json b/e2e-tests/path-prefix/package.json index e28122afc515c..7fa4d414afa7a 100644 --- a/e2e-tests/path-prefix/package.json +++ b/e2e-tests/path-prefix/package.json @@ -5,10 +5,12 @@ "author": "Kyle Mathews ", "dependencies": { "cypress": "^3.1.0", - "gatsby": "^2.0.118", + "gatsby": "^2.3.34", + "gatsby-plugin-feed": "^2.1.2", "gatsby-plugin-manifest": "^2.0.17", "gatsby-plugin-offline": "^2.0.23", "gatsby-plugin-react-helmet": "^3.0.6", + "gatsby-plugin-sitemap": "^2.0.12", "react": "^16.8.0", "react-dom": "^16.8.0", "react-helmet": "^5.2.0" @@ -18,18 +20,22 @@ ], "license": "MIT", "scripts": { + "prebuild": "rm -rf assets && mkdir -p assets/blog", "build": "gatsby build --prefix-paths", + "postbuild": "cp -r public/. assets/blog", "develop": "gatsby develop", "format": "prettier --write '**/*.js'", "test": "CYPRESS_SUPPORT=y npm run build && npm run start-server-and-test", - "start-server-and-test": "start-server-and-test serve http://localhost:9000/blog cy:run", - "serve": "gatsby serve --prefix-paths", + "start-server-and-test": "start-server-and-test serve \"http://localhost:9000/blog/|http://localhost:9001/blog/\" cy:run", + "serve": "gatsby serve --prefix-paths & npm run serve:assets", + "serve:assets": "node scripts/serve.js", "cy:open": "cypress open", "cy:run": "cypress run --browser chrome" }, "devDependencies": { "gatsby-cypress": "^0.1.7", "prettier": "^1.14.3", + "serve-handler": "^6.0.0", "start-server-and-test": "^1.7.1" }, "repository": { diff --git a/e2e-tests/path-prefix/scripts/serve.js b/e2e-tests/path-prefix/scripts/serve.js new file mode 100644 index 0000000000000..06b6af1a6982b --- /dev/null +++ b/e2e-tests/path-prefix/scripts/serve.js @@ -0,0 +1,28 @@ +const handler = require(`serve-handler`) +const http = require(`http`) +const path = require(`path`) + +const server = http.createServer((request, response) => + handler(request, response, { + public: path.resolve(`assets`), + headers: [ + { + source: `**/*`, + headers: [ + { + key: `Access-Control-Allow-Origin`, + value: `http://localhost:9000`, + }, + { + key: `Access-Control-Allow-Credentials`, + value: true, + }, + ], + }, + ], + }) +) + +server.listen(9001, () => { + console.log(`Running at http://localhost:9001`) +}) diff --git a/packages/gatsby-link/index.d.ts b/packages/gatsby-link/index.d.ts index f3134e91751ff..88695d7283fa8 100644 --- a/packages/gatsby-link/index.d.ts +++ b/packages/gatsby-link/index.d.ts @@ -37,6 +37,7 @@ export const navigate: NavigateFn * development and production */ export const withPrefix: (path: string) => string +export const withAssetPrefix: (path: string) => string /** * @deprecated diff --git a/packages/gatsby-link/src/__tests__/index.js b/packages/gatsby-link/src/__tests__/index.js index 1de977b8aede8..88793411025da 100644 --- a/packages/gatsby-link/src/__tests__/index.js +++ b/packages/gatsby-link/src/__tests__/index.js @@ -6,10 +6,10 @@ import { createHistory, LocationProvider, } from "@reach/router" -import Link, { navigate, push, replace, withPrefix } from "../" +import Link, { navigate, push, replace, withPrefix, withAssetPrefix } from "../" afterEach(() => { - global.__PATH_PREFIX__ = `` + global.__BASE_PATH__ = `` cleanup() }) @@ -34,12 +34,17 @@ const getReplace = () => { } const getWithPrefix = (pathPrefix = ``) => { - global.__PATH_PREFIX__ = pathPrefix + global.__BASE_PATH__ = pathPrefix return withPrefix } +const getWithAssetPrefix = (prefix = ``) => { + global.__PATH_PREFIX__ = prefix + return withAssetPrefix +} + const setup = ({ sourcePath = `/active`, linkProps, pathPrefix = `` } = {}) => { - global.__PATH_PREFIX__ = pathPrefix + global.__BASE_PATH__ = pathPrefix const source = createMemorySource(sourcePath) const history = createHistory(source) @@ -148,6 +153,28 @@ describe(`withPrefix`, () => { }) }) +describe(`withAssetPrefix`, () => { + it(`default prefix does not return "//"`, () => { + const to = `/` + const root = getWithAssetPrefix()(to) + expect(root).toEqual(to) + }) + + it(`respects pathPrefix`, () => { + const to = `/abc/` + const pathPrefix = `/blog` + const root = getWithAssetPrefix(pathPrefix)(to) + expect(root).toEqual(`${pathPrefix}${to}`) + }) + + it(`respects joined assetPrefix + pathPrefix`, () => { + const to = `/itsdatboi/` + const pathPrefix = `https://cdn.example.com/blog` + const root = getWithAssetPrefix(pathPrefix)(to) + expect(root).toEqual(`${pathPrefix}${to}`) + }) +}) + describe(`navigate`, () => { it(`navigates to correct path`, () => { const to = `/some-path` @@ -158,11 +185,11 @@ describe(`navigate`, () => { it(`respects pathPrefix`, () => { const to = `/some-path` - global.__PATH_PREFIX__ = `/blog` + global.__BASE_PATH__ = `/blog` getNavigate()(to) expect(global.___navigate).toHaveBeenCalledWith( - `${global.__PATH_PREFIX__}${to}`, + `${global.__BASE_PATH__}${to}`, undefined ) }) diff --git a/packages/gatsby-link/src/index.js b/packages/gatsby-link/src/index.js index 7b1e94d014d33..c27af7a39a9e2 100644 --- a/packages/gatsby-link/src/index.js +++ b/packages/gatsby-link/src/index.js @@ -1,4 +1,3 @@ -/*global __PATH_PREFIX__ */ import PropTypes from "prop-types" import React from "react" import { Link } from "@reach/router" @@ -8,7 +7,11 @@ import { parsePath } from "./parse-path" export { parsePath } export function withPrefix(path) { - return normalizePath(`${__PATH_PREFIX__}/${path}`) + return normalizePath(`${__BASE_PATH__}/${path}`) +} + +export function withAssetPrefix(path) { + return [__PATH_PREFIX__].concat([path.replace(/^\//, ``)]).join(`/`) } function normalizePath(path) { diff --git a/packages/gatsby-plugin-catch-links/src/__tests__/catch-links.js b/packages/gatsby-plugin-catch-links/src/__tests__/catch-links.js index f13ebf4f3f6e9..a75d9e9f473e8 100644 --- a/packages/gatsby-plugin-catch-links/src/__tests__/catch-links.js +++ b/packages/gatsby-plugin-catch-links/src/__tests__/catch-links.js @@ -4,7 +4,7 @@ import { navigate } from "gatsby" import * as catchLinks from "../catch-links" beforeAll(() => { - global.__PATH_PREFIX__ = `` + global.__BASE_PATH__ = `` // Set the base URL we will be testing against to http://localhost:8000/blog window.history.pushState({}, `APP Url`, `${pathPrefix}`) }) @@ -365,13 +365,13 @@ describe(`pathPrefix is handled if catched link to ${pathPrefix}/article navigat }) afterAll(() => { - global.__PATH_PREFIX__ = `` + global.__BASE_PATH__ = `` eventDestroyer() }) test(`on sites with pathPrefix '${pathPrefix}'`, done => { // simulate case with --prefix-paths and prefix /blog - global.__PATH_PREFIX__ = pathPrefix + global.__BASE_PATH__ = pathPrefix // create the element with href /blog/article const clickElement = document.createElement(`a`) @@ -408,7 +408,7 @@ describe(`pathPrefix is handled if catched link to ${pathPrefix}/article navigat test(`on sites without pathPrefix`, done => { // simulate default case without --prefix-paths - global.__PATH_PREFIX__ = `` + global.__BASE_PATH__ = `` // create the element with href /blog/article const clickElement = document.createElement(`a`) diff --git a/packages/gatsby-plugin-feed/package.json b/packages/gatsby-plugin-feed/package.json index 9e5b4cb9f96f7..0d55ba2bab876 100644 --- a/packages/gatsby-plugin-feed/package.json +++ b/packages/gatsby-plugin-feed/package.json @@ -29,7 +29,7 @@ "license": "MIT", "main": "index.js", "peerDependencies": { - "gatsby": "^2.0.0" + "gatsby": "^2.4.0" }, "repository": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-feed", "scripts": { diff --git a/packages/gatsby-plugin-feed/src/__tests__/gatsby-ssr.js b/packages/gatsby-plugin-feed/src/__tests__/gatsby-ssr.js index 4512841a7ddfd..995baba9eabae 100644 --- a/packages/gatsby-plugin-feed/src/__tests__/gatsby-ssr.js +++ b/packages/gatsby-plugin-feed/src/__tests__/gatsby-ssr.js @@ -1,14 +1,14 @@ const { onRenderBody } = require(`../gatsby-ssr`) -const defaultPathPrefix = global.__PATH_PREFIX__ - describe(`Adds for feed to head`, () => { + const prefix = global.__BASE_PATH__ beforeEach(() => { + global.__BASE_PATH__ = `` global.__PATH_PREFIX__ = `` }) - afterEach(() => { - global.__PATH_PREFIX__ = defaultPathPrefix + afterAll(() => { + global.__BASE_PATH__ = prefix }) it(`creates Link if feeds does exist`, async () => { diff --git a/packages/gatsby-plugin-feed/src/gatsby-ssr.js b/packages/gatsby-plugin-feed/src/gatsby-ssr.js index 85a0533a013b5..b958df1dc6684 100644 --- a/packages/gatsby-plugin-feed/src/gatsby-ssr.js +++ b/packages/gatsby-plugin-feed/src/gatsby-ssr.js @@ -1,7 +1,10 @@ import React from "react" -import { withPrefix } from "gatsby" +import { withPrefix as fallbackWithPrefix, withAssetPrefix } from "gatsby" import { defaultOptions } from "./internals" +// TODO: remove for v3 +const withPrefix = withAssetPrefix || fallbackWithPrefix + exports.onRenderBody = ({ setHeadComponents }, pluginOptions) => { const { feeds } = { ...defaultOptions, diff --git a/packages/gatsby-plugin-manifest/package.json b/packages/gatsby-plugin-manifest/package.json index afe3e714e7512..9711bf2a5d5bc 100644 --- a/packages/gatsby-plugin-manifest/package.json +++ b/packages/gatsby-plugin-manifest/package.json @@ -29,7 +29,7 @@ "license": "MIT", "main": "index.js", "peerDependencies": { - "gatsby": "^2.0.15" + "gatsby": "^2.4.0" }, "repository": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest", "scripts": { diff --git a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js index 6738e50e3593d..056c9cad504fc 100644 --- a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js +++ b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js @@ -19,6 +19,7 @@ const ssrArgs = { describe(`gatsby-plugin-manifest`, () => { beforeEach(() => { + global.__BASE_PATH__ = `` global.__PATH_PREFIX__ = `` headComponents = [] }) diff --git a/packages/gatsby-plugin-manifest/src/gatsby-ssr.js b/packages/gatsby-plugin-manifest/src/gatsby-ssr.js index c4abba42c8720..fb2625893571f 100644 --- a/packages/gatsby-plugin-manifest/src/gatsby-ssr.js +++ b/packages/gatsby-plugin-manifest/src/gatsby-ssr.js @@ -1,8 +1,12 @@ import React from "react" -import { withPrefix } from "gatsby" +import { withPrefix as fallbackWithPrefix, withAssetPrefix } from "gatsby" +import fs from "fs" import createContentDigest from "gatsby/dist/utils/create-content-digest" + import { defaultIcons, addDigestToPath } from "./common.js" -import fs from "fs" + +// TODO: remove for v3 +const withPrefix = withAssetPrefix || fallbackWithPrefix let iconDigest = null @@ -50,7 +54,7 @@ exports.onRenderBody = ({ setHeadComponents }, pluginOptions) => { ) diff --git a/packages/gatsby-plugin-offline/README.md b/packages/gatsby-plugin-offline/README.md index 0daac0c7bf3ac..6eccec542cfb6 100644 --- a/packages/gatsby-plugin-offline/README.md +++ b/packages/gatsby-plugin-offline/README.md @@ -89,6 +89,8 @@ outdated version registered in users' browsers. ## Notes +### Empty View Source and SEO + Gatsby offers great SEO capabilities and that is no different with `gatsby-plugin-offline`. However, you shouldn't think that Gatsby doesn't serve HTML tags anymore when looking at your source code in the browser (with `Right click` => `View source`). `View source` doesn't represent the actual HTML data since `gatsby-plugin-offline` registers and loads a service worker that will cache and handle this differently. Your site is loaded from the service worker, not from its actual source (check your `Network` tab in the DevTools for that). To see the HTML data that crawlers will receive, run this in your terminal: diff --git a/packages/gatsby-plugin-sitemap/src/__tests__/gatsby-node.js b/packages/gatsby-plugin-sitemap/src/__tests__/gatsby-node.js index 4afe8566cfd9b..18202c953aea0 100644 --- a/packages/gatsby-plugin-sitemap/src/__tests__/gatsby-node.js +++ b/packages/gatsby-plugin-sitemap/src/__tests__/gatsby-node.js @@ -2,10 +2,15 @@ jest.mock(`fs`) const fs = require(`fs`) const path = require(`path`) +const sitemap = require(`sitemap`) + const { onPostBuild } = require(`../gatsby-node`) const internals = require(`../internals`) const pathPrefix = `` -const sitemap = require(`sitemap`) + +beforeEach(() => { + global.__PATH_PREFIX__ = `` +}) describe(`Test plugin sitemap`, async () => { it(`default settings work properly`, async () => { diff --git a/packages/gatsby-plugin-sitemap/src/__tests__/internals.js b/packages/gatsby-plugin-sitemap/src/__tests__/internals.js index f1a3dd7004062..4db76e15f7d70 100644 --- a/packages/gatsby-plugin-sitemap/src/__tests__/internals.js +++ b/packages/gatsby-plugin-sitemap/src/__tests__/internals.js @@ -3,6 +3,10 @@ const { defaultOptions: { serialize }, } = require(`../internals`) +beforeEach(() => { + global.__PATH_PREFIX__ = `` +}) + describe(`results using default settings`, () => { const generateQueryResultsMock = ( { siteUrl } = { siteUrl: `http://dummy.url` } @@ -37,6 +41,10 @@ describe(`results using default settings`, () => { } const runTests = (pathPrefix = ``) => { + beforeEach(() => { + global.__PATH_PREFIX__ = pathPrefix + }) + it(`prepares all urls correctly`, async () => { const graphql = () => Promise.resolve(generateQueryResultsMock()) const queryRecords = await runQuery(graphql, ``, [], pathPrefix) diff --git a/packages/gatsby-plugin-sitemap/src/gatsby-node.js b/packages/gatsby-plugin-sitemap/src/gatsby-node.js index 9cdc218bb4a8d..a68064eb67ac1 100644 --- a/packages/gatsby-plugin-sitemap/src/gatsby-node.js +++ b/packages/gatsby-plugin-sitemap/src/gatsby-node.js @@ -10,7 +10,10 @@ import { const publicPath = `./public` -exports.onPostBuild = async ({ graphql, pathPrefix }, pluginOptions) => { +exports.onPostBuild = async ( + { graphql, pathPrefix, basePath = pathPrefix }, + pluginOptions +) => { const options = { ...pluginOptions } delete options.plugins delete options.createLinkInHead @@ -25,12 +28,7 @@ exports.onPostBuild = async ({ graphql, pathPrefix }, pluginOptions) => { // Paths we're excluding... const excludeOptions = exclude.concat(defaultOptions.exclude) - const queryRecords = await runQuery( - graphql, - query, - excludeOptions, - pathPrefix - ) + const queryRecords = await runQuery(graphql, query, excludeOptions, basePath) const urls = serialize(queryRecords) if (!rest.sitemapSize || urls.length <= rest.sitemapSize) { diff --git a/packages/gatsby-plugin-sitemap/src/gatsby-ssr.js b/packages/gatsby-plugin-sitemap/src/gatsby-ssr.js index 818242000ea33..e4e5c1b3c0614 100644 --- a/packages/gatsby-plugin-sitemap/src/gatsby-ssr.js +++ b/packages/gatsby-plugin-sitemap/src/gatsby-ssr.js @@ -1,7 +1,10 @@ import React from "react" -import { withPrefix } from "gatsby" +import { withPrefix as fallbackWithPrefix, withAssetPrefix } from "gatsby" import { defaultOptions } from "./internals" +// TODO: remove for v3 +const withPrefix = withAssetPrefix || fallbackWithPrefix + exports.onRenderBody = ({ setHeadComponents }, pluginOptions) => { let { output, createLinkInHead } = { ...defaultOptions, ...pluginOptions } diff --git a/packages/gatsby-plugin-sitemap/src/internals.js b/packages/gatsby-plugin-sitemap/src/internals.js index 72b9b75ed336c..58b8861480c7b 100644 --- a/packages/gatsby-plugin-sitemap/src/internals.js +++ b/packages/gatsby-plugin-sitemap/src/internals.js @@ -24,7 +24,6 @@ export const runQuery = (handler, query, excludes, pathPrefix) => // Add path prefix r.data.allSitePage.edges = r.data.allSitePage.edges.map(page => { - // uses `normalizePath` logic from `gatsby-link` page.node.path = (pathPrefix + page.node.path).replace(/^\/\//g, `/`) return page }) diff --git a/packages/gatsby/cache-dir/__tests__/static-entry.js b/packages/gatsby/cache-dir/__tests__/static-entry.js index a5fe877d1e5c0..8cb6487dd496b 100644 --- a/packages/gatsby/cache-dir/__tests__/static-entry.js +++ b/packages/gatsby/cache-dir/__tests__/static-entry.js @@ -184,6 +184,7 @@ describe(`develop-static-entry`, () => { describe(`static-entry sanity checks`, () => { beforeEach(() => { global.__PATH_PREFIX__ = `` + global.__BASE_PATH__ = `` }) const methodsToCheck = [ @@ -237,6 +238,7 @@ describe(`static-entry sanity checks`, () => { describe(`static-entry`, () => { beforeEach(() => { global.__PATH_PREFIX__ = `` + global.__BASE_PATH__ = `` }) test(`onPreRenderHTML can be used to replace headComponents`, done => { diff --git a/packages/gatsby/cache-dir/gatsby-browser-entry.js b/packages/gatsby/cache-dir/gatsby-browser-entry.js index b569481e9789c..b803c960d8f9c 100644 --- a/packages/gatsby/cache-dir/gatsby-browser-entry.js +++ b/packages/gatsby/cache-dir/gatsby-browser-entry.js @@ -2,6 +2,7 @@ import React from "react" import PropTypes from "prop-types" import Link, { withPrefix, + withAssetPrefix, navigate, push, replace, @@ -69,6 +70,7 @@ function graphql() { export { Link, + withAssetPrefix, withPrefix, graphql, parsePath, diff --git a/packages/gatsby/cache-dir/loader.js b/packages/gatsby/cache-dir/loader.js index 0d77a65805825..5a9dec232489e 100644 --- a/packages/gatsby/cache-dir/loader.js +++ b/packages/gatsby/cache-dir/loader.js @@ -203,7 +203,7 @@ let disableCorePrefetching = false const queue = { addPagesArray: newPages => { - findPage = pageFinderFactory(newPages, __PATH_PREFIX__) + findPage = pageFinderFactory(newPages, __BASE_PATH__) }, addDevRequires: devRequires => { syncRequires = devRequires diff --git a/packages/gatsby/cache-dir/production-app.js b/packages/gatsby/cache-dir/production-app.js index c3067b678e9d5..90d8f871289cc 100644 --- a/packages/gatsby/cache-dir/production-app.js +++ b/packages/gatsby/cache-dir/production-app.js @@ -66,10 +66,10 @@ apiRunnerAsync(`onClientEntry`).then(() => { // Make sure the window.page object is defined page && // The canonical path doesn't match the actual path (i.e. the address bar) - __PATH_PREFIX__ + page.path !== browserLoc.pathname && + __BASE_PATH__ + page.path !== browserLoc.pathname && // ...and if matchPage is specified, it also doesn't match the actual path (!page.matchPath || - !match(__PATH_PREFIX__ + page.matchPath, browserLoc.pathname)) && + !match(__BASE_PATH__ + page.matchPath, browserLoc.pathname)) && // Ignore 404 pages, since we want to keep the same URL page.path !== `/404.html` && !page.path.match(/^\/404\/?$/) && @@ -77,10 +77,9 @@ apiRunnerAsync(`onClientEntry`).then(() => { // pages have this canonical path) !page.path.match(/^\/offline-plugin-app-shell-fallback\/?$/) ) { - navigate( - __PATH_PREFIX__ + page.path + browserLoc.search + browserLoc.hash, - { replace: true } - ) + navigate(__BASE_PATH__ + page.path + browserLoc.search + browserLoc.hash, { + replace: true, + }) } loader.getResourcesForPathname(browserLoc.pathname).then(() => { @@ -88,7 +87,7 @@ apiRunnerAsync(`onClientEntry`).then(() => { createElement( Router, { - basepath: __PATH_PREFIX__, + basepath: __BASE_PATH__, }, createElement(RouteHandler, { path: `/*` }) ) diff --git a/packages/gatsby/cache-dir/register-service-worker.js b/packages/gatsby/cache-dir/register-service-worker.js index acb6347b450b0..52eb37a8475ae 100644 --- a/packages/gatsby/cache-dir/register-service-worker.js +++ b/packages/gatsby/cache-dir/register-service-worker.js @@ -9,7 +9,7 @@ if ( ) } else if (`serviceWorker` in navigator) { navigator.serviceWorker - .register(`${__PATH_PREFIX__}/sw.js`) + .register(`${__BASE_PATH__}/sw.js`) .then(function(reg) { reg.addEventListener(`updatefound`, () => { apiRunner(`onServiceWorkerUpdateFound`, { serviceWorker: reg }) diff --git a/packages/gatsby/cache-dir/root.js b/packages/gatsby/cache-dir/root.js index f6d73b8439d92..c19a134f30bcb 100644 --- a/packages/gatsby/cache-dir/root.js +++ b/packages/gatsby/cache-dir/root.js @@ -87,7 +87,7 @@ const Root = () => createElement( Router, { - basepath: __PATH_PREFIX__, + basepath: __BASE_PATH__, }, createElement(RouteHandler, { path: `/*` }) ) diff --git a/packages/gatsby/cache-dir/static-entry.js b/packages/gatsby/cache-dir/static-entry.js index aa3b9cfb95daa..dfb4e8f94a352 100644 --- a/packages/gatsby/cache-dir/static-entry.js +++ b/packages/gatsby/cache-dir/static-entry.js @@ -167,11 +167,11 @@ export default (pagePath, callback) => { const routerElement = createElement( ServerLocation, - { url: `${__PATH_PREFIX__}${pagePath}` }, + { url: `${__BASE_PATH__}${pagePath}` }, createElement( Router, { - baseuri: `${__PATH_PREFIX__}`, + baseuri: `${__BASE_PATH__}`, }, createElement(RouteHandler, { path: `/*` }) ) diff --git a/packages/gatsby/index.d.ts b/packages/gatsby/index.d.ts index 0cd4b2958713f..9d7d8f45a59e1 100644 --- a/packages/gatsby/index.d.ts +++ b/packages/gatsby/index.d.ts @@ -10,6 +10,7 @@ export { push, replace, withPrefix, + withAssetPrefix, } from "gatsby-link" export interface StaticQueryProps { diff --git a/packages/gatsby/src/commands/serve.js b/packages/gatsby/src/commands/serve.js index f0ea63f574728..06910500870c6 100644 --- a/packages/gatsby/src/commands/serve.js +++ b/packages/gatsby/src/commands/serve.js @@ -4,16 +4,17 @@ const openurl = require(`better-opn`) const fs = require(`fs-extra`) const compression = require(`compression`) const express = require(`express`) -const getConfigFile = require(`../bootstrap/get-config-file`) -const preferDefault = require(`../bootstrap/prefer-default`) const chalk = require(`chalk`) const { match: reachMatch } = require(`@reach/router/lib/utils`) -const detectPortInUseAndPrompt = require(`../utils/detect-port-in-use-and-prompt`) const rl = require(`readline`) const onExit = require(`signal-exit`) const telemetry = require(`gatsby-telemetry`) +const detectPortInUseAndPrompt = require(`../utils/detect-port-in-use-and-prompt`) +const getConfigFile = require(`../bootstrap/get-config-file`) +const preferDefault = require(`../bootstrap/prefer-default`) + const rlInterface = rl.createInterface({ input: process.stdin, output: process.stdout, @@ -68,8 +69,9 @@ module.exports = async program => { getConfigFile(program.directory, `gatsby-config`) ) - let pathPrefix = config && config.pathPrefix - pathPrefix = prefixPaths && pathPrefix ? pathPrefix : `/` + const { pathPrefix: configPathPrefix } = config + + const pathPrefix = prefixPaths && configPathPrefix ? configPathPrefix : `/` const root = path.join(program.directory, `public`) const pages = await getPages(program.directory) @@ -88,6 +90,15 @@ module.exports = async program => { } return next() }) + app.use(function(req, res, next) { + res.header(`Access-Control-Allow-Origin`, `http://${host}:${port}`) + res.header(`Access-Control-Allow-Credentials`, true) + res.header( + `Access-Control-Allow-Headers`, + `Origin, X-Requested-With, Content-Type, Accept` + ) + next() + }) app.use(pathPrefix, router) const startListening = () => { diff --git a/packages/gatsby/src/joi-schemas/__tests__/__snapshots__/joi.js.snap b/packages/gatsby/src/joi-schemas/__tests__/__snapshots__/joi.js.snap new file mode 100644 index 0000000000000..1be4b6fb32fbe --- /dev/null +++ b/packages/gatsby/src/joi-schemas/__tests__/__snapshots__/joi.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`gatsby config does not allow pathPrefix to be full URL 1`] = `"child \\"pathPrefix\\" fails because [\\"pathPrefix\\" must be a valid relative uri]"`; + +exports[`gatsby config throws when relative path used for both assetPrefix and pathPrefix 1`] = `"assetPrefix must be an absolute URI when used with pathPrefix"`; diff --git a/packages/gatsby/src/joi-schemas/__tests__/joi.js b/packages/gatsby/src/joi-schemas/__tests__/joi.js new file mode 100644 index 0000000000000..630276791cea5 --- /dev/null +++ b/packages/gatsby/src/joi-schemas/__tests__/joi.js @@ -0,0 +1,61 @@ +const { gatsbyConfigSchema } = require(`../joi`) + +describe(`gatsby config`, () => { + it(`strips trailing slashes from url fields`, () => { + const config = { + pathPrefix: `/blog///`, + assetPrefix: `https://cdn.example.com/`, + } + + expect(gatsbyConfigSchema.validate(config)).resolves.toEqual({ + pathPrefix: `/blog`, + assetPrefix: `https://cdn.example.com`, + }) + }) + + it(`allows assetPrefix to be full URL`, () => { + const config = { + assetPrefix: `https://cdn.example.com`, + } + + expect(gatsbyConfigSchema.validate(config)).resolves.toEqual(config) + }) + + it(`allows assetPrefix to be a URL with nested paths`, () => { + const config = { + assetPrefix: `https://cdn.example.com/some/nested/path`, + } + + expect(gatsbyConfigSchema.validate(config)).resolves.toEqual(config) + }) + + it(`allows relative paths for url fields`, () => { + const config = { + pathPrefix: `/blog`, + assetPrefix: `https://cdn.example.com`, + } + + expect(gatsbyConfigSchema.validate(config)).resolves.toEqual(config) + }) + + it(`does not allow pathPrefix to be full URL`, () => { + const config = { + pathPrefix: `https://google.com`, + } + + expect( + gatsbyConfigSchema.validate(config) + ).rejects.toThrowErrorMatchingSnapshot() + }) + + it(`throws when relative path used for both assetPrefix and pathPrefix`, () => { + const config = { + assetPrefix: `/assets`, + pathPrefix: `/blog`, + } + + expect( + gatsbyConfigSchema.validate(config) + ).rejects.toThrowErrorMatchingSnapshot() + }) +}) diff --git a/packages/gatsby/src/joi-schemas/joi.js b/packages/gatsby/src/joi-schemas/joi.js index 8a00a48ad33b2..d7e832e89a0d6 100644 --- a/packages/gatsby/src/joi-schemas/joi.js +++ b/packages/gatsby/src/joi-schemas/joi.js @@ -2,24 +2,58 @@ const Joi = require(`joi`) const stripTrailingSlash = chain => chain.replace(/(\w)\/+$/, `$1`) -export const gatsbyConfigSchema = Joi.object().keys({ - __experimentalThemes: Joi.array(), - polyfill: Joi.boolean(), - siteMetadata: Joi.object({ - siteUrl: stripTrailingSlash(Joi.string()).uri(), - }).unknown(), - pathPrefix: Joi.string().uri({ - allowRelative: true, - relativeOnly: true, - }), - mapping: Joi.object(), - plugins: Joi.array(), - proxy: Joi.object().keys({ - prefix: Joi.string().required(), - url: Joi.string().required(), - }), - developMiddleware: Joi.func(), -}) +export const gatsbyConfigSchema = Joi.object() + .keys({ + __experimentalThemes: Joi.array(), + polyfill: Joi.boolean(), + assetPrefix: stripTrailingSlash( + Joi.string().uri({ + allowRelative: true, + }) + ), + pathPrefix: stripTrailingSlash( + Joi.string().uri({ + allowRelative: true, + relativeOnly: true, + }) + ), + siteMetadata: Joi.object({ + siteUrl: stripTrailingSlash(Joi.string()).uri(), + }).unknown(), + mapping: Joi.object(), + plugins: Joi.array(), + proxy: Joi.object().keys({ + prefix: Joi.string().required(), + url: Joi.string().required(), + }), + developMiddleware: Joi.func(), + }) + // throws when both assetPrefix and pathPrefix are defined + .when( + Joi.object({ + assetPrefix: Joi.string().uri({ + allowRelative: true, + relativeOnly: true, + }), + pathPrefix: Joi.string().uri({ + allowRelative: true, + relativeOnly: true, + }), + }), + { + then: Joi.object({ + assetPrefix: Joi.string() + .uri({ + allowRelative: false, + }) + .error( + new Error( + `assetPrefix must be an absolute URI when used with pathPrefix` + ) + ), + }), + } + ) export const pageSchema = Joi.object() .keys({ diff --git a/packages/gatsby/src/utils/__tests__/get-public-path.js b/packages/gatsby/src/utils/__tests__/get-public-path.js new file mode 100644 index 0000000000000..6eb60aaaf0040 --- /dev/null +++ b/packages/gatsby/src/utils/__tests__/get-public-path.js @@ -0,0 +1,81 @@ +const getPublicPath = require(`../get-public-path`) + +const assetPrefix = `https://cdn.example.com` +const pathPrefix = `/blog` + +describe(`basic functionality`, () => { + it(`returns assetPrefix`, () => { + expect( + getPublicPath({ + assetPrefix, + prefixPaths: true, + }) + ).toBe(assetPrefix) + }) + + it(`returns pathPrefix`, () => { + expect( + getPublicPath({ + pathPrefix, + prefixPaths: true, + }) + ).toBe(`/blog`) + }) + + it(`joins assetPrefix and pathPrefix`, () => { + expect( + getPublicPath({ + pathPrefix, + assetPrefix, + prefixPaths: true, + }) + ).toBe(`${assetPrefix}${pathPrefix}`) + }) + + describe(`assetPrefix variations`, () => { + it(`handles relative assetPrefix`, () => { + const localAssetPrefix = `/assets` + expect( + getPublicPath({ + pathPrefix, + assetPrefix: localAssetPrefix, + prefixPaths: true, + }) + ).toBe(`${localAssetPrefix}${pathPrefix}`) + }) + + it(`handles URL assetPrefix, e.g. a CDN`, () => { + const cdn = `https://cdn.example.org` + expect( + getPublicPath({ + pathPrefix, + assetPrefix: cdn, + prefixPaths: true, + }) + ).toBe(`${cdn}${pathPrefix}`) + }) + + it(`handles double slashes`, () => { + const cdn = `//cdn.example.org` + expect( + getPublicPath({ + pathPrefix, + assetPrefix: cdn, + prefixPaths: true, + }) + ).toBe(`${cdn}${pathPrefix}`) + }) + + it(`handles trailing slashes`, () => { + ;[`/assets/`, `https://cdn.example.org/`].forEach(prefix => { + expect( + getPublicPath({ + pathPrefix, + assetPrefix: prefix, + prefixPaths: true, + }) + ).toBe(`${prefix.slice(0, -1)}${pathPrefix}`) + }) + }) + }) +}) diff --git a/packages/gatsby/src/utils/api-runner-node.js b/packages/gatsby/src/utils/api-runner-node.js index 44f0b9611848f..527ff7b28716a 100644 --- a/packages/gatsby/src/utils/api-runner-node.js +++ b/packages/gatsby/src/utils/api-runner-node.js @@ -15,6 +15,7 @@ const { buildInputObjectType, } = require(`../schema/types/type-builders`) const { emitter } = require(`../redux`) +const getPublicPath = require(`./get-public-path`) const { getNonGatsbyCodeFrame } = require(`./stack-trace-utils`) const { trackBuildError, decorateEvent } = require(`gatsby-telemetry`) @@ -74,7 +75,6 @@ const runAPI = (plugin, api, args) => { pluginSpan.setTag(`api`, api) pluginSpan.setTag(`plugin`, plugin.name) - let pathPrefix = `` const { store, emitter } = require(`../redux`) const { loadNodeContent, @@ -93,9 +93,10 @@ const runAPI = (plugin, api, args) => { { ...args, parentSpan: pluginSpan } ) - if (store.getState().program.prefixPaths) { - pathPrefix = store.getState().config.pathPrefix - } + const { config, program } = store.getState() + + const pathPrefix = (program.prefixPaths && config.pathPrefix) || `` + const publicPath = getPublicPath({ ...config, ...program }, ``) const namespacedCreateNodeId = id => createNodeId(id, plugin.name) @@ -152,7 +153,8 @@ const runAPI = (plugin, api, args) => { const apiCallArgs = [ { ...args, - pathPrefix, + basePath: pathPrefix, + pathPrefix: publicPath, boundActionCreators: actions, actions, loadNodeContent, diff --git a/packages/gatsby/src/utils/eslint-config.js b/packages/gatsby/src/utils/eslint-config.js index 0f5136124a040..f48e16b454b09 100644 --- a/packages/gatsby/src/utils/eslint-config.js +++ b/packages/gatsby/src/utils/eslint-config.js @@ -7,6 +7,7 @@ module.exports = schema => { globals: { graphql: true, __PATH_PREFIX__: true, + __BASE_PATH__: true, // this will rarely, if ever, be used by consumers }, extends: `react-app`, plugins: [`graphql`], diff --git a/packages/gatsby/src/utils/get-public-path.js b/packages/gatsby/src/utils/get-public-path.js new file mode 100644 index 0000000000000..035e4bc35958f --- /dev/null +++ b/packages/gatsby/src/utils/get-public-path.js @@ -0,0 +1,21 @@ +const trimSlashes = part => part.replace(/(^\/)|(\/$)/g, ``) + +const isURL = possibleUrl => + [`http://`, `https://`, `//`].some(expr => possibleUrl.startsWith(expr)) + +module.exports = function getPublicPath({ + assetPrefix, + pathPrefix, + prefixPaths, +}) { + if (prefixPaths && (assetPrefix || pathPrefix)) { + const normalized = [assetPrefix, pathPrefix] + .filter(part => part && part.length > 0) + .map(part => trimSlashes(part)) + .join(`/`) + + return isURL(normalized) ? normalized : `/${normalized}` + } + + return `` +} diff --git a/packages/gatsby/src/utils/path.js b/packages/gatsby/src/utils/path.js index d2c9645afddc5..98616148246fa 100644 --- a/packages/gatsby/src/utils/path.js +++ b/packages/gatsby/src/utils/path.js @@ -13,3 +13,7 @@ export function joinPath(...paths) { export function withBasePath(basePath) { return (...paths) => joinPath(basePath, ...paths) } + +export function withTrailingSlash(basePath) { + return `${basePath}/` +} diff --git a/packages/gatsby/src/utils/webpack.config.js b/packages/gatsby/src/utils/webpack.config.js index 4700ddcdbb24b..d49f0a9e6b413 100644 --- a/packages/gatsby/src/utils/webpack.config.js +++ b/packages/gatsby/src/utils/webpack.config.js @@ -7,9 +7,10 @@ const FriendlyErrorsWebpackPlugin = require(`@pieh/friendly-errors-webpack-plugi const PnpWebpackPlugin = require(`pnp-webpack-plugin`) const { store } = require(`../redux`) const { actions } = require(`../redux/actions`) +const getPublicPath = require(`./get-public-path`) const debug = require(`debug`)(`gatsby:webpack-config`) const report = require(`gatsby-cli/lib/reporter`) -const { withBasePath } = require(`./path`) +const { withBasePath, withTrailingSlash } = require(`./path`) const apiRunnerNode = require(`./api-runner-node`) const createUtils = require(`./webpack-utils`) @@ -21,12 +22,7 @@ const hasLocalEslint = require(`./local-eslint-config-finder`) // 3) build-javascript: Build JS and CSS chunks for production // 4) build-html: build all HTML files -module.exports = async ( - program, - directory, - suppliedStage, - webpackPort = 1500 -) => { +module.exports = async (program, directory, suppliedStage) => { const directoryPath = withBasePath(directory) process.env.GATSBY_BUILD_STAGE = suppliedStage @@ -36,6 +32,10 @@ module.exports = async ( const stage = suppliedStage const { rules, loaders, plugins } = await createUtils({ stage, program }) + const { assetPrefix, pathPrefix } = store.getState().config + + const publicPath = getPublicPath({ assetPrefix, pathPrefix, ...program }) + function processEnv(stage, defaultNodeEnv) { debug(`Building env for "${stage}"`) // node env should be DEVELOPMENT | PRODUCTION as these are commonly used in node land @@ -98,7 +98,7 @@ module.exports = async ( if (pubPath.substr(-1) === `/`) { hmrBasePath = pubPath } else { - hmrBasePath = `${pubPath}/` + hmrBasePath = withTrailingSlash(pubPath) } } @@ -133,18 +133,14 @@ module.exports = async ( library: `lib`, umdNamedDefine: true, globalObject: `this`, - publicPath: program.prefixPaths - ? `${store.getState().config.pathPrefix}/` - : `/`, + publicPath: withTrailingSlash(publicPath), } case `build-javascript`: return { filename: `[name]-[contenthash].js`, chunkFilename: `[name]-[contenthash].js`, path: directoryPath(`public`), - publicPath: program.prefixPaths - ? `${store.getState().config.pathPrefix}/` - : `/`, + publicPath: withTrailingSlash(publicPath), } default: throw new Error(`The state requested ${stage} doesn't exist.`) @@ -188,8 +184,10 @@ module.exports = async ( // optimizations for React) and what the link prefix is (__PATH_PREFIX__). plugins.define({ ...processEnv(stage, `development`), - __PATH_PREFIX__: JSON.stringify( - program.prefixPaths ? store.getState().config.pathPrefix : `` + __BASE_PATH__: JSON.stringify(program.prefixPaths ? pathPrefix : ``), + __PATH_PREFIX__: JSON.stringify(program.prefixPaths ? publicPath : ``), + __ASSET_PREFIX__: JSON.stringify( + program.prefixPaths ? assetPrefix : `` ), }), ] diff --git a/yarn.lock b/yarn.lock index 80e6c54ecfdb2..0678f2d431777 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19047,6 +19047,11 @@ shell-escape@^0.2.0: resolved "https://registry.yarnpkg.com/shell-escape/-/shell-escape-0.2.0.tgz#68fd025eb0490b4f567a027f0bf22480b5f84133" integrity sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM= +shell-escape@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/shell-escape/-/shell-escape-0.2.0.tgz#68fd025eb0490b4f567a027f0bf22480b5f84133" + integrity sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM= + shell-quote@1.6.1, shell-quote@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767"