diff --git a/plugin/src/helpers/config.ts b/plugin/src/helpers/config.ts index fc2e863d..1da166eb 100644 --- a/plugin/src/helpers/config.ts +++ b/plugin/src/helpers/config.ts @@ -15,7 +15,7 @@ import fs, { import type { GatsbyConfig, PluginRef } from 'gatsby' import { v4 as uuidv4 } from 'uuid' -import { checkPackageVersion } from './files' +import { checkPackageVersion, findModuleFromBase } from './files' import type { FunctionList } from './functions' /** @@ -338,4 +338,48 @@ function isEnvSet(envVar: string) { export function getGatsbyRoot(publish: string): string { return resolve(dirname(publish)) } + +export function shouldSkip(publishDir: string): boolean { + if (typeof process.env.NETLIFY_SKIP_GATSBY_BUILD_PLUGIN !== 'undefined') { + return isEnvSet('NETLIFY_SKIP_GATSBY_BUILD_PLUGIN') + } + + const siteRoot = getGatsbyRoot(publishDir) + + let shouldSkipResult = false + + try { + const gatsbyPath = findModuleFromBase({ + paths: [siteRoot], + candidates: ['gatsby/package.json'], + }) + if (gatsbyPath) { + const gatsbyPluginUtilsPath = findModuleFromBase({ + paths: [gatsbyPath, siteRoot], + candidates: ['gatsby-plugin-utils'], + }) + + // eslint-disable-next-line import/no-dynamic-require, n/global-require, @typescript-eslint/no-var-requires + const { hasFeature } = require(gatsbyPluginUtilsPath) + + if (hasFeature(`adapters`)) { + shouldSkipResult = true + } + } + } catch { + // ignore + } + + process.env.NETLIFY_SKIP_GATSBY_BUILD_PLUGIN = shouldSkipResult + ? 'true' + : 'false' + + if (shouldSkipResult) { + console.log( + 'Skipping @netlify/plugin-gatsby work, because used Gatsby version supports adapters.', + ) + } + + return shouldSkipResult +} /* eslint-enable max-lines */ diff --git a/plugin/src/index.ts b/plugin/src/index.ts index 84e73b41..4ee696c0 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -14,6 +14,7 @@ import { getNeededFunctions, modifyConfig, shouldSkipBundlingDatastore, + shouldSkip, } from './helpers/config' import { modifyFiles } from './helpers/files' import { deleteFunctions, writeFunctions } from './helpers/functions' @@ -33,6 +34,11 @@ export async function onPreBuild({ `Gatsby sites must publish the "public" directory, but your site’s publish directory is set to “${PUBLISH_DIR}”. Please set your publish directory to your Gatsby site’s "public" directory.`, ) } + + if (shouldSkip(PUBLISH_DIR)) { + return + } + await restoreCache({ utils, publish: PUBLISH_DIR }) await checkConfig({ utils, netlifyConfig }) @@ -47,6 +53,11 @@ export async function onBuild({ FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC, } = constants + + if (shouldSkip(PUBLISH_DIR)) { + return + } + const cacheDir = normalizedCacheDir(PUBLISH_DIR) if ( @@ -81,6 +92,10 @@ export async function onPostBuild({ constants: { PUBLISH_DIR, FUNCTIONS_DIST }, utils, }): Promise { + if (shouldSkip(PUBLISH_DIR)) { + return + } + await saveCache({ publish: PUBLISH_DIR, utils }) const cacheDir = normalizedCacheDir(PUBLISH_DIR) @@ -92,7 +107,11 @@ export async function onPostBuild({ } } -export async function onSuccess() { +export async function onSuccess({ constants: { PUBLISH_DIR } }) { + if (shouldSkip(PUBLISH_DIR)) { + return + } + // Pre-warm the lambdas as downloading the datastore file can take a while if (shouldSkipBundlingDatastore()) { const FETCH_TIMEOUT = 5000 diff --git a/plugin/test/fixtures/v5/with-adapters/.env.production b/plugin/test/fixtures/v5/with-adapters/.env.production new file mode 100644 index 00000000..72958bc0 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/.env.production @@ -0,0 +1 @@ +pickle=word diff --git a/plugin/test/fixtures/v5/with-adapters/.gitignore b/plugin/test/fixtures/v5/with-adapters/.gitignore new file mode 100644 index 00000000..459af46a --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/.gitignore @@ -0,0 +1,14 @@ +node_modules/ +.cache/ +public + +# Local Netlify folder +.netlify +netlify/functions/gatsby/functions +deployment.json + +# @netlify/plugin-gatsby ignores start +netlify/functions/gatsby +# @netlify/plugin-gatsby ignores end + +package-lock.json \ No newline at end of file diff --git a/plugin/test/fixtures/v5/with-adapters/.nvmrc b/plugin/test/fixtures/v5/with-adapters/.nvmrc new file mode 100644 index 00000000..cab13a79 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/.nvmrc @@ -0,0 +1 @@ +v14.17.0 diff --git a/plugin/test/fixtures/v5/with-adapters/e2e-tests/__snapshots__/functions.test.js.snap b/plugin/test/fixtures/v5/with-adapters/e2e-tests/__snapshots__/functions.test.js.snap new file mode 100644 index 00000000..6c33461e --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/e2e-tests/__snapshots__/functions.test.js.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Local functions can parse different ways of sending data file in multipart/form 1`] = ` +Object { + "body": Object { + "something": "here", + }, + "files": Array [ + Object { + "buffer": Object { + "data": Array [ + 104, + 105, + ], + "type": "Buffer", + }, + "encoding": "7bit", + "fieldname": "file", + "mimetype": "text/plain", + "originalname": "test.txt", + "size": 2, + }, + ], +} +`; + +exports[`Local routing dynamic routes 1`] = ` +Object { + "super": "additional", + "userId": "23", +} +`; + +exports[`Local routing dynamic routes 2`] = ` +Object { + "*": "super", + "0": "super", +} +`; + +exports[`Local routing routes with special characters 1`] = `"I-Am-Capitalized.js"`; diff --git a/plugin/test/fixtures/v5/with-adapters/e2e-tests/build.test.js b/plugin/test/fixtures/v5/with-adapters/e2e-tests/build.test.js new file mode 100644 index 00000000..09b9044f --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/e2e-tests/build.test.js @@ -0,0 +1,33 @@ +// eslint-disable-next-line node/no-unpublished-require +const { buildSite } = require('../../../../helpers') +const { readFileSync } = require('fs') + +jest.setTimeout(240_000) +describe('A site using gatsby version with adapters', () => { + it('successfully builds and disables @netlify/plugin-gatsby and gatsby-plugin-netlify', async () => { + const { + success, + logs: { stdout, stderr }, + } = await buildSite() + + // in CI warnings are outputted to stderr (yikes) + const fullOutput = stdout + stderr + + expect(success).toBeTruthy() + + expect(fullOutput).toContain( + 'Skipping @netlify/plugin-gatsby work, because used Gatsby version supports adapters.', + ) + expect(fullOutput).toContain('Disabling plugin "gatsby-plugin-netlify"') + + const _redirectsContent = readFileSync('public/_redirects', 'utf8') + + expect(_redirectsContent).not.toContain( + '# @netlify/plugin-gatsby redirects start', + ) + expect(_redirectsContent).not.toContain( + '## Created with gatsby-plugin-netlify', + ) + expect(_redirectsContent).toContain('# gatsby-adapter-netlify start') + }) +}) diff --git a/plugin/test/fixtures/v5/with-adapters/e2e-tests/fixtures/test.txt b/plugin/test/fixtures/v5/with-adapters/e2e-tests/fixtures/test.txt new file mode 100644 index 00000000..32f95c0d --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/e2e-tests/fixtures/test.txt @@ -0,0 +1 @@ +hi \ No newline at end of file diff --git a/plugin/test/fixtures/v5/with-adapters/e2e-tests/functions.test.js b/plugin/test/fixtures/v5/with-adapters/e2e-tests/functions.test.js new file mode 100644 index 00000000..4f0112b3 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/e2e-tests/functions.test.js @@ -0,0 +1,8 @@ +const { runTests } = require('./test-helpers') + +if (process.env.TEST_ENV === 'netlify') { + const { deploy_url } = require('../deployment.json') + runTests('Netlify', deploy_url) +} else { + runTests('Local', 'http://localhost:8888') +} diff --git a/plugin/test/fixtures/v5/with-adapters/e2e-tests/test-helpers.js b/plugin/test/fixtures/v5/with-adapters/e2e-tests/test-helpers.js new file mode 100644 index 00000000..30b4d1ea --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/e2e-tests/test-helpers.js @@ -0,0 +1,277 @@ +/* eslint-disable no-unused-vars */ +const fetch = require(`node-fetch`) +const { readFileSync } = require('fs') +const path = require('path') + +const FormData = require('form-data') +// Based on Gatsby Functions integration tests +// Source: https://github.com/gatsbyjs/gatsby/blob/master/integration-tests/functions/test-helpers.js + +exports.runTests = function runTests(env, host) { + jest.setTimeout(10_000) + + async function fetchTwice(url, options) { + const result = await fetch(url, options) + if (!result.headers.has('x-forwarded-host')) { + return result + } + return fetch(url, options) + } + + describe(env, () => { + describe(`routing`, () => { + test(`top-level API`, async () => { + const result = await fetchTwice(`${host}/api/top-level`).then((res) => + res.text(), + ) + + expect(result).toEqual('I am at the top-level') + }) + test(`secondary-level API`, async () => { + const result = await fetchTwice( + `${host}/api/a-directory/function`, + ).then((res) => res.text()) + + expect(result).toEqual('I am at a secondary-level') + }) + test(`secondary-level API with index.js`, async () => { + const result = await fetchTwice(`${host}/api/a-directory`).then((res) => + res.text(), + ) + + expect(result).toEqual('I am an index.js in a sub-directory!') + }) + test(`secondary-level API`, async () => { + const result = await fetchTwice(`${host}/api/dir/function`).then( + (res) => res.text(), + ) + + expect(result).toEqual('I am another sub-directory function') + }) + test(`routes with special characters`, async () => { + const routes = [ + `${host}/api/I-Am-Capitalized`, + // `netlify serve` doesn't handle encoded paths same as the platform, + // so skipping these tests + // `${host}/api/some whitespace`, + // `${host}/api/with-äöü-umlaut`, + // `${host}/api/some-àè-french`, + // encodeURI(`${host}/api/some-אודות`), + ] + + for (const route of routes) { + const result = await fetchTwice(route).then((res) => res.text()) + + expect(result).toMatchSnapshot() + } + }) + + test(`dynamic routes`, async () => { + const routes = [ + `${host}/api/users/23/additional`, + `${host}/api/dir/super`, + ] + + for (const route of routes) { + const result = await fetchTwice(route).then((res) => res.json()) + + expect(result).toMatchSnapshot() + } + }) + }) + + describe(`environment variables`, () => { + test(`can use inside functions`, async () => { + const result = await fetchTwice(`${host}/api/env-variables`).then( + (res) => res.text(), + ) + + expect(result).toEqual(`word`) + }) + }) + + describe(`typescript`, () => { + test(`typescript functions work`, async () => { + const result = await fetchTwice(`${host}/api/i-am-typescript`).then( + (res) => res.text(), + ) + + expect(result).toEqual('I am typescript') + }) + }) + + describe(`function errors don't crash the server`, () => { + // This test mainly just shows that the server doesn't crash. + test(`normal`, async () => { + const result = await fetchTwice(`${host}/api/error-send-function-twice`) + + expect(result.status).toEqual(200) + }) + }) + + describe(`response formats`, () => { + test(`returns json correctly`, async () => { + const res = await fetchTwice(`${host}/api/i-am-json`) + const result = await res.json() + + expect(result).toEqual({ + amIJSON: true, + }) + expect(res.headers.get('content-type')).toEqual('application/json') + }) + test(`returns json correctly via send`, async () => { + const res = await fetchTwice(`${host}/api/i-am-json-too`) + const result = await res.json() + + expect(result).toEqual({ + amIJSON: true, + }) + expect(res.headers.get('content-type')).toEqual('application/json') + }) + test(`returns boolean correctly via send`, async () => { + const res = await fetchTwice(`${host}/api/i-am-false`) + const result = await res.json() + + expect(result).toEqual(false) + expect(res.headers.get('content-type')).toEqual('application/json') + }) + test(`returns status correctly via send`, async () => { + const res = await fetchTwice(`${host}/api/i-am-status`) + const result = await res.text() + + expect(result).toEqual('OK') + expect(res.headers.get('content-type')).toEqual( + 'text/plain; charset=utf-8', + ) + }) + test(`returns text correctly`, async () => { + const res = await fetchTwice(`${host}/api/i-am-text`) + const result = await res.text() + + expect(result).toEqual('I am text') + expect(res.headers.get('content-type')).toEqual( + 'text/html; charset=utf-8', + ) + }) + }) + + describe(`functions can send custom statuses`, () => { + test(`can return 200 status`, async () => { + const res = await fetchTwice(`${host}/api/status`) + + expect(res.status).toEqual(200) + }) + + test(`can return 404 status`, async () => { + const res = await fetchTwice(`${host}/api/status?code=404`) + + expect(res.status).toEqual(404) + }) + + test(`can return 500 status`, async () => { + const res = await fetchTwice(`${host}/api/status?code=500`) + + expect(res.status).toEqual(500) + }) + }) + + describe(`functions can parse different ways of sending data`, () => { + test(`query string`, async () => { + const result = await fetchTwice(`${host}/api/parser?amIReal=true`).then( + (res) => res.json(), + ) + + expect(result).toEqual({ + amIReal: 'true', + }) + }) + + test(`form parameters`, async () => { + const { URLSearchParams } = require('url') + const params = new URLSearchParams() + params.append('a', `form parameters`) + const result = await fetchTwice(`${host}/api/parser`, { + method: `POST`, + body: params, + }).then((res) => res.json()) + + expect(result).toEqual({ + a: 'form parameters', + }) + }) + + test(`form data`, async () => { + const form = new FormData() + form.append('a', `form-data`) + const result = await fetchTwice(`${host}/api/parser`, { + method: `POST`, + body: form, + }).then((res) => res.json()) + + expect(result).toEqual({ + a: 'form-data', + }) + }) + + test(`json body`, async () => { + const body = { a: `json` } + const result = await fetchTwice(`${host}/api/parser`, { + method: `POST`, + body: JSON.stringify(body), + headers: { 'Content-Type': 'application/json' }, + }).then((res) => res.json()) + + expect(result).toEqual({ + a: 'json', + }) + }) + + it(`file in multipart/form`, async () => { + const file = readFileSync(path.join(__dirname, './fixtures/test.txt')) + + const form = new FormData() + form.append('file', file, { + filename: 'test.txt', + contentType: 'text/plain', + }) + form.append('something', 'here') + const result = await fetchTwice(`${host}/api/parser`, { + method: `POST`, + body: form, + headers: form.getHeaders(), + }).then((res) => res.json()) + + expect(result).toMatchSnapshot() + }) + }) + + describe(`functions get parsed cookies`, () => { + test(`cookie`, async () => { + const result = await fetchTwice(`${host}/api/cookie-me`, { + headers: { cookie: `foo=blue;` }, + }).then((res) => res.json()) + + expect(result).toEqual({ + foo: 'blue', + }) + }) + }) + + describe(`functions can redirect`, () => { + test(`normal`, async () => { + const result = await fetchTwice(`${host}/api/redirect-me`) + + expect(result.url).toEqual(`${host}/`) + }) + }) + + describe(`functions can have custom middleware`, () => { + test(`normal`, async () => { + const result = await fetchTwice(`${host}/api/cors`) + + const headers = Object.fromEntries(result.headers) + expect(headers[`access-control-allow-origin`]).toEqual(`*`) + }) + }) + }) +} diff --git a/plugin/test/fixtures/v5/with-adapters/gatsby-config.js b/plugin/test/fixtures/v5/with-adapters/gatsby-config.js new file mode 100644 index 00000000..6e3b3d9a --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/gatsby-config.js @@ -0,0 +1,6 @@ +module.exports = { + siteMetadata: { + title: 'Function test', + }, + plugins: [`gatsby-plugin-netlify`], +} diff --git a/plugin/test/fixtures/v5/with-adapters/netlify.toml b/plugin/test/fixtures/v5/with-adapters/netlify.toml new file mode 100644 index 00000000..72865a74 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/netlify.toml @@ -0,0 +1,6 @@ +[build] +command = "npm run build" +publish = "public/" + +[[plugins]] +package = "../../../../src/index.ts" diff --git a/plugin/test/fixtures/v5/with-adapters/package.json b/plugin/test/fixtures/v5/with-adapters/package.json new file mode 100644 index 00000000..f75276ea --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/package.json @@ -0,0 +1,39 @@ +{ + "name": "with-adapters", + "version": "1.0.0", + "private": true, + "description": "Function and adapters test", + "author": "Matt Kane", + "keywords": [ + "gatsby" + ], + "scripts": { + "develop": "HOST=0.0.0.0 gatsby develop", + "start": "HOST=0.0.0.0 gatsby develop", + "build": "gatsby --version && gatsby build", + "serve": "gatsby serve", + "clean": "gatsby clean", + "build:netlify": "jest build.test.js", + "preview": "netlify serve", + "test": "run-s build:netlify test:e2e", + "test:e2e": "start-server-and-test preview 8888 test:jest", + "test:jest": "jest functions.test.js" + }, + "dependencies": { + "gatsby": "5.10.0-alpha-adapters.165", + "gatsby-plugin-netlify": "5.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "form-data": "^4.0.0", + "jest": "^26.6.3", + "node-fetch": "^2.6.1", + "npm-run-all": "^4.1.5", + "start-server-and-test": "^1.12.2" + }, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/I-Am-Capitalized.js b/plugin/test/fixtures/v5/with-adapters/src/api/I-Am-Capitalized.js new file mode 100644 index 00000000..32d09568 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/I-Am-Capitalized.js @@ -0,0 +1,4 @@ +const path = require(`path`) +export default function topLevel(req, res) { + res.send(`${path.parse(__filename).name}${path.parse(__filename).ext}`) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/a-directory/function.js b/plugin/test/fixtures/v5/with-adapters/src/api/a-directory/function.js new file mode 100644 index 00000000..537e4829 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/a-directory/function.js @@ -0,0 +1,3 @@ +export default function topLevel(req, res) { + res.send(`I am at a secondary-level`) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/a-directory/index.js b/plugin/test/fixtures/v5/with-adapters/src/api/a-directory/index.js new file mode 100644 index 00000000..a646bc11 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/a-directory/index.js @@ -0,0 +1,3 @@ +export default function topLevel(req, res) { + res.send(`I am an index.js in a sub-directory!`) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/cookie-me.ts b/plugin/test/fixtures/v5/with-adapters/src/api/cookie-me.ts new file mode 100644 index 00000000..7f4664c9 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/cookie-me.ts @@ -0,0 +1,8 @@ +import { GatsbyFunctionResponse, GatsbyFunctionRequest } from 'gatsby' + +export default function topLevel( + req: GatsbyFunctionRequest, + res: GatsbyFunctionResponse, +) { + res.json(req.cookies) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/cors.js b/plugin/test/fixtures/v5/with-adapters/src/api/cors.js new file mode 100644 index 00000000..10dd8925 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/cors.js @@ -0,0 +1,18 @@ +import Cors from 'cors' + +const cors = Cors() + +export default async function corsHandler(req, res) { + // Run Cors middleware and handle errors. + await new Promise((resolve, reject) => { + cors(req, res, (result) => { + if (result instanceof Error) { + reject(result) + } + + resolve(result) + }) + }) + + res.json(`Hi from Gatsby Functions`) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/dir/[...].js b/plugin/test/fixtures/v5/with-adapters/src/api/dir/[...].js new file mode 100644 index 00000000..e76f3038 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/dir/[...].js @@ -0,0 +1,3 @@ +export default function topLevel(req, res) { + res.json(req.params) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/dir/function.js b/plugin/test/fixtures/v5/with-adapters/src/api/dir/function.js new file mode 100644 index 00000000..b83f7110 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/dir/function.js @@ -0,0 +1,3 @@ +export default function topLevel(req, res) { + res.send(`I am another sub-directory function`) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/env-variables.js b/plugin/test/fixtures/v5/with-adapters/src/api/env-variables.js new file mode 100644 index 00000000..7d09b81f --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/env-variables.js @@ -0,0 +1,3 @@ +export default function topLevel(req, res) { + res.send(process.env.pickle) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/error-send-function-twice.js b/plugin/test/fixtures/v5/with-adapters/src/api/error-send-function-twice.js new file mode 100644 index 00000000..8e5b2582 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/error-send-function-twice.js @@ -0,0 +1,4 @@ +export default function handler(req, res) { + res.send(`hi`) + res.json({ willCauseError: true }) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/hello-ts.ts b/plugin/test/fixtures/v5/with-adapters/src/api/hello-ts.ts new file mode 100644 index 00000000..37f90f86 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/hello-ts.ts @@ -0,0 +1,3 @@ +export default function handler(req, res) { + res.status(200).json({ hello: `world` }) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/hello-world.js b/plugin/test/fixtures/v5/with-adapters/src/api/hello-world.js new file mode 100644 index 00000000..37f90f86 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/hello-world.js @@ -0,0 +1,3 @@ +export default function handler(req, res) { + res.status(200).json({ hello: `world` }) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/i-am-false.js b/plugin/test/fixtures/v5/with-adapters/src/api/i-am-false.js new file mode 100644 index 00000000..fea6703e --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/i-am-false.js @@ -0,0 +1,3 @@ +export default function topLevel(req, res) { + res.send(false) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/i-am-json-too.js b/plugin/test/fixtures/v5/with-adapters/src/api/i-am-json-too.js new file mode 100644 index 00000000..8909f045 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/i-am-json-too.js @@ -0,0 +1,3 @@ +export default function topLevel(req, res) { + res.send({ amIJSON: true }) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/i-am-json.js b/plugin/test/fixtures/v5/with-adapters/src/api/i-am-json.js new file mode 100644 index 00000000..ea5ffd15 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/i-am-json.js @@ -0,0 +1,3 @@ +export default function topLevel(req, res) { + res.json({ amIJSON: true }) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/i-am-status.js b/plugin/test/fixtures/v5/with-adapters/src/api/i-am-status.js new file mode 100644 index 00000000..374ebf13 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/i-am-status.js @@ -0,0 +1,3 @@ +export default function topLevel(req, res) { + res.send(200) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/i-am-text.js b/plugin/test/fixtures/v5/with-adapters/src/api/i-am-text.js new file mode 100644 index 00000000..30b66967 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/i-am-text.js @@ -0,0 +1,3 @@ +export default function topLevel(req, res) { + res.send('I am text') +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/i-am-typescript.ts b/plugin/test/fixtures/v5/with-adapters/src/api/i-am-typescript.ts new file mode 100644 index 00000000..24b16d2a --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/i-am-typescript.ts @@ -0,0 +1,10 @@ +import { GatsbyFunctionResponse, GatsbyFunctionRequest } from 'gatsby' + +export default function topLevel( + req: GatsbyFunctionRequest, + res: GatsbyFunctionResponse, +) { + if (req.method === `GET`) { + res.send(`I am typescript`) + } +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/parser.js b/plugin/test/fixtures/v5/with-adapters/src/api/parser.js new file mode 100644 index 00000000..66e2b10f --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/parser.js @@ -0,0 +1,13 @@ +export default function topLevel(req, res) { + if (req.query && Object.keys(req.query).length !== 0) { + res.json(req.query) + } else if (req.files && req.files.length !== 0) { + res.json({ files: req.files, body: req.body }) + } else if (req.body) { + res.json(req.body) + } else { + res.json({ + message: `No body was sent. Try a POST request or query string`, + }) + } +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/redirect-me.ts b/plugin/test/fixtures/v5/with-adapters/src/api/redirect-me.ts new file mode 100644 index 00000000..b141f7ab --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/redirect-me.ts @@ -0,0 +1,8 @@ +import { GatsbyFunctionResponse, GatsbyFunctionRequest } from 'gatsby' + +export default function topLevel( + req: GatsbyFunctionRequest, + res: GatsbyFunctionResponse, +) { + res.redirect(`/`) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/some whitespace.js b/plugin/test/fixtures/v5/with-adapters/src/api/some whitespace.js new file mode 100644 index 00000000..32d09568 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/some whitespace.js @@ -0,0 +1,4 @@ +const path = require(`path`) +export default function topLevel(req, res) { + res.send(`${path.parse(__filename).name}${path.parse(__filename).ext}`) +} diff --git "a/plugin/test/fixtures/v5/with-adapters/src/api/some-\303\240\303\250-french.js" "b/plugin/test/fixtures/v5/with-adapters/src/api/some-\303\240\303\250-french.js" new file mode 100644 index 00000000..32d09568 --- /dev/null +++ "b/plugin/test/fixtures/v5/with-adapters/src/api/some-\303\240\303\250-french.js" @@ -0,0 +1,4 @@ +const path = require(`path`) +export default function topLevel(req, res) { + res.send(`${path.parse(__filename).name}${path.parse(__filename).ext}`) +} diff --git "a/plugin/test/fixtures/v5/with-adapters/src/api/some-\327\220\327\225\327\223\327\225\327\252.js" "b/plugin/test/fixtures/v5/with-adapters/src/api/some-\327\220\327\225\327\223\327\225\327\252.js" new file mode 100644 index 00000000..32d09568 --- /dev/null +++ "b/plugin/test/fixtures/v5/with-adapters/src/api/some-\327\220\327\225\327\223\327\225\327\252.js" @@ -0,0 +1,4 @@ +const path = require(`path`) +export default function topLevel(req, res) { + res.send(`${path.parse(__filename).name}${path.parse(__filename).ext}`) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/status.js b/plugin/test/fixtures/v5/with-adapters/src/api/status.js new file mode 100644 index 00000000..edbe1f9f --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/status.js @@ -0,0 +1,4 @@ +export default function topLevel(req, res) { + const status = req.query.code ? req.query.code : 200 + res.status(status).send(`I am at the top-level`) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/top-level.js b/plugin/test/fixtures/v5/with-adapters/src/api/top-level.js new file mode 100644 index 00000000..56d7d954 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/top-level.js @@ -0,0 +1,3 @@ +export default function topLevel(req, res) { + res.send(`I am at the top-level`) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/api/users/[userId]/[super].js b/plugin/test/fixtures/v5/with-adapters/src/api/users/[userId]/[super].js new file mode 100644 index 00000000..583a404a --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/api/users/[userId]/[super].js @@ -0,0 +1,3 @@ +export default function userIdHandler(req, res) { + res.json(req.params) +} diff --git "a/plugin/test/fixtures/v5/with-adapters/src/api/with-\303\244\303\266\303\274-umlaut.js" "b/plugin/test/fixtures/v5/with-adapters/src/api/with-\303\244\303\266\303\274-umlaut.js" new file mode 100644 index 00000000..32d09568 --- /dev/null +++ "b/plugin/test/fixtures/v5/with-adapters/src/api/with-\303\244\303\266\303\274-umlaut.js" @@ -0,0 +1,4 @@ +const path = require(`path`) +export default function topLevel(req, res) { + res.send(`${path.parse(__filename).name}${path.parse(__filename).ext}`) +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/images/icon.png b/plugin/test/fixtures/v5/with-adapters/src/images/icon.png new file mode 100644 index 00000000..38b2fb0e Binary files /dev/null and b/plugin/test/fixtures/v5/with-adapters/src/images/icon.png differ diff --git a/plugin/test/fixtures/v5/with-adapters/src/pages/404.js b/plugin/test/fixtures/v5/with-adapters/src/pages/404.js new file mode 100644 index 00000000..0697ec5a --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/pages/404.js @@ -0,0 +1,52 @@ +import { Link } from 'gatsby' +import * as React from 'react' + +// styles +const pageStyles = { + color: '#232129', + padding: '96px', + fontFamily: '-apple-system, Roboto, sans-serif, serif', +} +const headingStyles = { + marginTop: 0, + marginBottom: 64, + maxWidth: 320, +} + +const paragraphStyles = { + marginBottom: 48, +} +const codeStyles = { + color: '#8A6534', + padding: 4, + backgroundColor: '#FFF4DB', + fontSize: '1.25rem', + borderRadius: 4, +} + +// markup +const NotFoundPage = () => ( +
+ Not found +

Page not found

+

+ Sorry{' '} + + 😔 + {' '} + we couldn’t find what you were looking for. +
+ {process.env.NODE_ENV === 'development' ? ( + <> +
+ Try creating a page in src/pages/. +
+ + ) : null} +
+ Go home. +

+
+) + +export default NotFoundPage diff --git a/plugin/test/fixtures/v5/with-adapters/src/pages/dsg.js b/plugin/test/fixtures/v5/with-adapters/src/pages/dsg.js new file mode 100644 index 00000000..7602d89b --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/pages/dsg.js @@ -0,0 +1,182 @@ +import * as React from 'react' + +// styles +const pageStyles = { + color: '#232129', + paddingLeft: 60, + fontFamily: '-apple-system, Roboto, sans-serif, serif', +} + +const listStyles = { + marginBottom: 96, + paddingLeft: 0, +} +const listItemStyles = { + fontWeight: 300, + fontSize: 24, + maxWidth: 560, + marginBottom: 10, +} + +const linkStyle = { + color: '#8954A8', + fontWeight: 'bold', + fontSize: 16, + verticalAlign: '5%', +} + +const descriptionStyle = { + color: '#232129', + fontSize: 14, + marginTop: 10, + marginBottom: 0, + lineHeight: 1.25, +} + +// data +const links = [ + { text: 'Hello World', url: 'api/hello-world', description: '' }, + { + text: 'I Am Capitalized', + url: 'api/I-Am-Capitalized', + description: 'Shows case-sensitive URLs', + }, + { + text: 'Cookies', + url: 'api/cookie-me', + description: 'Reads browser cookies', + }, + { text: 'Cors', url: 'api/cors', description: 'Uses custom middleware' }, + { + text: 'env var', + url: 'api/env-variables', + description: 'Reads .env var from build', + }, + { + text: 'Error caught', + url: 'api/error-send-function-twice', + description: "Doesn't crash the server on error", + }, + { + text: 'JSON output', + url: 'api/i-am-json', + description: 'Uses json() helper', + }, + { + text: 'I am TypeScript', + url: 'api/i-am-typescript', + description: 'Is a TypeScript function', + }, + { + text: 'Value parser', + url: 'api/parser?message=These are query params&another=And so is this&hint=Try a form or JSON body POST', + description: 'Parses body. POST to me', + }, + { + text: 'Redirect', + url: 'api/redirect-me', + description: 'Redirects back to this page', + }, + { text: 'Whitespace in URL', url: 'api/some whitespace' }, + { text: 'Accented characters', url: 'api/some-àè-french' }, + { text: 'Non-latin characters', url: 'api/some-אודות' }, + { + text: 'Status', + url: 'api/status?code=418', + description: 'Sets status code using status() helper', + }, + { text: 'Directory index', url: 'api/a-directory' }, + { + text: 'Directory subpage', + url: 'api/a-directory/function', + }, + { + text: 'Directory catch-all', + url: 'api/dir/anything-here', + description: 'Change the catch-all value and see captured value', + }, + { + text: 'Directory catch-all override', + url: 'api/dir/function', + description: 'A named function overrides the catch-all', + }, + { + text: 'Named params', + url: 'api/users/123/hello world', + description: 'Captures named path params', + }, +] + +// markup +const IndexPage = () => { + React.useEffect(() => { + document.cookie = 'thiscookie=was%20set%20on%20previous%20page' + }) + return ( +
+ Home Page +

Gatsby Functions demo

+ + Gatsby G Logo +
+ ) +} + +export default IndexPage + +export function config() { + return () => { + return { + defer: true, + } + } +} diff --git a/plugin/test/fixtures/v5/with-adapters/src/pages/index.js b/plugin/test/fixtures/v5/with-adapters/src/pages/index.js new file mode 100644 index 00000000..75dcc278 --- /dev/null +++ b/plugin/test/fixtures/v5/with-adapters/src/pages/index.js @@ -0,0 +1,174 @@ +import * as React from 'react' + +// styles +const pageStyles = { + color: '#232129', + paddingLeft: 60, + fontFamily: '-apple-system, Roboto, sans-serif, serif', +} + +const listStyles = { + marginBottom: 96, + paddingLeft: 0, +} +const listItemStyles = { + fontWeight: 300, + fontSize: 24, + maxWidth: 560, + marginBottom: 10, +} + +const linkStyle = { + color: '#8954A8', + fontWeight: 'bold', + fontSize: 16, + verticalAlign: '5%', +} + +const descriptionStyle = { + color: '#232129', + fontSize: 14, + marginTop: 10, + marginBottom: 0, + lineHeight: 1.25, +} + +// data +const links = [ + { text: 'Hello World', url: 'api/hello-world', description: '' }, + { + text: 'I Am Capitalized', + url: 'api/I-Am-Capitalized', + description: 'Shows case-sensitive URLs', + }, + { + text: 'Cookies', + url: 'api/cookie-me', + description: 'Reads browser cookies', + }, + { text: 'Cors', url: 'api/cors', description: 'Uses custom middleware' }, + { + text: 'env var', + url: 'api/env-variables', + description: 'Reads .env var from build', + }, + { + text: 'Error caught', + url: 'api/error-send-function-twice', + description: "Doesn't crash the server on error", + }, + { + text: 'JSON output', + url: 'api/i-am-json', + description: 'Uses json() helper', + }, + { + text: 'I am TypeScript', + url: 'api/i-am-typescript', + description: 'Is a TypeScript function', + }, + { + text: 'Value parser', + url: 'api/parser?message=These are query params&another=And so is this&hint=Try a form or JSON body POST', + description: 'Parses body. POST to me', + }, + { + text: 'Redirect', + url: 'api/redirect-me', + description: 'Redirects back to this page', + }, + { text: 'Whitespace in URL', url: 'api/some whitespace' }, + { text: 'Accented characters', url: 'api/some-àè-french' }, + { text: 'Non-latin characters', url: 'api/some-אודות' }, + { + text: 'Status', + url: 'api/status?code=418', + description: 'Sets status code using status() helper', + }, + { text: 'Directory index', url: 'api/a-directory' }, + { + text: 'Directory subpage', + url: 'api/a-directory/function', + }, + { + text: 'Directory catch-all', + url: 'api/dir/anything-here', + description: 'Change the catch-all value and see captured value', + }, + { + text: 'Directory catch-all override', + url: 'api/dir/function', + description: 'A named function overrides the catch-all', + }, + { + text: 'Named params', + url: 'api/users/123/hello world', + description: 'Captures named path params', + }, +] + +// markup +const IndexPage = () => { + React.useEffect(() => { + document.cookie = 'thiscookie=was%20set%20on%20previous%20page' + }) + return ( +
+ Home Page +

Gatsby Functions demo

+ + Gatsby G Logo +
+ ) +} + +export default IndexPage diff --git a/plugin/test/unit/helpers/config.spec.ts b/plugin/test/unit/helpers/config.spec.ts index f8ade095..5436bea7 100644 --- a/plugin/test/unit/helpers/config.spec.ts +++ b/plugin/test/unit/helpers/config.spec.ts @@ -11,7 +11,10 @@ import { createMetadataFileAndCopyDatastore, mutateConfig, shouldSkipBundlingDatastore, + shouldSkip, } from '../../../src/helpers/config' +// eslint-disable-next-line import/no-namespace +import * as filesModule from '../../../src/helpers/files' import { enableGatsbyExcludeDatastoreFromBundle } from '../../helpers' const chance = new Chance() @@ -272,4 +275,61 @@ describe('createMetadataFileAndCopyDatastore', () => { TEST_TIMEOUT, ) }) + +describe('shouldSkip', () => { + let findModuleFromBaseSpy + const publishDir = `/opt/repo/public` + const gatsbyPackageJsonPath = `/opt/public/node_modules/gatsby/package.json` + const gatsbyPluginUtilsPath = `/opt/public/node_modules/gatsby-plugin-utils/dist/index.js` + let mockHasAdaptersFeature + beforeAll(() => { + findModuleFromBaseSpy = jest + .spyOn(filesModule, 'findModuleFromBase') + // eslint-disable-next-line max-nested-callbacks + .mockImplementation(({ candidates }) => { + if (candidates.includes(`gatsby/package.json`)) { + return gatsbyPackageJsonPath + } + + if (candidates.includes(`gatsby-plugin-utils`)) { + return gatsbyPluginUtilsPath + } + + return null + }) + jest.mock( + gatsbyPluginUtilsPath, + // eslint-disable-next-line max-nested-callbacks + () => ({ + // eslint-disable-next-line max-nested-callbacks + hasFeature: (feat) => { + if (feat === `adapters`) { + return mockHasAdaptersFeature + } + return false + }, + }), + { + virtual: true, + }, + ) + }) + afterEach(() => { + findModuleFromBaseSpy.mockClear() + delete process.env.NETLIFY_SKIP_GATSBY_BUILD_PLUGIN + }) + afterAll(() => { + findModuleFromBaseSpy.mockReset() + }) + + it(`doesn't skip when gatsby version supports adapters`, () => { + mockHasAdaptersFeature = false + expect(shouldSkip(publishDir)).toBe(false) + }) + + it(`does skip when gatsby version doesn't support adapters`, () => { + mockHasAdaptersFeature = true + expect(shouldSkip(publishDir)).toBe(true) + }) +}) /* eslint-enable ava/no-import-test-files */ diff --git a/plugin/test/unit/index.spec.ts b/plugin/test/unit/index.spec.ts index 6330da05..c072a88b 100644 --- a/plugin/test/unit/index.spec.ts +++ b/plugin/test/unit/index.spec.ts @@ -223,7 +223,7 @@ describe('plugin', () => { }) it('makes requests to pre-warm the lambdas if GATSBY_EXCLUDE_DATASTORE_FROM_BUNDLE is enabled', async () => { - await onSuccess() + await onSuccess(defaultArgs) const controller = new AbortController() expect(fetch).toHaveBeenNthCalledWith( 1, @@ -245,7 +245,7 @@ describe('plugin', () => { it('does not make requests to pre-warm the lambdas if GATSBY_EXCLUDE_DATASTORE_FROM_BUNDLE is disabled', async () => { process.env.GATSBY_EXCLUDE_DATASTORE_FROM_BUNDLE = 'false' - await onSuccess() + await onSuccess(defaultArgs) expect(fetch).toBeCalledTimes(0) }) @@ -253,7 +253,7 @@ describe('plugin', () => { it('does not make requests to pre-warm the lambdas if process.env.GATSBY_EXCLUDE_DATASTORE_FROM_BUNDLE is not defined', async () => { delete process.env.GATSBY_EXCLUDE_DATASTORE_FROM_BUNDLE - await onSuccess() + await onSuccess(defaultArgs) expect(fetch).toBeCalledTimes(0) }) @@ -261,7 +261,7 @@ describe('plugin', () => { it('does not make requests to pre-warm the lambdas if process.env.DEPLOY_PRIME_URL is not defined', async () => { delete process.env.DEPLOY_PRIME_URL - await onSuccess() + await onSuccess(defaultArgs) expect(fetch).toBeCalledTimes(0) })