Skip to content

Commit b2d4aef

Browse files
LekoArtspieh
andauthored
feat(gatsby): Adapters (#38232)
* initial - cache restoration * snapshot * cache.restore() return comment * redirect status * note about named wildcard paths * don't pass named wildcards to function manifest * fix status code resolution for redirects * scaffold initial gatsby-adapter-netlify package and use if for dev purposes (hardcoded for now) * build adapter package as ESM and load it as such, so we can use ESM-only utils - like @netlify/cache-utils * no require in esm world * add redirect headers * start scaffolding autoinstallation of adapters * webpack assets + unmanaged assets (start) * static queries, app-data.json, minor refactors and initial setup for having routes sorted by specificity * move adapter version to 1.0.0 * generalize get-latest-apis for adapters * handle JS files in get-latest-gatsby-files * set peerDep * use other testing pkg * get installation, discovery, re-using working * update versions * move adapter init into its own file * add version checking * adjust comment * move stuff around * feat: add headers to gatsby-config * misc stuff * initial engine lambda * start headers utils * update deps * rewrite util * linting * convert to obj args * remove todo comment * add requiredFiles to functions manifest * make headers default to [] * move constants to own file * export rankRoute * delete unneeded util * createHeadersMatcher initial impl * use createHeadersMatcher * fix types * add http status code type * improve createHeadersMatcher and add tests * move adapterManager init to initialize func * adjust func args to move reporter and allow adapter options * add "adapter" option to gatsby-config * put netlify adapter first in the list - it will only match when env var is set, so won't change default behavior of using testing one * kebabcase function name as function id * export FunctionDefinition type * req.path -> req.url * initial functions wrapping/bundling in gatsby-adapter-netlify * remove netlify adapter from gatsby deps, add gatsby as devDep to adapter (to access types) * fix bundling function files containing [ ] * unify tsconfig for adapter * add joi testing for adapter setting * typescript: make bootstrap work again * generate redirect/rewrite rules, generate 2 variants of ssr-engine (odb and regular) * move routes manifest handling into its own module * generate _headers rules * add sorting to routesManifest * adjust graphql-engine bundling to not leave unreasolvable imports * ssr lambda handling when it executes in read-only dir (use tmpdir() then) * inject functions matchPath into function bundle and generate req.params inside of it * serve api from path prefixed path as well * add path prefix stripping in function wrapper * add cache store and restore in gatsby-adapter-netlify * adjust internal 'lambda' name to 'function' * format lambda-handler * misc changes * missing rename * compile gatsby-adapter-netlify to cjs * add generator field * use netlify adpter when NETLIFY or NETLIFY_LOCAL env var is defined * use headers from config for ssg/dsg * allow specyfing different lmdb binary than current process, use abi83 if adapters are used (it works on node14, 16 and 18) * get-route-path tests * manager refactoring + typo fix * initialize adapters e2e test * cypress: remove viewPort configs * use next preid for netlify adapter * remove test adapter from adapters manifest * don't log errors when testing for user installed adapters * update adapters manifest * cleanup gatsby-adapter-netlify a bit, add more public adapter related types to gatsby * e2e: update gitignore * resolve lmdb binary from lmdb package and not hardcode the forced path * gatsby-plugin-image add downlevelIteration * fix persisted redux keys * update snapshot and mocks * fix: only run adapters during gatsby build * resolve netlify functions runtime deps from adapter context * improve public typings * adding adapter to e2e test so that dev-cli copies stuff over * update babel-preset-gatsby-package dep * e2e: test functions and assets * e2e: client-only WIP * e2e: improve basics * e2e: improve client-only * e2e: redirects * merge _headers and _redirects instead of overwriting it * apply trailing slash option + pass through trailingSlash & pathPrefix * add unit tests for manager * improve manager tests * update types * improve e2e tests * add excludeDatastoreFromEngineFunction flow * normalize path after globbing * mock shouldBundleDatastore * rename adapter config types to be less confusing with gatsby-config * keep same obfuscated path between builds * normalize more paths * support custom 404/500 page for serverless functions * update snapshot * generate relative imports in function * skip trying to copy data to tmp if we are downloading from cdn * improve TS types * mock uuid * put requiredFiles in correct place heh * typo * snapshot * handle GATSBY_EXCLUDE_DATASTORE_FROM_BUNDLE env var * improve README & update types * code block * handle partytown routes * handle slices (html and slice-data) * handle chunk-map and webpack.stats * feat: add name to functionsManifest & displayName to Netlify * update snapshot * rename headers constants * handle image-cdn and file-cdn * update routesManifest test fixture and snapshot * don't log unmanaged static assets anymore * handle some TODOs * add 'adapters' to feature list * tmp: make peer dependency allow to use with canaries * feat: add 'supports' to config to let adapters provide some capabilities and potentially fail builds with clear explanation instead of producing faulty deploy * adjust some text * apply trailingSlash to tests and other stuff, add utils * readme and package.json update * fix ts versions * enable typecheck for adapters.js * don't try to use install adapter if no version matches, install version specified in adapters.js * allow adapter to disable prior deployment plugins * disable gatsby-plugin-netlify when using gatsby-adapter-netlify * fix ts * handle non-alpha-numerc paths * verbose log adapter package that is being installed * only try to restore cache if payload provided * memoize cache utils * log adapter version * handle body parsing in produced api functions * properly handle cases when body parsing already happened and when there is no function handler export * handle body parsing config in generated function * drop unused * remove redirects created by previous deployment plugins * properly handle default body for status responses in api functions * put some dev logs as verbose to limit regular terminal output * put some dev logs as verbose to limit regular terminal output vol2 * maybe deflake function tests * maybe precompile api functions in develop function tests --------- Co-authored-by: Michal Piechowiak <[email protected]>
1 parent 7a2778b commit b2d4aef

File tree

138 files changed

+5816
-284
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

138 files changed

+5816
-284
lines changed

.circleci/config.yml

+11
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,15 @@ jobs:
444444
- store_test_results:
445445
path: e2e-tests/trailing-slash/cypress/results
446446

447+
e2e_tests_adapters:
448+
<<: *e2e-executor
449+
steps:
450+
- run: echo 'export CYPRESS_RECORD_KEY="${CY_CLOUD_ADAPTERS}"' >> "$BASH_ENV"
451+
- e2e-test:
452+
test_path: e2e-tests/adapters
453+
- store_test_results:
454+
path: e2e-tests/adapters/cypress/results
455+
447456
starters_validate:
448457
executor: node
449458
steps:
@@ -594,6 +603,8 @@ workflows:
594603
<<: *e2e-test-workflow
595604
- e2e_tests_trailing-slash:
596605
<<: *e2e-test-workflow
606+
- e2e_tests_adapters:
607+
<<: *e2e-test-workflow
597608
- e2e_tests_development_runtime_with_react_18:
598609
<<: *e2e-test-workflow
599610
- e2e_tests_production_runtime_with_react_18:

e2e-tests/adapters/.gitignore

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
node_modules/
2+
.cache/
3+
public
4+
5+
# Local Netlify folder
6+
.netlify
7+
8+
# Cypress output
9+
cypress/videos/
10+
cypress/screenshots/
11+
12+
# Custom .yarnrc file for gatsby-dev on Yarn 3
13+
.yarnrc.yml

e2e-tests/adapters/README.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# adapters
2+
3+
E2E testing suite for Gatsby's [adapters](http://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting/adapters/) feature.
4+
If possible, run the tests locally with a CLI. Otherwise deploy the site to the target platform and run Cypress on the deployed URL.
5+
6+
Adapters being tested:
7+
8+
- [gatsby-adapter-netlify](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-adapter-netlify)
9+
10+
## Usage
11+
12+
- To run all tests, use `npm run test`
13+
- To run individual tests, use `npm run test:%NAME` where `test:%NAME` is the script, e.g. `npm run test:netlify`
14+
15+
If you want to open Cypress locally as a UI, you can run the `:debug` scripts. For example, `npm run test:netlify:debug` to test the Netlify Adapter with Cypress open.
16+
17+
### Adding a new adapter
18+
19+
- Add a new Cypress config inside `cypress/configs`
20+
- Add a new `test:` script that should run `start-server-and-test`. You can check what e.g. `test:netlify` is doing.
21+
- Run the Cypress test suites that should work. If you want to exclude a spec, you can use Cypress' [excludeSpecPattern](https://docs.cypress.io/guides/references/configuration#excludeSpecPattern)
22+
23+
## External adapters
24+
25+
As mentioned in [Creating an Adapter](https://gatsbyjs.com/docs/how-to/previews-deploys-hosting/creating-an-adapter/#testing) you can use this test suite for your own adapter.
26+
27+
Copy the whole `adapters` folder, and follow [adding a new adapter](#adding-a-new-adapter).

e2e-tests/adapters/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const title = "Adapters"
2+
export const siteDescription = "End-to-End tests for Gatsby Adapters"

e2e-tests/adapters/cypress.config.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { defineConfig } from "cypress"
2+
3+
export default defineConfig({
4+
e2e: {
5+
baseUrl: `http://localhost:9000`,
6+
projectId: `4enh4m`,
7+
videoUploadOnPasses: false,
8+
experimentalRunAllSpecs: true,
9+
retries: 2,
10+
},
11+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { defineConfig } from "cypress"
2+
3+
export default defineConfig({
4+
e2e: {
5+
baseUrl: `http://localhost:8888`,
6+
// Netlify doesn't handle trailing slash behaviors really, so no use in testing it
7+
excludeSpecPattern: [`cypress/e2e/trailing-slash.cy.ts`,],
8+
projectId: `4enh4m`,
9+
videoUploadOnPasses: false,
10+
experimentalRunAllSpecs: true,
11+
retries: 2,
12+
},
13+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { title } from "../../constants"
2+
3+
describe('Basics', () => {
4+
beforeEach(() => {
5+
cy.intercept("/gatsby-icon.png").as("static-folder-image")
6+
cy.intercept("/static/astro-**.png").as("img-import")
7+
8+
cy.visit('/').waitForRouteChange()
9+
})
10+
11+
it('should display index page', () => {
12+
cy.get('h1').should('have.text', title)
13+
cy.title().should('eq', 'Adapters E2E')
14+
})
15+
// If this test fails, run "gatsby build" and retry
16+
it('should serve assets from "static" folder', () => {
17+
cy.wait("@static-folder-image").should(req => {
18+
expect(req.response.statusCode).to.be.gte(200).and.lt(400)
19+
})
20+
21+
cy.get('[alt="Gatsby Monogram Logo"]').should('be.visible')
22+
})
23+
it('should serve assets imported through webpack', () => {
24+
cy.wait("@img-import").should(req => {
25+
expect(req.response.statusCode).to.be.gte(200).and.lt(400)
26+
})
27+
28+
cy.get('[alt="Gatsby Astronaut"]').should('be.visible')
29+
})
30+
it(`should show custom 404 page on invalid URL`, () => {
31+
cy.visit(`/non-existent-page`, {
32+
failOnStatusCode: false,
33+
})
34+
35+
cy.get('h1').should('have.text', 'Page not found')
36+
})
37+
it('should apply CSS', () => {
38+
cy.get(`h1`).should(
39+
`have.css`,
40+
`color`,
41+
`rgb(21, 21, 22)`
42+
)
43+
})
44+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
Cypress.on('uncaught:exception', (err) => {
2+
if (err.message.includes('Minified React error')) {
3+
return false
4+
}
5+
})
6+
7+
describe('Sub-Router', () => {
8+
const routes = [
9+
{
10+
path: "/routes/sub-router",
11+
marker: "index",
12+
label: "Index route"
13+
},
14+
{
15+
path: `/routes/sub-router/page/profile`,
16+
marker: `profile`,
17+
label: `Dynamic route`,
18+
},
19+
{
20+
path: `/routes/sub-router/not-found`,
21+
marker: `NotFound`,
22+
label: `Default route (not found)`,
23+
},
24+
{
25+
path: `/routes/sub-router/nested`,
26+
marker: `nested-page/index`,
27+
label: `Index route inside nested router`,
28+
},
29+
{
30+
path: `/routes/sub-router/nested/foo`,
31+
marker: `nested-page/foo`,
32+
label: `Dynamic route inside nested router`,
33+
},
34+
{
35+
path: `/routes/sub-router/static`,
36+
marker: `static-sibling`,
37+
label: `Static route that is a sibling to client only path`,
38+
},
39+
] as const
40+
41+
routes.forEach(({ path, marker, label }) => {
42+
it(label, () => {
43+
cy.visit(path).waitForRouteChange()
44+
cy.get(`[data-testid="dom-marker"]`).contains(marker)
45+
46+
cy.url().should(
47+
`match`,
48+
new RegExp(`^${Cypress.config().baseUrl + path}/?$`)
49+
)
50+
})
51+
})
52+
})
53+
54+
describe('Paths', () => {
55+
const routes = [
56+
{
57+
name: 'client-only',
58+
param: 'dune',
59+
},
60+
{
61+
name: 'client-only/wildcard',
62+
param: 'atreides/harkonnen',
63+
},
64+
{
65+
name: 'client-only/named-wildcard',
66+
param: 'corinno/fenring',
67+
},
68+
] as const
69+
70+
for (const route of routes) {
71+
it(`should return "${route.name}" result`, () => {
72+
cy.visit(`/routes/${route.name}${route.param ? `/${route.param}` : ''}`).waitForRouteChange()
73+
cy.get("[data-testid=title]").should("have.text", route.name)
74+
cy.get("[data-testid=params]").should("have.text", route.param)
75+
})
76+
}
77+
})
78+
79+
describe('Prioritize', () => {
80+
it('should prioritize static page over matchPath page with wildcard', () => {
81+
cy.visit('/routes/client-only/prioritize').waitForRouteChange()
82+
cy.get("[data-testid=title]").should("have.text", "client-only/prioritize static")
83+
})
84+
it('should return result for wildcard on nested prioritized path', () => {
85+
cy.visit('/routes/client-only/prioritize/nested').waitForRouteChange()
86+
cy.get("[data-testid=title]").should("have.text", "client-only/prioritize matchpath")
87+
cy.get("[data-testid=params]").should("have.text", "nested")
88+
})
89+
})
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { title } from "../../constants"
2+
3+
describe("Deferred Static Generation (DSG)", () => {
4+
it("should work correctly", () => {
5+
cy.visit("/routes/dsg/static").waitForRouteChange()
6+
7+
cy.get("h1").contains("DSG")
8+
})
9+
it("should work with page queries", () => {
10+
cy.visit("/routes/dsg/graphql-query").waitForRouteChange()
11+
12+
cy.get(`[data-testid="title"]`).should("have.text", title)
13+
})
14+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const routes = [
2+
{
3+
name: 'static',
4+
param: '',
5+
},
6+
{
7+
name: 'param',
8+
param: 'dune',
9+
},
10+
{
11+
name: 'wildcard',
12+
param: 'atreides/harkonnen'
13+
},
14+
{
15+
name: 'named-wildcard',
16+
param: 'corinno/fenring'
17+
}
18+
] as const
19+
20+
describe('Functions', () => {
21+
for (const route of routes) {
22+
it(`should return "${route.name}" result`, () => {
23+
cy.request(`/api/${route.name}${route.param ? `/${route.param}` : ''}`).as(`req-${route.name}`)
24+
cy.get(`@req-${route.name}`).its('body').should('contain', `Hello World${route.param ? ` from ${route.param}` : ``}`)
25+
})
26+
}
27+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { applyTrailingSlashOption } from "../../utils"
2+
3+
Cypress.on("uncaught:exception", (err) => {
4+
if (err.message.includes("Minified React error")) {
5+
return false
6+
}
7+
})
8+
9+
const TRAILING_SLASH = Cypress.env(`TRAILING_SLASH`) || `never`
10+
11+
// Those tests won't work using `gatsby serve` because it doesn't support redirects
12+
13+
describe("Redirects", () => {
14+
it("should redirect from non-existing page to existing", () => {
15+
cy.visit(applyTrailingSlashOption(`/redirect`, TRAILING_SLASH), {
16+
failOnStatusCode: false,
17+
}).waitForRouteChange()
18+
.assertRoute(applyTrailingSlashOption(`/routes/redirect/hit`, TRAILING_SLASH))
19+
20+
cy.get(`h1`).should(`have.text`, `Hit`)
21+
})
22+
it("should respect that pages take precedence over redirects", () => {
23+
cy.visit(applyTrailingSlashOption(`/routes/redirect/existing`, TRAILING_SLASH), {
24+
failOnStatusCode: false,
25+
}).waitForRouteChange()
26+
.assertRoute(applyTrailingSlashOption(`/routes/redirect/existing`, TRAILING_SLASH))
27+
28+
cy.get(`h1`).should(`have.text`, `Existing`)
29+
})
30+
it("should support hash parameter on direct visit", () => {
31+
cy.visit(applyTrailingSlashOption(`/redirect`, TRAILING_SLASH) + `#anchor`, {
32+
failOnStatusCode: false,
33+
}).waitForRouteChange()
34+
35+
cy.location(`pathname`).should(`equal`, applyTrailingSlashOption(`/routes/redirect/hit`, TRAILING_SLASH))
36+
cy.location(`hash`).should(`equal`, `#anchor`)
37+
cy.location(`search`).should(`equal`, ``)
38+
})
39+
it("should support search parameter on direct visit", () => {
40+
cy.visit(applyTrailingSlashOption(`/redirect`, TRAILING_SLASH) + `?query_param=hello`, {
41+
failOnStatusCode: false,
42+
}).waitForRouteChange()
43+
44+
cy.location(`pathname`).should(`equal`, applyTrailingSlashOption(`/routes/redirect/hit`, TRAILING_SLASH))
45+
cy.location(`hash`).should(`equal`, ``)
46+
cy.location(`search`).should(`equal`, `?query_param=hello`)
47+
})
48+
it("should support search & hash parameter on direct visit", () => {
49+
cy.visit(applyTrailingSlashOption(`/redirect`, TRAILING_SLASH) + `?query_param=hello#anchor`, {
50+
failOnStatusCode: false,
51+
}).waitForRouteChange()
52+
53+
cy.location(`pathname`).should(`equal`, applyTrailingSlashOption(`/routes/redirect/hit`, TRAILING_SLASH))
54+
cy.location(`hash`).should(`equal`, `#anchor`)
55+
cy.location(`search`).should(`equal`, `?query_param=hello`)
56+
})
57+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { siteDescription } from "../../constants"
2+
3+
describe("Slices", () => {
4+
it("should work correctly", () => {
5+
cy.visit('/').waitForRouteChange()
6+
7+
cy.get(`footer`).should("have.text", siteDescription)
8+
})
9+
})
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const staticPath = "/routes/ssr/static"
2+
const paramPath = "/routes/ssr/param"
3+
4+
describe("Server Side Rendering (SSR)", () => {
5+
it(`direct visit no query params (${staticPath})`, () => {
6+
cy.visit(staticPath).waitForRouteChange()
7+
cy.get(`[data-testid="query"]`).contains(`{}`)
8+
cy.get(`[data-testid="params"]`).contains(`{}`)
9+
})
10+
11+
it(`direct visit with query params (${staticPath})`, () => {
12+
cy.visit(staticPath + `?foo=bar`).waitForRouteChange()
13+
cy.get(`[data-testid="query"]`).contains(`{"foo":"bar"}`)
14+
cy.get(`[data-testid="params"]`).contains(`{}`)
15+
})
16+
17+
it(`direct visit no query params (${paramPath})`, () => {
18+
cy.visit(paramPath + `/foo`).waitForRouteChange()
19+
cy.get(`[data-testid="query"]`).contains(`{}`)
20+
cy.get(`[data-testid="params"]`).contains(`{"param":"foo"}`)
21+
})
22+
23+
it(`direct visit with query params (${paramPath})`, () => {
24+
cy.visit(paramPath + `/foo` + `?foo=bar`).waitForRouteChange()
25+
cy.get(`[data-testid="query"]`).contains(`{"foo":"bar"}`)
26+
cy.get(`[data-testid="params"]`).contains(`{"param":"foo"}`)
27+
})
28+
29+
it(`should display custom 500 page`, () => {
30+
const errorPath = `/routes/ssr/error-path`
31+
32+
cy.visit(errorPath, { failOnStatusCode: false }).waitForRouteChange()
33+
34+
cy.location(`pathname`)
35+
.should(`equal`, errorPath)
36+
.get(`h1`)
37+
.should(`have.text`, `INTERNAL SERVER ERROR`)
38+
})
39+
})

0 commit comments

Comments
 (0)