-
Notifications
You must be signed in to change notification settings - Fork 27.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[dynamicIO] Instrument
Math.random()
to be considered synchronously…
… dynamic
- Loading branch information
Showing
70 changed files
with
1,710 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// This file should be imported before any others. It sets up the environment | ||
// for later imports to work properly. | ||
|
||
// expose AsyncLocalStorage on global for react usage if it isn't already provided by the environment | ||
if (typeof (globalThis as any).AsyncLocalStorage !== 'function') { | ||
const { AsyncLocalStorage } = require('async_hooks') | ||
;(globalThis as any).AsyncLocalStorage = AsyncLocalStorage | ||
} | ||
|
||
if (typeof (globalThis as any).WebSocket !== 'function') { | ||
Object.defineProperty(globalThis, 'WebSocket', { | ||
get() { | ||
return require('next/dist/compiled/ws').WebSocket | ||
}, | ||
}) | ||
} |
36 changes: 36 additions & 0 deletions
36
packages/next/src/server/node-environment-extensions/random.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
/** | ||
* We extend Math.random() during builds and revalidates to ensure that prerenders don't observe randomness | ||
* When dynamicIO is enabled. randomness is a form of IO even though it resolves synchronously. When dyanmicIO is | ||
* enabled we need to ensure that randomness is excluded from prerenders. | ||
* | ||
* The extensions here never error nor alter the random generation itself and thus should be transparent to callers. | ||
*/ | ||
|
||
import { workAsyncStorage } from '../../client/components/work-async-storage.external' | ||
import { | ||
isDynamicIOPrerender, | ||
prerenderAsyncStorage, | ||
} from '../app-render/prerender-async-storage.external' | ||
import { abortOnSynchronousDynamicDataAccess } from '../app-render/dynamic-rendering' | ||
|
||
const originalRandom = Math.random | ||
|
||
Math.random = function () { | ||
const workStore = workAsyncStorage.getStore() | ||
if (workStore) { | ||
const prerenderStore = prerenderAsyncStorage.getStore() | ||
if ( | ||
prerenderStore && | ||
prerenderStore.type === 'prerender' && | ||
isDynamicIOPrerender(prerenderStore) | ||
) { | ||
abortOnSynchronousDynamicDataAccess( | ||
workStore.route, | ||
'`Math.random()`', | ||
prerenderStore | ||
) | ||
} | ||
} | ||
|
||
return originalRandom.apply(this, arguments as any) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,5 @@ | ||
// This file should be imported before any others. It sets up the environment | ||
// for later imports to work properly. | ||
|
||
// expose AsyncLocalStorage on global for react usage if it isn't already provided by the environment | ||
if (typeof (globalThis as any).AsyncLocalStorage !== 'function') { | ||
const { AsyncLocalStorage } = require('async_hooks') | ||
;(globalThis as any).AsyncLocalStorage = AsyncLocalStorage | ||
} | ||
|
||
if (typeof (globalThis as any).WebSocket !== 'function') { | ||
Object.defineProperty(globalThis, 'WebSocket', { | ||
get() { | ||
return require('next/dist/compiled/ws').WebSocket | ||
}, | ||
}) | ||
} | ||
import './node-environment-baseline' | ||
import './node-environment-extensions/random' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.platform-dynamic.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import { nextTestSetup } from 'e2e-utils' | ||
|
||
const WITH_PPR = !!process.env.__NEXT_EXPERIMENTAL_PPR | ||
|
||
const stackStart = /\s+at / | ||
|
||
function createExpectError(cliOutput: string) { | ||
let cliIndex = 0 | ||
return function expectError( | ||
containing: string, | ||
withStackContaining?: string | ||
) { | ||
const initialCliIndex = cliIndex | ||
let lines = cliOutput.slice(cliIndex).split('\n') | ||
|
||
let i = 0 | ||
while (i < lines.length) { | ||
let line = lines[i++] + '\n' | ||
cliIndex += line.length | ||
if (line.includes(containing)) { | ||
if (typeof withStackContaining !== 'string') { | ||
return | ||
} else { | ||
while (i < lines.length) { | ||
let stackLine = lines[i++] + '\n' | ||
if (!stackStart.test(stackLine)) { | ||
expect(stackLine).toContain(withStackContaining) | ||
} | ||
if (stackLine.includes(withStackContaining)) { | ||
return | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
expect(cliOutput.slice(initialCliIndex)).toContain(containing) | ||
} | ||
} | ||
|
||
function runTests(options: { withMinification: boolean }) { | ||
const isTurbopack = !!process.env.TURBOPACK | ||
const { withMinification } = options | ||
describe(`Dynamic IO Errors - ${withMinification ? 'With Minification' : 'Without Minification'}`, () => { | ||
describe('Sync Dynamic - With Fallback - Math.random()', () => { | ||
const { next, isNextDev, skipped } = nextTestSetup({ | ||
files: __dirname + '/fixtures/sync-random-with-fallback', | ||
skipStart: true, | ||
}) | ||
|
||
if (skipped) { | ||
return | ||
} | ||
|
||
if (isNextDev) { | ||
it('does not run in dev', () => {}) | ||
return | ||
} | ||
|
||
beforeEach(async () => { | ||
if (!withMinification) { | ||
await next.patchFile('next.config.js', (content) => | ||
content.replace( | ||
'serverMinification: true,', | ||
'serverMinification: false,' | ||
) | ||
) | ||
} | ||
}) | ||
|
||
it('should not error the build when calling Math.random() if all dynamic access is inside a Suspense boundary', async () => { | ||
try { | ||
await next.start() | ||
} catch { | ||
throw new Error('expected build not to fail for fully static project') | ||
} | ||
|
||
if (WITH_PPR) { | ||
expect(next.cliOutput).toContain('◐ / ') | ||
const $ = await next.render$('/') | ||
expect($('[data-fallback]').length).toBe(2) | ||
} else { | ||
expect(next.cliOutput).toContain('ƒ / ') | ||
const $ = await next.render$('/') | ||
expect($('[data-fallback]').length).toBe(0) | ||
} | ||
}) | ||
}) | ||
|
||
describe('Sync Dynamic - Without Fallback - Math.random()', () => { | ||
const { next, isNextDev, skipped } = nextTestSetup({ | ||
files: __dirname + '/fixtures/sync-random-without-fallback', | ||
skipStart: true, | ||
}) | ||
|
||
if (skipped) { | ||
return | ||
} | ||
|
||
if (isNextDev) { | ||
it('does not run in dev', () => {}) | ||
return | ||
} | ||
|
||
beforeEach(async () => { | ||
if (!withMinification) { | ||
await next.patchFile('next.config.js', (content) => | ||
content.replace( | ||
'serverMinification: true,', | ||
'serverMinification: false,' | ||
) | ||
) | ||
} | ||
}) | ||
|
||
it('should error the build if Math.random() happens before some component outside a Suspense boundary is complete', async () => { | ||
try { | ||
await next.start() | ||
} catch { | ||
// we expect the build to fail | ||
} | ||
const expectError = createExpectError(next.cliOutput) | ||
|
||
expectError( | ||
'Error: Route / used a synchronous Dynamic API: `Math.random()`, which caused this component to not finish rendering before the prerender completed and no fallback UI was defined.', | ||
// Turbopack doesn't support disabling minification yet | ||
withMinification || isTurbopack ? undefined : 'IndirectionTwo' | ||
) | ||
expectError('Error occurred prerendering page "/"') | ||
expectError( | ||
'Error: Route / used `Math.random()` while prerendering which caused some part of the page to be dynamic without a Suspense boundary above it defining a fallback UI.' | ||
) | ||
expectError('exiting the build.') | ||
}) | ||
}) | ||
}) | ||
} | ||
|
||
runTests({ withMinification: true }) | ||
runTests({ withMinification: false }) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
test/e2e/app-dir/dynamic-io-errors/fixtures/sync-random-with-fallback/app/indirection.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
'use client' | ||
|
||
export function IndirectionOne({ children }) { | ||
return children | ||
} | ||
|
||
export function IndirectionTwo({ children }) { | ||
return children | ||
} | ||
|
||
export function IndirectionThree({ children }) { | ||
return children | ||
} |
9 changes: 9 additions & 0 deletions
9
test/e2e/app-dir/dynamic-io-errors/fixtures/sync-random-with-fallback/app/layout.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export default function Root({ children }: { children: React.ReactNode }) { | ||
return ( | ||
<html> | ||
<body> | ||
<main>{children}</main> | ||
</body> | ||
</html> | ||
) | ||
} |
Oops, something went wrong.