diff --git a/e2e-tests/development-runtime/.gitignore b/e2e-tests/development-runtime/.gitignore
index 3cbf65216661d..b98e14052be21 100644
--- a/e2e-tests/development-runtime/.gitignore
+++ b/e2e-tests/development-runtime/.gitignore
@@ -76,3 +76,6 @@ cypress/videos
__history__.json
src/gatsby-types.d.ts
+/test-results/
+/playwright-report/
+/playwright/.cache/
diff --git a/e2e-tests/development-runtime/cypress/integration/gatsby-script/gatsby-script-inline-scripts.js b/e2e-tests/development-runtime/cypress/integration/gatsby-script/gatsby-script-inline-scripts.js
new file mode 100644
index 0000000000000..c729ae2cead10
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/gatsby-script/gatsby-script-inline-scripts.js
@@ -0,0 +1,195 @@
+import { inlineScript } from "../../../gatsby-script-scripts"
+import { resourceRecord, markRecord } from "../../../gatsby-script-records"
+
+const page = {
+ target: `/gatsby-script-inline-scripts/`,
+ navigation: `/gatsby-script-navigation/`,
+}
+
+const typesOfInlineScripts = [
+ {
+ descriptor: `dangerouslySetInnerHTML`,
+ inlineScriptType: inlineScript.dangerouslySet,
+ },
+ {
+ descriptor: `template literals`,
+ inlineScriptType: inlineScript.templateLiteral,
+ },
+]
+
+/**
+ * Normally we would duplicate the tests so they're flatter and easier to debug,
+ * but since the test count grew and the cases are exactly the same we'll iterate.
+ */
+
+for (const { descriptor, inlineScriptType } of typesOfInlineScripts) {
+ describe(`inline scripts set via ${descriptor}`, () => {
+ describe(`using the post-hydrate strategy`, () => {
+ it(`should execute successfully`, () => {
+ cy.visit(page.target)
+
+ cy.getRecord(
+ `post-hydrate-${inlineScriptType}`,
+ `success`,
+ true
+ ).should(`equal`, `true`)
+ })
+
+ it(`should load after the framework bundle has loaded`, () => {
+ cy.visit(page.target)
+
+ // Assert framework is loaded before inline script is executed
+ cy.getRecord(
+ `post-hydrate-${inlineScriptType}`,
+ markRecord.executeStart
+ ).then(dangerouslySetExecuteStart => {
+ cy.getRecord(`framework`, resourceRecord.responseEnd).should(
+ `be.lessThan`,
+ dangerouslySetExecuteStart
+ )
+ })
+ })
+ })
+
+ describe(`using the idle strategy`, () => {
+ it(`should execute successfully`, () => {
+ cy.visit(page.target)
+
+ cy.getRecord(`idle-${inlineScriptType}`, `success`, true).should(
+ `equal`,
+ `true`
+ )
+ })
+
+ it(`should load after other strategies`, () => {
+ cy.visit(page.target)
+
+ cy.getRecord(`idle-${inlineScriptType}`, markRecord.executeStart).then(
+ dangerouslySetExecuteStart => {
+ cy.getRecord(
+ `post-hydrate-${inlineScriptType}`,
+ markRecord.executeStart
+ ).should(`be.lessThan`, dangerouslySetExecuteStart)
+ }
+ )
+ })
+ })
+
+ describe(`when navigation occurs`, () => {
+ it(`should load only once on initial page load`, () => {
+ cy.visit(page.target)
+
+ cy.get(`table[id=script-mark-records] tbody`)
+ .children()
+ .should(`have.length`, 4)
+ cy.getRecord(
+ `post-hydrate-${inlineScriptType}`,
+ `strategy`,
+ true
+ ).should(`equal`, `post-hydrate`)
+ cy.getRecord(`idle-${inlineScriptType}`, `strategy`, true).should(
+ `equal`,
+ `idle`
+ )
+ })
+
+ it(`should load only once after the page is refreshed`, () => {
+ cy.visit(page.target).waitForRouteChange()
+ cy.reload().url().should(`contain`, page.target)
+
+ cy.get(`table[id=script-mark-records] tbody`)
+ .children()
+ .should(`have.length`, 4)
+ cy.getRecord(
+ `post-hydrate-${inlineScriptType}`,
+ `strategy`,
+ true
+ ).should(`equal`, `post-hydrate`)
+ cy.getRecord(`idle-${inlineScriptType}`, `strategy`, true).should(
+ `equal`,
+ `idle`
+ )
+ })
+
+ it(`should load only once after anchor link navigation`, () => {
+ cy.visit(page.target)
+ cy.get(`a[id=anchor-link-back-to-index]`).click()
+ cy.url().should(`contain`, page.navigation)
+ cy.get(`a[href="${page.target}"][id=anchor-link]`).click()
+
+ cy.get(`table[id=script-mark-records] tbody`)
+ .children()
+ .should(`have.length`, 4)
+ cy.getRecord(
+ `post-hydrate-${inlineScriptType}`,
+ `strategy`,
+ true
+ ).should(`equal`, `post-hydrate`)
+ cy.getRecord(`idle-${inlineScriptType}`, `strategy`, true).should(
+ `equal`,
+ `idle`
+ )
+ })
+
+ it(`should load only once if the page is revisited via browser back/forward buttons after anchor link navigation`, () => {
+ cy.visit(page.navigation).waitForRouteChange()
+ cy.get(`a[href="${page.target}"][id=anchor-link]`).click()
+ cy.get(`table[id=script-mark-records] tbody`) // Make sure history has time to change
+ cy.go(`back`)
+ cy.go(`forward`)
+
+ cy.get(`table[id=script-mark-records] tbody`)
+ .children()
+ .should(`have.length`, 4)
+ cy.getRecord(
+ `post-hydrate-${inlineScriptType}`,
+ `strategy`,
+ true
+ ).should(`equal`, `post-hydrate`)
+ cy.getRecord(`idle-${inlineScriptType}`, `strategy`, true).should(
+ `equal`,
+ `idle`
+ )
+ })
+
+ it(`should load only once after Gatsby link navigation`, () => {
+ cy.visit(page.target)
+ cy.get(`a[id=gatsby-link-back-to-index]`).click()
+ cy.get(`a[href="${page.target}"][id=gatsby-link]`).click()
+
+ cy.get(`table[id=script-mark-records] tbody`)
+ .children()
+ .should(`have.length`, 4)
+ cy.getRecord(
+ `post-hydrate-${inlineScriptType}`,
+ `strategy`,
+ true
+ ).should(`equal`, `post-hydrate`)
+ cy.getRecord(`idle-${inlineScriptType}`, `strategy`, true).should(
+ `equal`,
+ `idle`
+ )
+ })
+
+ it(`should load only once if the page is revisited via browser back/forward buttons after Gatsby link navigation`, () => {
+ cy.visit(page.navigation)
+ cy.get(`a[href="${page.target}"][id=gatsby-link]`).click()
+ cy.go(`back`)
+ cy.go(`forward`)
+
+ cy.get(`table[id=script-mark-records] tbody`)
+ .children()
+ .should(`have.length`, 4)
+ cy.getRecord(
+ `post-hydrate-${inlineScriptType}`,
+ `strategy`,
+ true
+ ).should(`equal`, `post-hydrate`)
+ cy.getRecord(`idle-${inlineScriptType}`, `strategy`, true).should(
+ `equal`,
+ `idle`
+ )
+ })
+ })
+ })
+}
diff --git a/e2e-tests/development-runtime/cypress/integration/gatsby-script/gatsby-script-scripts-with-sources.js b/e2e-tests/development-runtime/cypress/integration/gatsby-script/gatsby-script-scripts-with-sources.js
new file mode 100644
index 0000000000000..b087868bd8907
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/gatsby-script/gatsby-script-scripts-with-sources.js
@@ -0,0 +1,166 @@
+import { script } from "../../../gatsby-script-scripts"
+import { resourceRecord } from "../../../gatsby-script-records"
+
+const page = {
+ target: `/gatsby-script-scripts-with-sources/`,
+ navigation: `/gatsby-script-navigation/`,
+}
+
+describe(`scripts with sources`, () => {
+ describe(`using the post-hydrate strategy`, () => {
+ it(`should load successfully`, () => {
+ cy.visit(page.target)
+ cy.getRecord(script.three, `success`, true).should(`equal`, `true`)
+ })
+
+ it(`should load after the framework bundle has loaded`, () => {
+ cy.visit(page.target)
+
+ // Assert framework is loaded before three starts loading
+ cy.getRecord(script.three, resourceRecord.fetchStart).then(
+ threeFetchStart => {
+ cy.getRecord(`framework`, resourceRecord.responseEnd).should(
+ `be.lessThan`,
+ threeFetchStart
+ )
+ }
+ )
+ })
+
+ it(`should call an on load callback once the script has loaded`, () => {
+ cy.visit(page.target)
+ cy.getRecord(script.three, resourceRecord.responseEnd).then(() => {
+ cy.get(`[data-on-load-result=post-hydrate]`)
+ })
+ })
+
+ it(`should call an on error callback if an error occurred`, () => {
+ cy.visit(page.target)
+ cy.get(`[data-on-error-result=post-hydrate]`)
+ })
+ })
+
+ describe(`using the idle strategy`, () => {
+ it(`should load successfully`, () => {
+ cy.visit(page.target)
+ cy.getRecord(script.marked, `success`, true).should(`equal`, `true`)
+ })
+
+ it(`should load after other strategies`, () => {
+ cy.visit(page.target)
+
+ cy.getRecord(script.marked, resourceRecord.fetchStart).then(
+ markedFetchStart => {
+ cy.getRecord(script.three, resourceRecord.fetchStart).should(
+ `be.lessThan`,
+ markedFetchStart
+ )
+ }
+ )
+ })
+
+ it(`should call an on load callback once the script has loaded`, () => {
+ cy.visit(page.target)
+ cy.getRecord(script.marked, resourceRecord.responseEnd).then(() => {
+ cy.get(`[data-on-load-result=idle]`)
+ })
+ })
+
+ it(`should call an on error callback if an error occurred`, () => {
+ cy.visit(page.target)
+ cy.get(`[data-on-error-result=idle]`)
+ })
+ })
+
+ describe(`when navigation occurs`, () => {
+ it(`should load only once on initial page load`, () => {
+ cy.visit(page.target)
+
+ cy.get(`table[id=script-resource-records] tbody`)
+ .children()
+ .should(`have.length`, 5)
+ cy.getRecord(script.three, `strategy`, true).should(
+ `equal`,
+ `post-hydrate`
+ )
+ cy.getRecord(script.marked, `strategy`, true).should(`equal`, `idle`)
+ })
+
+ it(`should load only once after the page is refreshed`, () => {
+ cy.visit(page.target)
+ cy.reload()
+
+ cy.get(`table[id=script-resource-records] tbody`)
+ .children()
+ .should(`have.length`, 5)
+ cy.getRecord(script.three, `strategy`, true).should(
+ `equal`,
+ `post-hydrate`
+ )
+ cy.getRecord(script.marked, `strategy`, true).should(`equal`, `idle`)
+ })
+
+ it(`should load only once after anchor link navigation`, () => {
+ cy.visit(page.target)
+ cy.get(`a[id=anchor-link-back-to-index]`).click()
+ cy.url().should(`contain`, page.navigation)
+ cy.get(`a[href="${page.target}"][id=anchor-link]`).click()
+
+ cy.get(`table[id=script-resource-records] tbody`)
+ .children()
+ .should(`have.length`, 5)
+ cy.getRecord(script.three, `strategy`, true).should(
+ `equal`,
+ `post-hydrate`
+ )
+ cy.getRecord(script.marked, `strategy`, true).should(`equal`, `idle`)
+ })
+
+ it(`should load only once if the page is revisited via browser back/forward buttons after anchor link navigation`, () => {
+ cy.visit(page.navigation)
+ cy.get(`a[href="${page.target}"][id=anchor-link]`).click()
+ cy.go(`back`)
+ cy.go(`forward`)
+
+ cy.get(`table[id=script-resource-records] tbody`)
+ .children()
+ .should(`have.length`, 5)
+ cy.getRecord(script.three, `strategy`, true).should(
+ `equal`,
+ `post-hydrate`
+ )
+ cy.getRecord(script.marked, `strategy`, true).should(`equal`, `idle`)
+ })
+
+ it(`should load only once after Gatsby link navigation`, () => {
+ cy.visit(page.target)
+ cy.get(`a[id=gatsby-link-back-to-index]`).click()
+ cy.get(`a[href="${page.target}"][id=gatsby-link]`).click()
+
+ cy.get(`table[id=script-resource-records] tbody`)
+ .children()
+ .should(`have.length`, 5)
+ cy.getRecord(script.three, `strategy`, true).should(
+ `equal`,
+ `post-hydrate`
+ )
+ cy.getRecord(script.marked, `strategy`, true).should(`equal`, `idle`)
+ })
+
+ it(`should load only once if the page is revisited via browser back/forward buttons after Gatsby link navigation`, () => {
+ cy.visit(page.navigation)
+ cy.get(`a[href="${page.target}"][id=gatsby-link]`).click()
+ cy.go(`back`)
+ cy.go(`forward`)
+
+ cy.get(`table[id=script-resource-records] tbody`)
+ .children()
+ .should(`have.length`, 5)
+ cy.getRecord(script.three, `strategy`, true).should(
+ `equal`,
+ `post-hydrate`
+ )
+ cy.getRecord(script.marked, `strategy`, true).should(`equal`, `idle`)
+ })
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/integration/gatsby-script/gatsby-script-shimmed-req-idle-callback.js b/e2e-tests/development-runtime/cypress/integration/gatsby-script/gatsby-script-shimmed-req-idle-callback.js
new file mode 100644
index 0000000000000..bae5d0b841110
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/gatsby-script/gatsby-script-shimmed-req-idle-callback.js
@@ -0,0 +1,52 @@
+import { script } from "../../../gatsby-script-scripts"
+import { resourceRecord } from "../../../gatsby-script-records"
+
+// The page that we will assert against
+const page = `/gatsby-script-scripts-with-sources`
+
+Cypress.on(`window:before:load`, win => {
+ cy.spy(win, `requestIdleCallback`).as(`requestIdleCallback`)
+ win.requestIdleCallback = undefined
+})
+
+/*
+ * Some browsers don't support the requestIdleCallback API, so we need to
+ * shim it. Here we test that the idle behaviour remains the same with shimmed requestIdleCallback
+ */
+describe(`using the idle strategy with shimmed requestIdleCallback`, () => {
+ it(`should load successfully`, () => {
+ cy.visit(page).waitForRouteChange()
+ cy.getRecord(script.marked, `success`, true).should(`equal`, `true`)
+
+ cy.get(`@requestIdleCallback`).should(`not.be.called`)
+ })
+
+ it(`should load after other strategies`, () => {
+ cy.visit(page).waitForRouteChange()
+
+ cy.getRecord(script.marked, resourceRecord.fetchStart).then(
+ markedFetchStart => {
+ cy.getRecord(script.three, resourceRecord.fetchStart).should(
+ `be.lessThan`,
+ markedFetchStart
+ )
+ }
+ )
+ cy.get(`@requestIdleCallback`).should(`not.be.called`)
+ })
+
+ it(`should call an on load callback once the script has loaded`, () => {
+ cy.visit(page).waitForRouteChange()
+ cy.getRecord(script.marked, resourceRecord.responseEnd).then(() => {
+ cy.get(`[data-on-load-result=idle]`)
+ })
+ cy.get(`@requestIdleCallback`).should(`not.be.called`)
+ })
+
+ it(`should call an on error callback if an error occurred`, () => {
+ cy.visit(page).waitForRouteChange()
+ cy.get(`[data-on-error-result=idle]`)
+
+ cy.get(`@requestIdleCallback`).should(`not.be.called`)
+ })
+})
diff --git a/e2e-tests/development-runtime/cypress/integration/gatsby-script/gatsby-script-ssr-browser-apis.js b/e2e-tests/development-runtime/cypress/integration/gatsby-script/gatsby-script-ssr-browser-apis.js
new file mode 100644
index 0000000000000..2a470190bd8f7
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/gatsby-script/gatsby-script-ssr-browser-apis.js
@@ -0,0 +1,15 @@
+import { script } from "../../../gatsby-script-scripts"
+
+const page = `/gatsby-script-ssr-browser-apis/`
+
+it(`scripts load successfully when used via wrapPageElement`, () => {
+ cy.visit(page)
+ cy.getRecord(script.three, `success`, true).should(`equal`, `true`)
+ cy.getRecord(script.marked, `success`, true).should(`equal`, `true`)
+})
+
+it(`scripts load successfully when used via wrapRootElement`, () => {
+ cy.visit(page)
+ cy.getRecord(script.jQuery, `success`, true).should(`equal`, `true`)
+ cy.getRecord(script.popper, `success`, true).should(`equal`, `true`)
+})
diff --git a/e2e-tests/development-runtime/cypress/support/commands.js b/e2e-tests/development-runtime/cypress/support/commands.js
index bb6981c2e5d8d..947d235eec469 100644
--- a/e2e-tests/development-runtime/cypress/support/commands.js
+++ b/e2e-tests/development-runtime/cypress/support/commands.js
@@ -108,13 +108,13 @@ Cypress.Commands.add(`waitForHmr`, (message = `App is up to date`) => {
cy.wait(1000)
})
-Cypress.Commands.add(`getFastRefreshOverlay`, () => (
- cy.get('gatsby-fast-refresh').shadow()
-))
+Cypress.Commands.add(`getFastRefreshOverlay`, () =>
+ cy.get(`gatsby-fast-refresh`).shadow()
+)
-Cypress.Commands.add(`assertNoFastRefreshOverlay`, () => (
- cy.get('gatsby-fast-refresh').should('not.exist')
-))
+Cypress.Commands.add(`assertNoFastRefreshOverlay`, () =>
+ cy.get(`gatsby-fast-refresh`).should(`not.exist`)
+)
addMatchImageSnapshotCommand({
customDiffDir: `/__diff_output__`,
@@ -124,3 +124,15 @@ addMatchImageSnapshotCommand({
failureThreshold: 0.08,
failureThresholdType: `percent`,
})
+
+/**
+ * Get a record from a table cell in one of the test components.
+ * @example cy.getRecord(Script.dayjs, ResourceRecord.fetchStart)
+ * @example cy.getRecord(`${ScriptStrategy.preHydrate}-${InlineScript.dangerouslySet}`, MarkRecord.executeStart)
+ */
+Cypress.Commands.add(`getRecord`, (key, metric, raw = false) => {
+ return cy
+ .get(`[id=${key}] [id=${metric}]`)
+ .invoke(`text`)
+ .then(value => (raw ? value : Number(value)))
+})
diff --git a/e2e-tests/development-runtime/gatsby-browser.js b/e2e-tests/development-runtime/gatsby-browser.js
index d8a65b3cc6595..1e783bea454bd 100644
--- a/e2e-tests/development-runtime/gatsby-browser.js
+++ b/e2e-tests/development-runtime/gatsby-browser.js
@@ -1,3 +1,4 @@
+import WrapPageElement from "./src/wrap-page-element"
import WrapRootElement from "./src/wrap-root-element"
import * as React from "react"
@@ -23,6 +24,10 @@ export const onPrefetchPathname = ({ pathname }) => {
addLogEntry(`onPrefetchPathname`, pathname)
}
+export const wrapPageElement = ({ element, props }) => (
+
+)
+
export const wrapRootElement = ({ element }) => (
)
diff --git a/e2e-tests/development-runtime/gatsby-config.js b/e2e-tests/development-runtime/gatsby-config.js
index 9ccc5806d1583..13d43473d4f25 100644
--- a/e2e-tests/development-runtime/gatsby-config.js
+++ b/e2e-tests/development-runtime/gatsby-config.js
@@ -36,10 +36,7 @@ module.exports = {
{
resolve: `gatsby-transformer-remark`,
options: {
- plugins: [
- `gatsby-remark-subcache`,
- `gatsby-remark-images`
- ],
+ plugins: [`gatsby-remark-subcache`, `gatsby-remark-images`],
},
},
`gatsby-plugin-sharp`,
@@ -63,4 +60,5 @@ module.exports = {
// To learn more, visit: https://gatsby.dev/offline
// 'gatsby-plugin-offline',
],
+ partytownProxiedURLs: [`https://unpkg.com/three@0.139.1/build/three.js`],
}
diff --git a/e2e-tests/development-runtime/gatsby-script-records.js b/e2e-tests/development-runtime/gatsby-script-records.js
new file mode 100644
index 0000000000000..fff69c671fbcc
--- /dev/null
+++ b/e2e-tests/development-runtime/gatsby-script-records.js
@@ -0,0 +1,12 @@
+/**
+ * Naming matches `PerformanceResourceTiming` interface.
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming}
+ */
+export const resourceRecord = {
+ fetchStart: `fetch-start`,
+ responseEnd: `response-end`,
+}
+
+export const markRecord = {
+ executeStart: `execute-start`,
+}
diff --git a/e2e-tests/development-runtime/gatsby-script-scripts.js b/e2e-tests/development-runtime/gatsby-script-scripts.js
new file mode 100644
index 0000000000000..d206c8642fb66
--- /dev/null
+++ b/e2e-tests/development-runtime/gatsby-script-scripts.js
@@ -0,0 +1,68 @@
+export const script = {
+ three: `three`,
+ marked: `marked`,
+ jQuery: `jQuery`,
+ popper: `popper`,
+}
+
+export const scripts = {
+ [script.three]: `https://unpkg.com/three@0.139.1/build/three.js`,
+ [script.marked]: `https://cdn.jsdelivr.net/npm/marked/marked.min.js`,
+ [script.jQuery]: `https://code.jquery.com/jquery-3.4.1.min.js`,
+ [script.popper]: `https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js`,
+}
+
+export const scriptStrategyIndex = {
+ [script.three]: `post-hydrate`,
+ [script.marked]: `idle`,
+ [script.jQuery]: `post-hydrate`,
+ [script.popper]: `idle`,
+}
+
+export const scriptSuccessIndex = {
+ // @ts-ignore
+ [script.three]: () => typeof THREE === `object`,
+ // @ts-ignore
+ [script.marked]: () => typeof marked === `object`,
+ // @ts-ignore
+ [script.jQuery]: () => typeof jQuery === `function`,
+ // @ts-ignore
+ [script.popper]: () => typeof Popper === `function`,
+}
+
+export const scriptUrlIndex = {
+ [scripts.three]: script.three,
+ [scripts.marked]: script.marked,
+ [scripts.jQuery]: script.jQuery,
+ [scripts.popper]: script.popper,
+}
+
+export const scriptUrls = new Set(Object.values(scripts))
+
+export const inlineScript = {
+ dangerouslySet: `dangerously-set`,
+ templateLiteral: `template-literal`,
+}
+
+// Create an object literal instead of iterating so the contents are explicit
+export const inlineScripts = {
+ "dangerously-set": {
+ "post-hydrate": constructInlineScript(`dangerously-set`, `post-hydrate`),
+ idle: constructInlineScript(`dangerously-set`, `idle`),
+ },
+ "template-literal": {
+ "post-hydrate": constructInlineScript(`template-literal`, `post-hydrate`),
+ idle: constructInlineScript(`template-literal`, `idle`),
+ },
+}
+
+function constructInlineScript(type, strategy) {
+ return `
+ performance.mark(\`inline-script\`, { detail: {
+ strategy: \`${strategy}\`,
+ type: \`${type}\`,
+ executeStart: performance.now()
+ }})
+ window[\`${strategy}-${type}\`] = true;
+ `
+}
diff --git a/e2e-tests/development-runtime/package.json b/e2e-tests/development-runtime/package.json
index 3a917d6370960..0b9eda2fe591d 100644
--- a/e2e-tests/development-runtime/package.json
+++ b/e2e-tests/development-runtime/package.json
@@ -49,12 +49,19 @@
"update:webhook": "node scripts/webhook.js",
"update:cms-webhook": "node scripts/cms-webhook.js",
"update:preview": "curl -X POST -d \"{ \\\"fake-data-update\\\": true }\" -H \"Content-Type: application/json\" http://localhost:8000/__refresh",
- "start-server-and-test": "start-server-and-test develop http://localhost:8000 cy:run",
- "start-server-and-debug": "start-server-and-test develop http://localhost:8000 cy:open",
+ "start-server-and-test": "start-server-and-test develop http://localhost:8000 combined",
+ "start-server-and-test:locally": "start-server-and-test develop http://localhost:8000 cy:open",
"cy:open": "cypress open",
- "cy:run": "node ../../scripts/cypress-run-with-conditional-record-flag.js --browser chrome"
+ "cy:run": "node ../../scripts/cypress-run-with-conditional-record-flag.js --browser chrome",
+ "playwright": "playwright test --project=chromium",
+ "playwright:debug": "playwright test --project=chromium --debug",
+ "start-server-and-test:playwright": "start-server-and-test develop http://localhost:8000 playwright",
+ "start-server-and-test:playwright-debug": "start-server-and-test develop http://localhost:8000 playwright:debug",
+ "combined": "npm run playwright && npm run cy:run",
+ "postinstall": "playwright install chromium"
},
"devDependencies": {
+ "@playwright/test": "^1.22.0",
"@testing-library/cypress": "^7.0.0",
"cross-env": "^5.2.0",
"cypress": "6.1.0",
diff --git a/e2e-tests/development-runtime/playwright.config.ts b/e2e-tests/development-runtime/playwright.config.ts
new file mode 100644
index 0000000000000..ddaffc935a26d
--- /dev/null
+++ b/e2e-tests/development-runtime/playwright.config.ts
@@ -0,0 +1,57 @@
+import type { PlaywrightTestConfig } from "@playwright/test"
+import { devices } from "@playwright/test"
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+const config: PlaywrightTestConfig = {
+ testDir: "./playwright",
+ /* Maximum time one test can run for. */
+ timeout: 30 * 1000,
+ expect: {
+ /**
+ * Maximum time expect() should wait for the condition to be met.
+ * For example in `await expect(locator).toHaveText();`
+ */
+ timeout: 5000,
+ },
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: "html",
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
+ actionTimeout: 0,
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: "http://localhost:8000",
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: "on-first-retry",
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: "chromium",
+ use: {
+ ...devices["Desktop Chrome"],
+ },
+ },
+ ],
+
+ /* Folder for test artifacts such as screenshots, videos, traces, etc. */
+ // outputDir: 'test-results/',
+}
+
+export default config
diff --git a/e2e-tests/development-runtime/playwright/gatsby-script-off-main-thread.spec.ts b/e2e-tests/development-runtime/playwright/gatsby-script-off-main-thread.spec.ts
new file mode 100644
index 0000000000000..4a1cef1c08e17
--- /dev/null
+++ b/e2e-tests/development-runtime/playwright/gatsby-script-off-main-thread.spec.ts
@@ -0,0 +1,27 @@
+import { test, expect } from "@playwright/test"
+
+const id = {
+ templateLiteral: `inline-script-template-literal-mutation`,
+ dangerouslySet: `inline-script-dangerously-set-mutation`,
+}
+
+test.describe(`off-main-thread scripts`, () => {
+ test(`should load successfully`, async ({ page }) => {
+ await page.goto(`/gatsby-script-off-main-thread/`)
+
+ // @ts-ignore
+ const scriptWithSrc = await page.evaluate(() => typeof THREE === `function`)
+
+ const templateLiteral = await page
+ .locator(`[id=${id.templateLiteral}]`)
+ .textContent()
+
+ const dangerouslySet = await page
+ .locator(`[id=${id.dangerouslySet}]`)
+ .textContent()
+
+ await expect(scriptWithSrc).toBeTruthy()
+ await expect(templateLiteral).toEqual(`${id.templateLiteral}: success`)
+ await expect(dangerouslySet).toEqual(`${id.dangerouslySet}: success`)
+ })
+})
diff --git a/e2e-tests/development-runtime/src/components/gatsby-script-mark-records.js b/e2e-tests/development-runtime/src/components/gatsby-script-mark-records.js
new file mode 100644
index 0000000000000..00d90e573eb9e
--- /dev/null
+++ b/e2e-tests/development-runtime/src/components/gatsby-script-mark-records.js
@@ -0,0 +1,60 @@
+import React, { useState, useEffect } from "react"
+import { markRecord } from "../../gatsby-script-records"
+import { trim } from "../utils/trim"
+
+/**
+ * Displays performance mark records of scripts in a table.
+ */
+export function ScriptMarkRecords(props) {
+ const { check, count } = props
+
+ const [records, setRecords] = useState([])
+
+ /**
+ * Poll for the mark records we care about.
+ * We'll use this approach instead of listening for the load event to be consistent.
+ */
+ useEffect(() => {
+ const interval = setInterval(() => {
+ const markRecords = performance.getEntriesByType(`mark`)
+
+ const scriptRecords = markRecords.filter(check)
+
+ if (scriptRecords.length === count || performance.now() > 10000) {
+ setRecords(scriptRecords)
+ clearInterval(interval)
+ }
+ }, 100)
+ }, [])
+
+ return (
+
+
+
+ Type |
+ Strategy |
+ Success |
+ Execute start (ms) |
+
+
+
+ {records
+ .sort((a, b) => a.detail.executeStart - b.detail.executeStart)
+ .map(record => {
+ const { strategy, type, executeStart } = record.detail
+ const key = `${strategy}-${type}`
+ // @ts-ignore Do not complain about key not being a number
+ const success = `${typeof window[key] === `boolean`}`
+ return (
+
+ {type} |
+ {strategy} |
+ {success} |
+ {trim(executeStart)} |
+
+ )
+ })}
+
+
+ )
+}
diff --git a/e2e-tests/development-runtime/src/components/gatsby-script-resource-records.js b/e2e-tests/development-runtime/src/components/gatsby-script-resource-records.js
new file mode 100644
index 0000000000000..313fdfbcf22d7
--- /dev/null
+++ b/e2e-tests/development-runtime/src/components/gatsby-script-resource-records.js
@@ -0,0 +1,79 @@
+import React, { useState, useEffect } from "react"
+import {
+ scriptUrlIndex,
+ scriptStrategyIndex,
+ scriptSuccessIndex,
+} from "../../gatsby-script-scripts"
+import { resourceRecord } from "../../gatsby-script-records"
+import { trim } from "../utils/trim"
+
+/**
+ * Displays performance resource records of scripts in a table.
+ */
+export function ScriptResourceRecords(props) {
+ const { check, count } = props
+
+ const [records, setRecords] = useState([])
+
+ /**
+ * Poll for the resource records we care about.
+ * Use this approach since `PerformanceObserver` doesn't give us preload link records (e.g. framework)
+ */
+ useEffect(() => {
+ const interval = setInterval(() => {
+ const resourceRecords = performance.getEntriesByType(`resource`)
+
+ const scriptRecords = resourceRecords.filter(check)
+
+ if (scriptRecords.length === count || performance.now() > 10000) {
+ setRecords(scriptRecords)
+ clearInterval(interval)
+ }
+ }, 100)
+ }, [])
+
+ return (
+
+
+
+ Script |
+ Strategy |
+ Success |
+ Fetch start (ms) |
+ Response end (ms) |
+
+
+
+ {records
+ .sort((a, b) => a.fetchStart - b.fetchStart)
+ .map(record => {
+ const { name: url, fetchStart, responseEnd } = record || {}
+
+ let name
+ let strategy
+ let success
+
+ if (record.name.includes(`framework`)) {
+ name = `framework`
+ strategy = `N/A`
+ success = `N/A`
+ } else {
+ name = scriptUrlIndex[url]
+ strategy = scriptStrategyIndex[name]
+ success = `${scriptSuccessIndex[name]()}`
+ }
+
+ return (
+
+ {name} |
+ {strategy} |
+ {success} |
+ {trim(fetchStart)} |
+ {trim(responseEnd)} |
+
+ )
+ })}
+
+
+ )
+}
diff --git a/e2e-tests/development-runtime/src/hooks/use-occupy-main-thread.js b/e2e-tests/development-runtime/src/hooks/use-occupy-main-thread.js
new file mode 100644
index 0000000000000..f6d76654f6a87
--- /dev/null
+++ b/e2e-tests/development-runtime/src/hooks/use-occupy-main-thread.js
@@ -0,0 +1,16 @@
+import { useEffect } from "react"
+
+/**
+ * Attempts to occupy the main thread with work so that the idle strategy can be observed.
+ */
+export function useOccupyMainThread(timeout = 3000) {
+ useEffect(() => {
+ const interval = setInterval(() => {
+ console.log(`Occupying main thread`)
+ }, 0)
+
+ setTimeout(() => {
+ clearInterval(interval)
+ }, timeout)
+ }, [])
+}
diff --git a/e2e-tests/development-runtime/src/pages/gatsby-script-inline-scripts.js b/e2e-tests/development-runtime/src/pages/gatsby-script-inline-scripts.js
new file mode 100644
index 0000000000000..5c1777d866a73
--- /dev/null
+++ b/e2e-tests/development-runtime/src/pages/gatsby-script-inline-scripts.js
@@ -0,0 +1,71 @@
+import * as React from "react"
+import { Link, Script } from "gatsby"
+import { ScriptResourceRecords } from "../components/gatsby-script-resource-records"
+import { ScriptMarkRecords } from "../components/gatsby-script-mark-records"
+import { useOccupyMainThread } from "../hooks/use-occupy-main-thread"
+import { inlineScripts, inlineScript } from "../../gatsby-script-scripts"
+
+function InlineScriptsPage() {
+ useOccupyMainThread()
+
+ return (
+
+ Script component e2e test
+
+
+ Framework script
+ record.name.includes(`framework`)}
+ count={1}
+ />
+
+
+ Inline scripts
+ record.name === `inline-script`}
+ count={4}
+ />
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default InlineScriptsPage
diff --git a/e2e-tests/development-runtime/src/pages/gatsby-script-navigation.js b/e2e-tests/development-runtime/src/pages/gatsby-script-navigation.js
new file mode 100644
index 0000000000000..3135d4e3e1be2
--- /dev/null
+++ b/e2e-tests/development-runtime/src/pages/gatsby-script-navigation.js
@@ -0,0 +1,47 @@
+import * as React from "react"
+import { Link } from "gatsby"
+
+const pages = [
+ {
+ name: `Scripts with sources`,
+ path: `/gatsby-script-scripts-with-sources/`,
+ },
+ { name: `Inline scripts`, path: `/gatsby-script-inline-scripts/` },
+ {
+ name: `Scripts from SSR and browser apis`,
+ path: `/gatsby-script-ssr-browser-apis/`,
+ },
+]
+
+function IndexPage() {
+ return (
+
+ Script component e2e test
+ Tests are on other pages, links below.
+
+ Links to pages (anchor):
+
+
+ Links to pages (gatsby-link):
+
+ {pages.map(({ name, path }) => (
+ -
+
+ {`${name} (gatsby-link)`}
+
+
+ ))}
+
+
+ )
+}
+
+export default IndexPage
diff --git a/e2e-tests/development-runtime/src/pages/gatsby-script-off-main-thread.js b/e2e-tests/development-runtime/src/pages/gatsby-script-off-main-thread.js
new file mode 100644
index 0000000000000..a6387e0dd2cc0
--- /dev/null
+++ b/e2e-tests/development-runtime/src/pages/gatsby-script-off-main-thread.js
@@ -0,0 +1,44 @@
+import * as React from "react"
+import { Script } from "gatsby"
+import { scripts } from "../../gatsby-script-scripts"
+
+function OffMainThreadScripts() {
+ return (
+
+ Script component e2e test
+
+
+ Scripts with sources
+
+
+
+
+
+
+
+ )
+}
+
+function createScript(id) {
+ return `
+ const main = document.querySelector('main');
+ const elem = document.createElement('div');
+ elem.id = '${id}-mutation';
+ elem.textContent = '${id}-mutation: success';
+ main.appendChild(elem);
+ `
+}
+
+export default OffMainThreadScripts
diff --git a/e2e-tests/development-runtime/src/pages/gatsby-script-scripts-with-sources.js b/e2e-tests/development-runtime/src/pages/gatsby-script-scripts-with-sources.js
new file mode 100644
index 0000000000000..28a682aa7048c
--- /dev/null
+++ b/e2e-tests/development-runtime/src/pages/gatsby-script-scripts-with-sources.js
@@ -0,0 +1,63 @@
+import * as React from "react"
+import { Link, Script } from "gatsby"
+import { ScriptResourceRecords } from "../components/gatsby-script-resource-records"
+import { useOccupyMainThread } from "../hooks/use-occupy-main-thread"
+import { scripts, scriptUrls } from "../../gatsby-script-scripts"
+import { onLoad, onError } from "../utils/gatsby-script-callbacks"
+
+function ScriptsWithSourcesPage() {
+ useOccupyMainThread()
+
+ return (
+
+ Script component e2e test
+
+
+ Scripts with sources
+
+ scriptUrls.has(record.name) || record.name.includes(`framework`)
+ }
+ count={5} // Include scripts from ssr/browser APIs
+ />
+
+
+
+
+
+
+
+ )
+}
+
+export default InlineScriptsPage
diff --git a/e2e-tests/production-runtime/src/pages/gatsby-script-navigation.js b/e2e-tests/production-runtime/src/pages/gatsby-script-navigation.js
new file mode 100644
index 0000000000000..3135d4e3e1be2
--- /dev/null
+++ b/e2e-tests/production-runtime/src/pages/gatsby-script-navigation.js
@@ -0,0 +1,47 @@
+import * as React from "react"
+import { Link } from "gatsby"
+
+const pages = [
+ {
+ name: `Scripts with sources`,
+ path: `/gatsby-script-scripts-with-sources/`,
+ },
+ { name: `Inline scripts`, path: `/gatsby-script-inline-scripts/` },
+ {
+ name: `Scripts from SSR and browser apis`,
+ path: `/gatsby-script-ssr-browser-apis/`,
+ },
+]
+
+function IndexPage() {
+ return (
+
+ Script component e2e test
+ Tests are on other pages, links below.
+
+ Links to pages (anchor):
+
+
+ Links to pages (gatsby-link):
+
+ {pages.map(({ name, path }) => (
+ -
+
+ {`${name} (gatsby-link)`}
+
+
+ ))}
+
+
+ )
+}
+
+export default IndexPage
diff --git a/e2e-tests/production-runtime/src/pages/gatsby-script-off-main-thread.js b/e2e-tests/production-runtime/src/pages/gatsby-script-off-main-thread.js
new file mode 100644
index 0000000000000..b888521385bd1
--- /dev/null
+++ b/e2e-tests/production-runtime/src/pages/gatsby-script-off-main-thread.js
@@ -0,0 +1,44 @@
+import * as React from "react"
+import { Script } from "gatsby"
+import { scripts } from "../../gatsby-script-scripts"
+
+function OffMainThreadScripts() {
+ return (
+
+ Script component e2e test
+
+
+ Scripts with sources
+
+
+
+
+
+
+
+ )
+}
+
+function createScript(id) {
+ return `
+ const main = document.querySelector('main');
+ const elem = document.createElement('div');
+ elem.id = '${id}-mutation';
+ elem.textContent = '${id}-mutation: success';
+ main.appendChild(elem);
+ `
+}
+
+export default OffMainThreadScripts
diff --git a/e2e-tests/production-runtime/src/pages/gatsby-script-scripts-with-sources.js b/e2e-tests/production-runtime/src/pages/gatsby-script-scripts-with-sources.js
new file mode 100644
index 0000000000000..28a682aa7048c
--- /dev/null
+++ b/e2e-tests/production-runtime/src/pages/gatsby-script-scripts-with-sources.js
@@ -0,0 +1,63 @@
+import * as React from "react"
+import { Link, Script } from "gatsby"
+import { ScriptResourceRecords } from "../components/gatsby-script-resource-records"
+import { useOccupyMainThread } from "../hooks/use-occupy-main-thread"
+import { scripts, scriptUrls } from "../../gatsby-script-scripts"
+import { onLoad, onError } from "../utils/gatsby-script-callbacks"
+
+function ScriptsWithSourcesPage() {
+ useOccupyMainThread()
+
+ return (
+
+ Script component e2e test
+
+
+ Scripts with sources
+
+ scriptUrls.has(record.name) || record.name.includes(`framework`)
+ }
+ count={5} // Include scripts from ssr/browser APIs
+ />
+
+
+
+
+
+
+
+
+ )
+}
+
+export default IndexPage
+```
diff --git a/packages/gatsby-script/package.json b/packages/gatsby-script/package.json
new file mode 100644
index 0000000000000..abbf93ceb785d
--- /dev/null
+++ b/packages/gatsby-script/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "gatsby-script",
+ "description": "An enhanced script component for Gatsby sites with support for various loading strategies",
+ "version": "1.0.0-next.3",
+ "author": "Ty Hopp ",
+ "bugs": {
+ "url": "https://github.com/gatsbyjs/gatsby/issues"
+ },
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "typesVersions": {
+ "*": {
+ "*": [
+ "dist/*.d.ts",
+ "dist/index.d.ts"
+ ]
+ }
+ },
+ "files": [
+ "dist/*"
+ ],
+ "scripts": {
+ "typegen": "rimraf \"dist/**/*.d.ts\" && tsc --emitDeclarationOnly --declaration --declarationDir dist/",
+ "build": "babel src --extensions \".js,.jsx,.ts,.tsx\" --out-dir dist --ignore \"**/__tests__\"",
+ "watch": "babel -w src --extensions \".js,.jsx,.ts,.tsx\" --out-dir dist --ignore \"**/__tests__\"",
+ "prepare": "cross-env NODE_ENV=production npm run build && npm run typegen"
+ },
+ "dependencies": {},
+ "devDependencies": {
+ "@babel/cli": "^7.15.4",
+ "@babel/core": "^7.15.5",
+ "@babel/preset-typescript": "^7.16.7",
+ "@testing-library/react": "^11.2.7",
+ "babel-preset-gatsby-package": "^2.15.0-next.0",
+ "cross-env": "^7.0.3",
+ "rimraf": "^3.0.2",
+ "typescript": "^4.6.3"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.9.0 || ^17.0.0 || ^18.0.0"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "homepage": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-script#readme",
+ "keywords": [
+ "gatsby"
+ ],
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/gatsbyjs/gatsby.git",
+ "directory": "packages/gatsby-script"
+ }
+}
diff --git a/packages/gatsby-script/src/__tests__/gatsby-script.tsx b/packages/gatsby-script/src/__tests__/gatsby-script.tsx
new file mode 100644
index 0000000000000..e3100553f7884
--- /dev/null
+++ b/packages/gatsby-script/src/__tests__/gatsby-script.tsx
@@ -0,0 +1,131 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import React from "react"
+import { render } from "@testing-library/react/pure"
+import { Script, ScriptStrategy, scriptCache } from "../gatsby-script"
+
+const scripts: Record = {
+ react: `https://unpkg.com/react@18/umd/react.development.js`,
+ inline: `console.log('Hello world!')`,
+}
+
+const strategies: Array = [
+ ScriptStrategy.postHydrate,
+ ScriptStrategy.idle,
+]
+
+jest.mock(`../request-idle-callback-shim`, () => {
+ const originalModule = jest.requireActual(`../request-idle-callback-shim`)
+
+ return {
+ __esModule: true,
+ ...originalModule,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ requestIdleCallback: jest.fn(callback => callback()),
+ }
+})
+
+describe(`Script`, () => {
+ beforeAll(() => {
+ // @ts-ignore Mock it out for now
+ performance.getEntriesByName = jest.fn(() => [])
+ })
+
+ afterEach(() => {
+ scriptCache.delete(scripts.react)
+ while (document.body.hasChildNodes()) {
+ document.body.lastElementChild?.remove()
+ }
+ })
+
+ it(`should default to a post-hydrate strategy`, async () => {
+ const { container } = render()
+ const script = container.parentElement.querySelector(`script`)
+ expect(script.getAttribute(`data-strategy`)).toEqual(
+ ScriptStrategy.postHydrate
+ )
+ })
+
+ it(`should be possible to declare a post-hydrate strategy`, () => {
+ const { container } = render(
+
+ )
+ const script = container.parentElement.querySelector(`script`)
+ expect(script.getAttribute(`data-strategy`)).toEqual(
+ ScriptStrategy.postHydrate
+ )
+ })
+
+ it(`should be possible to declare an idle strategy`, () => {
+ const { container } = render(
+
+ )
+ const script = container.parentElement.querySelector(`script`)
+ expect(script.getAttribute(`data-strategy`)).toEqual(ScriptStrategy.idle)
+ })
+
+ it(`should be possible to declare an off-main-thread strategy`, () => {
+ const { container } = render(
+
+ )
+ const script = container.parentElement.querySelector(`script`)
+ expect(script.getAttribute(`data-strategy`)).toEqual(
+ ScriptStrategy.offMainThread
+ )
+ expect(script.getAttribute(`type`)).toEqual(`text/partytown`)
+ })
+
+ it(`should include inline scripts passed via the dangerouslySetInnerHTML prop in the DOM`, () => {
+ for (const strategy of strategies) {
+ const { container } = render(
+
+ )
+ const script = container.parentElement.querySelector(`script`)
+ expect(script.textContent).toBe(scripts.inline)
+ }
+ })
+
+ it(`should include inline scripts passed via template literals in the DOM`, () => {
+ for (const strategy of strategies) {
+ const { container } = render(
+
+ )
+ const script = container.parentElement.querySelector(`script`)
+ expect(script.textContent).toBe(scripts.inline)
+ }
+ })
+
+ it(`should apply normal attributes`, () => {
+ const lines = {
+ first: `It's the bear necessities`,
+ second: `the simple bear necessities`,
+ third: `forget about your worries and your strife`,
+ }
+
+ for (const strategy of strategies) {
+ const { container } = render(
+
+ )
+
+ const script = container.parentElement.querySelector(`script`)
+
+ expect(script.dataset.first).toBe(lines.first)
+ expect(script.dataset.second).toBe(lines.second)
+ expect(script.dataset.third).toBe(lines.third)
+ }
+ })
+})
diff --git a/packages/gatsby-script/src/gatsby-script.tsx b/packages/gatsby-script/src/gatsby-script.tsx
new file mode 100644
index 0000000000000..2cd7741ac2837
--- /dev/null
+++ b/packages/gatsby-script/src/gatsby-script.tsx
@@ -0,0 +1,189 @@
+import React, { useEffect, useContext } from "react"
+import { PartytownContext } from "./partytown-context"
+import type { ReactElement, ScriptHTMLAttributes } from "react"
+import { requestIdleCallback } from "./request-idle-callback-shim"
+
+export enum ScriptStrategy {
+ postHydrate = `post-hydrate`,
+ idle = `idle`,
+ offMainThread = `off-main-thread`,
+}
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export interface ScriptProps
+ extends Omit, `onLoad` | `onError`> {
+ id?: string
+ strategy?: ScriptStrategy | `post-hydrate` | `idle` | `off-main-thread`
+ children?: string
+ onLoad?: (event: Event) => void
+ onError?: (event: ErrorEvent) => void
+ forward?: Array
+}
+
+const handledProps = new Set([
+ `src`,
+ `strategy`,
+ `dangerouslySetInnerHTML`,
+ `children`,
+ `onLoad`,
+ `onError`,
+])
+
+export const scriptCache = new Set()
+
+export function Script(props: ScriptProps): ReactElement | null {
+ const {
+ id,
+ src,
+ strategy = ScriptStrategy.postHydrate,
+ onLoad,
+ onError,
+ } = props || {}
+ const { collectScript } = useContext(PartytownContext)
+
+ useEffect(() => {
+ let script: HTMLScriptElement | null
+
+ switch (strategy) {
+ case ScriptStrategy.postHydrate:
+ script = injectScript(props)
+ break
+ case ScriptStrategy.idle:
+ requestIdleCallback(() => {
+ script = injectScript(props)
+ })
+ break
+ case ScriptStrategy.offMainThread:
+ if (typeof window !== `undefined` && collectScript) {
+ const attributes = resolveAttributes(props)
+ collectScript(attributes)
+ }
+ break
+ }
+
+ return (): void => {
+ if (onLoad) {
+ script?.removeEventListener(`load`, onLoad)
+ }
+ if (onError) {
+ script?.removeEventListener(`error`, onError)
+ }
+ script?.remove()
+ }
+ }, [])
+
+ if (strategy === ScriptStrategy.offMainThread) {
+ const inlineScript = resolveInlineScript(props)
+ const attributes = resolveAttributes(props)
+
+ if (typeof window === `undefined` && collectScript) {
+ const identifier = id || src || `no-id-or-src`
+ console.warn(
+ `Unable to collect off-main-thread script '${identifier}' for configuration with Partytown.\nGatsby script components must be used either as a child of your page, in wrapPageElement, or wrapRootElement.\nSee https://gatsby.dev/gatsby-script for more information.`
+ )
+ collectScript(attributes)
+ }
+
+ if (inlineScript) {
+ return (
+
+ )
+ }
+ return (
+
+ )
+ }
+
+ return null
+}
+
+function injectScript(props: ScriptProps): HTMLScriptElement | null {
+ const {
+ id,
+ src,
+ strategy = ScriptStrategy.postHydrate,
+ onLoad,
+ onError,
+ } = props || {}
+
+ if (scriptCache.has(id || src)) {
+ return null
+ }
+
+ const inlineScript = resolveInlineScript(props)
+ const attributes = resolveAttributes(props)
+
+ const script = document.createElement(`script`)
+
+ if (id) {
+ script.id = id
+ }
+
+ script.dataset.strategy = strategy
+
+ for (const [key, value] of Object.entries(attributes)) {
+ script.setAttribute(key, value)
+ }
+
+ if (inlineScript) {
+ script.textContent = inlineScript
+ }
+
+ if (src) {
+ script.src = src
+ }
+
+ if (onLoad) {
+ script.addEventListener(`load`, onLoad)
+ }
+
+ if (onError) {
+ script.addEventListener(`error`, onError)
+ }
+
+ document.body.appendChild(script)
+
+ scriptCache.add(id || src)
+
+ return script
+}
+
+function resolveInlineScript(props: ScriptProps): string {
+ const { dangerouslySetInnerHTML, children = `` } = props || {}
+ const { __html: dangerousHTML = `` } = dangerouslySetInnerHTML || {}
+ return dangerousHTML || children
+}
+
+function resolveAttributes(props: ScriptProps): Record {
+ const attributes: Record = {}
+
+ for (const [key, value] of Object.entries(props)) {
+ if (handledProps.has(key)) {
+ continue
+ }
+ attributes[key] = value
+ }
+
+ return attributes
+}
+
+function proxyPartytownUrl(url: string | undefined): string | undefined {
+ if (!url) {
+ return undefined
+ }
+ return `/__partytown-proxy?url=${encodeURIComponent(url)}`
+}
diff --git a/packages/gatsby-script/src/index.ts b/packages/gatsby-script/src/index.ts
new file mode 100644
index 0000000000000..8f908ee595028
--- /dev/null
+++ b/packages/gatsby-script/src/index.ts
@@ -0,0 +1,2 @@
+export * from "./gatsby-script"
+export * from "./partytown-context"
diff --git a/packages/gatsby-script/src/partytown-context.tsx b/packages/gatsby-script/src/partytown-context.tsx
new file mode 100644
index 0000000000000..bd2dc840bc0e2
--- /dev/null
+++ b/packages/gatsby-script/src/partytown-context.tsx
@@ -0,0 +1,8 @@
+import { createContext } from "react"
+import { ScriptProps } from "./gatsby-script"
+
+const PartytownContext: React.Context<{
+ collectScript?: (script: ScriptProps) => void
+}> = createContext({})
+
+export { PartytownContext }
diff --git a/packages/gatsby-script/src/request-idle-callback-shim.ts b/packages/gatsby-script/src/request-idle-callback-shim.ts
new file mode 100644
index 0000000000000..c0506fa45da4d
--- /dev/null
+++ b/packages/gatsby-script/src/request-idle-callback-shim.ts
@@ -0,0 +1,18 @@
+// https://developer.chrome.com/blog/using-requestidlecallback/#checking-for-requestidlecallback
+// https://github.com/vercel/next.js/blob/canary/packages/next/client/request-idle-callback.ts
+
+export const requestIdleCallback =
+ (typeof self !== `undefined` &&
+ self.requestIdleCallback &&
+ self.requestIdleCallback.bind(window)) ||
+ function (cb: IdleRequestCallback): number {
+ const start = Date.now()
+ return setTimeout(function () {
+ cb({
+ didTimeout: false,
+ timeRemaining: function () {
+ return Math.max(0, 50 - (Date.now() - start))
+ },
+ })
+ }, 1) as unknown as number
+ }
diff --git a/packages/gatsby-script/tsconfig.json b/packages/gatsby-script/tsconfig.json
new file mode 100644
index 0000000000000..aa2a7db61b937
--- /dev/null
+++ b/packages/gatsby-script/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.json",
+ "exclude": [
+ "node_modules",
+ "src/__tests__",
+ "dist"
+ ]
+}
diff --git a/packages/gatsby/cache-dir/gatsby-browser-entry.js b/packages/gatsby/cache-dir/gatsby-browser-entry.js
index 94a1e268ffbb9..723a310c45dfb 100644
--- a/packages/gatsby/cache-dir/gatsby-browser-entry.js
+++ b/packages/gatsby/cache-dir/gatsby-browser-entry.js
@@ -112,3 +112,5 @@ export {
useStaticQuery,
prefetchPathname,
}
+
+export * from "gatsby-script"
diff --git a/packages/gatsby/index.d.ts b/packages/gatsby/index.d.ts
index 02e229db21feb..66dee4351b5a9 100644
--- a/packages/gatsby/index.d.ts
+++ b/packages/gatsby/index.d.ts
@@ -27,6 +27,8 @@ export {
withAssetPrefix,
} from "gatsby-link"
+export * from "gatsby-script"
+
export const useScrollRestoration: (key: string) => {
ref: React.MutableRefObject
onScroll(): void
@@ -256,6 +258,11 @@ export interface GatsbyConfig {
prefix: string
url: string
}
+ /**
+ * A list of trusted URLs that will be proxied for use with the gatsby-script off-main-thread strategy.
+ * @see https://gatsby.dev/gatsby-script
+ */
+ partytownProxiedURLs?: Array
/** Sometimes you need more granular/flexible access to the development server. Gatsby exposes the Express.js development server to your site’s gatsby-config.js where you can add Express middleware as needed. */
developMiddleware?(app: any): void
}
diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json
index 9eda507c8c0de..c675f927658c1 100644
--- a/packages/gatsby/package.json
+++ b/packages/gatsby/package.json
@@ -18,6 +18,7 @@
"@babel/runtime": "^7.15.4",
"@babel/traverse": "^7.15.4",
"@babel/types": "^7.15.4",
+ "@builder.io/partytown": "^0.5.2",
"@gatsbyjs/reach-router": "^1.3.6",
"@gatsbyjs/webpack-hot-middleware": "^2.25.2",
"@graphql-codegen/add": "^3.1.1",
@@ -79,6 +80,7 @@
"execa": "^5.1.1",
"express": "^4.17.1",
"express-graphql": "^0.12.0",
+ "express-http-proxy": "^1.6.3",
"fastest-levenshtein": "^1.0.12",
"fastq": "^1.13.0",
"file-loader": "^6.2.0",
@@ -96,6 +98,7 @@
"gatsby-plugin-typescript": "^4.15.0-next.1",
"gatsby-plugin-utils": "^3.9.0-next.2",
"gatsby-react-router-scroll": "^5.15.0-next.0",
+ "gatsby-script": "1.0.0-next.3",
"gatsby-telemetry": "^3.15.0-next.1",
"gatsby-worker": "^1.15.0-next.0",
"glob": "^7.2.3",
@@ -176,6 +179,7 @@
"@babel/register": "^7.15.3",
"@types/eslint": "^8.2.1",
"@types/express": "^4.17.13",
+ "@types/express-http-proxy": "^1.6.3",
"@types/micromatch": "^4.0.1",
"@types/normalize-path": "^3.0.0",
"@types/reach__router": "^1.3.5",
@@ -259,7 +263,7 @@
"build:internal-plugins": "copyfiles -u 1 src/internal-plugins/**/package.json dist",
"build:rawfiles": "copyfiles -u 1 src/internal-plugins/**/raw_* dist",
"build:cjs": "babel cache-dir --out-dir cache-dir/commonjs --ignore \"**/__tests__\" --ignore \"**/__mocks__\" && copyfiles -u 1 cache-dir/**/*.json cache-dir/commonjs",
- "build:src": "babel src --out-dir dist --source-maps --verbose --ignore \"**/gatsby-cli.js,src/internal-plugins/dev-404-page/raw_dev-404-page.js,**/__tests__,**/__mocks__\" --extensions \".ts,.js\"",
+ "build:src": "babel src --out-dir dist --source-maps --verbose --ignore \"**/gatsby-cli.js,src/internal-plugins/dev-404-page/raw_dev-404-page.js,**/__tests__,**/__mocks__\" --extensions \".ts,.tsx,.js\"",
"build:types": "tsc --emitDeclarationOnly --declaration --declarationDir dist && node scripts/check-declaration.js",
"clean-test-bundles": "find test/ -type f -name bundle.js* -exec rm -rf {} +",
"prebuild": "rimraf dist && rimraf cache-dir/commonjs",
diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/load-plugins.ts.snap b/packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/load-plugins.ts.snap
index ba478723ae7ae..e2ec0eae1baba 100644
--- a/packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/load-plugins.ts.snap
+++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/load-plugins.ts.snap
@@ -305,6 +305,27 @@ Array [
"subPluginPaths": undefined,
"version": "1.0.0",
},
+ Object {
+ "browserAPIs": Array [
+ "wrapRootElement",
+ ],
+ "id": "",
+ "name": "partytown",
+ "nodeAPIs": Array [
+ "onCreateDevServer",
+ "onPreBootstrap",
+ "createPages",
+ ],
+ "pluginOptions": Object {
+ "plugins": Array [],
+ },
+ "resolve": "",
+ "ssrAPIs": Array [
+ "wrapRootElement",
+ "onRenderBody",
+ ],
+ "version": "1.0.0",
+ },
]
`;
@@ -646,6 +667,27 @@ Array [
"subPluginPaths": undefined,
"version": "1.0.0",
},
+ Object {
+ "browserAPIs": Array [
+ "wrapRootElement",
+ ],
+ "id": "",
+ "name": "partytown",
+ "nodeAPIs": Array [
+ "onCreateDevServer",
+ "onPreBootstrap",
+ "createPages",
+ ],
+ "pluginOptions": Object {
+ "plugins": Array [],
+ },
+ "resolve": "",
+ "ssrAPIs": Array [
+ "wrapRootElement",
+ "onRenderBody",
+ ],
+ "version": "1.0.0",
+ },
]
`;
@@ -998,6 +1040,27 @@ Array [
"subPluginPaths": undefined,
"version": "1.0.0",
},
+ Object {
+ "browserAPIs": Array [
+ "wrapRootElement",
+ ],
+ "id": "",
+ "name": "partytown",
+ "nodeAPIs": Array [
+ "onCreateDevServer",
+ "onPreBootstrap",
+ "createPages",
+ ],
+ "pluginOptions": Object {
+ "plugins": Array [],
+ },
+ "resolve": "",
+ "ssrAPIs": Array [
+ "wrapRootElement",
+ "onRenderBody",
+ ],
+ "version": "1.0.0",
+ },
]
`;
diff --git a/packages/gatsby/src/bootstrap/load-plugins/load-internal-plugins.ts b/packages/gatsby/src/bootstrap/load-plugins/load-internal-plugins.ts
index 579dc3d2ec4f5..55e1a07234f98 100644
--- a/packages/gatsby/src/bootstrap/load-plugins/load-internal-plugins.ts
+++ b/packages/gatsby/src/bootstrap/load-plugins/load-internal-plugins.ts
@@ -161,5 +161,15 @@ export function loadInternalPlugins(
plugins.push(processedPageCreatorPlugin)
+ // Partytown plugin collects usage of
+ // in `wrapRootElement`, so we have to make sure it's the last one running to be able to
+ // collect scripts that users might inject in their `wrapRootElement`
+ plugins.push(
+ processPlugin(
+ path.join(__dirname, `../../internal-plugins/partytown`),
+ rootDir
+ )
+ )
+
return plugins
}
diff --git a/packages/gatsby/src/commands/develop-process.ts b/packages/gatsby/src/commands/develop-process.ts
index 697f372d34fdb..17996b56909b4 100644
--- a/packages/gatsby/src/commands/develop-process.ts
+++ b/packages/gatsby/src/commands/develop-process.ts
@@ -71,11 +71,17 @@ onExit(() => {
})
})
-process.on(`message`, msg => {
- if (msg.type === `COMMAND` && msg.action.type === `EXIT`) {
- process.exit(msg.action.payload)
+process.on(
+ `message`,
+ (msg: {
+ type: string
+ action: { type: string; payload: number | undefined }
+ }) => {
+ if (msg.type === `COMMAND` && msg.action.type === `EXIT`) {
+ process.exit(msg.action.payload)
+ }
}
-})
+)
interface IDevelopArgs extends IProgram {
debugInfo: IDebugInfo | null
diff --git a/packages/gatsby/src/commands/serve.ts b/packages/gatsby/src/commands/serve.ts
index ae40172c18c77..1913c07e6f01b 100644
--- a/packages/gatsby/src/commands/serve.ts
+++ b/packages/gatsby/src/commands/serve.ts
@@ -25,6 +25,10 @@ import { initTracer } from "../utils/tracer"
import { configureTrailingSlash } from "../utils/express-middlewares"
import { getDataStore, detectLmdbStore } from "../datastore"
import { functionMiddlewares } from "../internal-plugins/functions/middleware"
+import {
+ partytownProxyPath,
+ partytownProxy,
+} from "../internal-plugins/partytown/proxy"
process.env.GATSBY_EXPERIMENTAL_LMDB_STORE = `1`
detectLmdbStore()
@@ -119,6 +123,12 @@ module.exports = async (program: IServeProgram): Promise => {
const root = path.join(program.directory, `public`)
const app = express()
+
+ // Proxy gatsby-script using off-main-thread strategy
+ const { partytownProxiedURLs = [] } = config || {}
+
+ app.use(partytownProxyPath, partytownProxy(partytownProxiedURLs))
+
// eslint-disable-next-line new-cap
const router = express.Router()
diff --git a/packages/gatsby/src/internal-plugins/partytown/gatsby-browser.tsx b/packages/gatsby/src/internal-plugins/partytown/gatsby-browser.tsx
new file mode 100644
index 0000000000000..7e3c496abcf13
--- /dev/null
+++ b/packages/gatsby/src/internal-plugins/partytown/gatsby-browser.tsx
@@ -0,0 +1,62 @@
+import React, { ReactElement, useState } from "react"
+import type { GatsbySSR } from "gatsby"
+import { Partytown } from "@builder.io/partytown/react"
+import { PartytownContext, ScriptProps } from "gatsby-script"
+
+interface ICollectedForwardsState {
+ collectedForwards: Set
+ collectedAnyScript: boolean
+}
+
+function PartytownProvider({ children }): ReactElement {
+ const [{ collectedForwards, collectedAnyScript }, setState] =
+ useState({
+ collectedForwards: new Set(),
+ collectedAnyScript: false,
+ })
+
+ return (
+ {
+ let stateShouldChange = false
+ const potentialNewState = {
+ collectedAnyScript,
+ collectedForwards,
+ }
+
+ if (!collectedAnyScript) {
+ potentialNewState.collectedAnyScript = true
+ stateShouldChange = true
+ }
+
+ if (newScript?.forward) {
+ if (Array.isArray(newScript.forward)) {
+ for (const singleForward of newScript.forward) {
+ if (!potentialNewState.collectedForwards.has(singleForward)) {
+ potentialNewState.collectedForwards.add(singleForward)
+ stateShouldChange = true
+ }
+ }
+ } else {
+ console.log(`unexpected shape of forward`, newScript)
+ }
+ }
+
+ if (stateShouldChange) {
+ setState(potentialNewState)
+ }
+ },
+ }}
+ >
+ {children}
+ {collectedAnyScript && (
+
+ )}
+
+ )
+}
+
+export const wrapRootElement: GatsbySSR[`wrapRootElement`] = ({ element }) => (
+ {element}
+)
diff --git a/packages/gatsby/src/internal-plugins/partytown/gatsby-node.ts b/packages/gatsby/src/internal-plugins/partytown/gatsby-node.ts
new file mode 100644
index 0000000000000..f8d81cbf3c6b7
--- /dev/null
+++ b/packages/gatsby/src/internal-plugins/partytown/gatsby-node.ts
@@ -0,0 +1,44 @@
+import path from "path"
+import { copyLibFiles } from "@builder.io/partytown/utils"
+import { CreateDevServerArgs } from "gatsby"
+import { partytownProxyPath, partytownProxy } from "./proxy"
+
+/**
+ * Copy Partytown library files to public.
+ * @see {@link https://partytown.builder.io/copy-library-files}
+ */
+exports.onPreBootstrap = async ({ store }): Promise => {
+ const { program } = store.getState()
+ await copyLibFiles(path.join(program.directory, `public`, `~partytown`))
+}
+
+/**
+ * Implement reverse proxy so scripts can fetch in web workers without CORS errors.
+ * @see {@link https://partytown.builder.io/proxying-requests}
+ */
+exports.createPages = ({ actions, store }): void => {
+ const { createRedirect } = actions
+
+ const { config = {} } = store.getState()
+ const { partytownProxiedURLs = [] } = config
+
+ for (const host of partytownProxiedURLs) {
+ const encodedURL: string = encodeURI(host)
+
+ createRedirect({
+ fromPath: `${partytownProxyPath}?url=${encodedURL}`,
+ toPath: encodedURL,
+ statusCode: 200,
+ })
+ }
+}
+
+export async function onCreateDevServer({
+ app,
+ store,
+}: CreateDevServerArgs): Promise {
+ const { config } = store.getState()
+ const { partytownProxiedURLs = [] } = config || {}
+
+ app.use(partytownProxyPath, partytownProxy(partytownProxiedURLs))
+}
diff --git a/packages/gatsby/src/internal-plugins/partytown/gatsby-ssr.tsx b/packages/gatsby/src/internal-plugins/partytown/gatsby-ssr.tsx
new file mode 100644
index 0000000000000..a26f14bad2365
--- /dev/null
+++ b/packages/gatsby/src/internal-plugins/partytown/gatsby-ssr.tsx
@@ -0,0 +1,42 @@
+import React from "react"
+import type { GatsbySSR } from "gatsby"
+import { Partytown } from "@builder.io/partytown/react"
+import { PartytownContext, ScriptProps } from "gatsby-script"
+
+const collectedScripts: Map> = new Map()
+
+export const wrapRootElement: GatsbySSR[`wrapRootElement`] = ({
+ element,
+ pathname,
+}) => (
+ {
+ const currentCollectedScripts = collectedScripts.get(pathname) || []
+ currentCollectedScripts.push(newScript)
+ collectedScripts.set(pathname, currentCollectedScripts)
+ },
+ }}
+ >
+ {element}
+
+)
+
+export const onRenderBody: GatsbySSR[`onRenderBody`] = ({
+ pathname,
+ setHeadComponents,
+}) => {
+ const collectedScriptsOnPage = collectedScripts.get(pathname)
+
+ if (!collectedScriptsOnPage?.length) {
+ return
+ }
+
+ const collectedForwards: Array = collectedScriptsOnPage?.flatMap(
+ (script: ScriptProps) => script?.forward || []
+ )
+
+ setHeadComponents([])
+
+ collectedScripts.delete(pathname)
+}
diff --git a/packages/gatsby/src/internal-plugins/partytown/index.js b/packages/gatsby/src/internal-plugins/partytown/index.js
new file mode 100644
index 0000000000000..172f1ae6a468c
--- /dev/null
+++ b/packages/gatsby/src/internal-plugins/partytown/index.js
@@ -0,0 +1 @@
+// noop
diff --git a/packages/gatsby/src/internal-plugins/partytown/package.json b/packages/gatsby/src/internal-plugins/partytown/package.json
new file mode 100644
index 0000000000000..5c7922919a85f
--- /dev/null
+++ b/packages/gatsby/src/internal-plugins/partytown/package.json
@@ -0,0 +1,12 @@
+{
+ "private": true,
+ "name": "partytown",
+ "version": "1.0.0",
+ "description": "Internal plugin that handles Partytown setup",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "Ty Hopp ",
+ "license": "MIT"
+}
diff --git a/packages/gatsby/src/internal-plugins/partytown/proxy.ts b/packages/gatsby/src/internal-plugins/partytown/proxy.ts
new file mode 100644
index 0000000000000..8bf597be2ca86
--- /dev/null
+++ b/packages/gatsby/src/internal-plugins/partytown/proxy.ts
@@ -0,0 +1,16 @@
+import proxy from "express-http-proxy"
+import type { RequestHandler } from "express"
+
+export const partytownProxyPath = `/__partytown-proxy`
+
+export function partytownProxy(
+ partytownProxiedURLs: Array
+): RequestHandler {
+ return proxy(req => new URL(req.query.url as string).origin as string, {
+ filter: req => partytownProxiedURLs.some(url => req.query?.url === url),
+ proxyReqPathResolver: req => {
+ const { pathname = ``, search = `` } = new URL(req.query?.url as string)
+ return pathname + search
+ },
+ })
+}
diff --git a/packages/gatsby/src/joi-schemas/joi.ts b/packages/gatsby/src/joi-schemas/joi.ts
index c652a13ccc9ab..2b20272e7df76 100644
--- a/packages/gatsby/src/joi-schemas/joi.ts
+++ b/packages/gatsby/src/joi-schemas/joi.ts
@@ -48,6 +48,7 @@ export const gatsbyConfigSchema: Joi.ObjectSchema = Joi.object()
})
)
.single(),
+ partytownProxiedURLs: Joi.array().items(Joi.string()),
developMiddleware: Joi.func(),
jsxRuntime: Joi.string().valid(`automatic`, `classic`).default(`classic`),
jsxImportSource: Joi.string(),
diff --git a/packages/gatsby/src/redux/types.ts b/packages/gatsby/src/redux/types.ts
index 15b57c21aeda3..1ee6aea1042b2 100644
--- a/packages/gatsby/src/redux/types.ts
+++ b/packages/gatsby/src/redux/types.ts
@@ -98,6 +98,7 @@ export interface IGatsbyConfig {
polyfill?: boolean
developMiddleware?: any
proxy?: any
+ partytownProxiedURLs?: Array
pathPrefix?: string
assetPrefix?: string
mapping?: Record
diff --git a/packages/gatsby/src/utils/webpack-utils.ts b/packages/gatsby/src/utils/webpack-utils.ts
index 213470b7cb4ea..ab1127b59ebf9 100644
--- a/packages/gatsby/src/utils/webpack-utils.ts
+++ b/packages/gatsby/src/utils/webpack-utils.ts
@@ -498,6 +498,7 @@ export const createWebpackUtils = (
`dom-helpers`,
`gatsby-legacy-polyfills`,
`gatsby-link`,
+ `gatsby-script`,
`gatsby-react-router-scroll`,
`invariant`,
`lodash`,
diff --git a/yarn.lock b/yarn.lock
index 9077a6ac0b3e6..1fdeadaa31712 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1825,6 +1825,11 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
+"@builder.io/partytown@^0.5.2":
+ version "0.5.4"
+ resolved "https://registry.yarnpkg.com/@builder.io/partytown/-/partytown-0.5.4.tgz#1a89069978734e132fa4a59414ddb64e4b94fde7"
+ integrity sha512-qnikpQgi30AS01aFlNQV6l8/qdZIcP76mp90ti+u4rucXHsn4afSKivQXApqxvrQG9+Ibv45STyvHizvxef/7A==
+
"@contentful/rich-text-react-renderer@^15.12.1":
version "15.12.1"
resolved "https://registry.yarnpkg.com/@contentful/rich-text-react-renderer/-/rich-text-react-renderer-15.12.1.tgz#978c335e7ad5284dc6790a6a8c0ec16878b957b0"
@@ -4890,6 +4895,13 @@
version "1.2.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
+"@types/express-http-proxy@^1.6.3":
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/@types/express-http-proxy/-/express-http-proxy-1.6.3.tgz#35fc0fb32e7741bc50619869de381ef759621fd0"
+ integrity sha512-dX3+Cb0HNPtqhC5JUWzzuODHRlgJRZx7KvwKVVwkOvm+8vOtpsh3qy8+qLv5X1hs4vdVHWKyXf86DwJot5H8pg==
+ dependencies:
+ "@types/express" "*"
+
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18":
version "4.17.28"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8"
@@ -4899,7 +4911,7 @@
"@types/qs" "*"
"@types/range-parser" "*"
-"@types/express@^4.17.13":
+"@types/express@*", "@types/express@^4.17.13":
version "4.17.13"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
@@ -9438,7 +9450,7 @@ debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, d
dependencies:
ms "2.1.2"
-debug@^3.0.0, debug@^3.1.0, debug@^3.2.6, debug@^3.2.7:
+debug@^3.0.0, debug@^3.0.1, debug@^3.1.0, debug@^3.2.6, debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
@@ -10463,6 +10475,11 @@ es6-promise@^4.0.3, es6-promise@^4.0.5:
version "4.2.5"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054"
+es6-promise@^4.1.1:
+ version "4.2.8"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
+ integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
+
es6-promisify@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
@@ -11104,6 +11121,15 @@ express-graphql@^0.12.0:
http-errors "1.8.0"
raw-body "^2.4.1"
+express-http-proxy@^1.6.3:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/express-http-proxy/-/express-http-proxy-1.6.3.tgz#f3ef139ffd49a7962e7af0462bbcca557c913175"
+ integrity sha512-/l77JHcOUrDUX8V67E287VEUQT0lbm71gdGVoodnlWBziarYKgMcpqT7xvh/HM8Jv52phw8Bd8tY+a7QjOr7Yg==
+ dependencies:
+ debug "^3.0.1"
+ es6-promise "^4.1.1"
+ raw-body "^2.3.0"
+
express@4.17.1, express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@@ -20290,7 +20316,7 @@ raw-body@2.4.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
-raw-body@2.5.1, raw-body@^2.4.1:
+raw-body@2.5.1, raw-body@^2.3.0, raw-body@^2.4.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
@@ -24741,7 +24767,7 @@ typedarray@^0.0.6, typedarray@~0.0.5:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-typescript@^4.1.3, typescript@^4.6.4:
+typescript@^4.1.3, typescript@^4.6.3, typescript@^4.6.4:
version "4.6.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9"
integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==