| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| |
|
| | type ReportableResult = |
| | | ConsoleCallReport |
| | | ErrorReport |
| | | OutputReport |
| | | SerializableDataReport |
| |
|
| | type ConsoleCallReport = { |
| | type: 'console-call' |
| | method: string |
| | input: string |
| | } |
| |
|
| | type ErrorReport = { type: 'error'; message: string } |
| | type OutputReport = { type: 'output'; message: string } |
| | type SerializableDataReport = { |
| | type: 'serialized' |
| | key: string |
| | data: string | number | boolean |
| | } |
| |
|
| | declare global { |
| | function reportResult(result: ReportableResult): void |
| | } |
| |
|
| | import { Worker } from 'node:worker_threads' |
| |
|
| | type WorkerResult = { |
| | exitCode: number |
| | stderr: string |
| | consoleCalls: Array<{ method: string; input: string }> |
| | data: Record<string, unknown> |
| | messages: Array<ReportableResult> |
| | } |
| |
|
| | export function runWorkerCode(fn: Function): Promise<WorkerResult> { |
| | return new Promise((resolve, reject) => { |
| | const script = ` |
| | const { parentPort } = require('node:worker_threads'); |
| | (async () => { |
| | const { AsyncLocalStorage } = require('node:async_hooks'); |
| | // We need to put this on the global because Next.js does not import it |
| | // from node directly to be compatible with edge runtimes. |
| | globalThis.AsyncLocalStorage = AsyncLocalStorage; |
| | |
| | global.reportResult = (value) => { |
| | parentPort?.postMessage(value); |
| | }; |
| | |
| | const fn = (${fn.toString()}); |
| | try { |
| | const out = await fn(); |
| | await new Promise(r => setImmediate(r)); |
| | reportResult({ type: 'result', out }); |
| | } catch (e) { |
| | reportResult({ type: 'error', message: String(e && e.message || e) }); |
| | } |
| | })(); |
| | ` |
| |
|
| | const w = new Worker(script, { |
| | eval: true, |
| | workerData: null, |
| | argv: [], |
| | execArgv: ['--conditions=react-server'], |
| | stderr: true, |
| | stdout: false, |
| | env: { |
| | FORCE_COLOR: '1', |
| | }, |
| | }) |
| |
|
| | const messages: Array<ReportableResult> = [] |
| | const consoleCalls: Array<{ method: string; input: string }> = [] |
| | const data = {} as Record<string, unknown> |
| | let stderr = '' |
| |
|
| | w.on('message', (m) => { |
| | messages.push(m) |
| | switch (m.type) { |
| | case 'console-call': |
| | consoleCalls.push({ |
| | method: m.method, |
| | input: m.input, |
| | }) |
| | break |
| | case 'serialized': |
| | data[m.key] = JSON.parse(m.data) |
| | break |
| | default: |
| | break |
| | } |
| | }) |
| | w.on('error', (err) => console.error('Worker error', err)) |
| | w.on('error', reject) |
| | w.stderr?.on('data', (b) => (stderr += String(b))) |
| | w.on('exit', (code) => |
| | resolve({ |
| | exitCode: code ?? -1, |
| | consoleCalls, |
| | data, |
| | messages, |
| | stderr, |
| | }) |
| | ) |
| | }) |
| | } |
| |
|
| | describe('console-exit patches', () => { |
| | describe('basic functionality', () => { |
| | it('should patch console methods to dim when instructed', async () => { |
| | async function testForWorker() { |
| | const { |
| | consoleAsyncStorage, |
| | } = require('next/dist/server/app-render/console-async-storage.external') |
| |
|
| | |
| | console.log = function (...args) { |
| | let dimmed = false |
| | if ( |
| | args.find((a) => |
| | typeof a === 'string' ? a.includes('color:') : false |
| | ) |
| | ) { |
| | dimmed = true |
| | } |
| | reportResult({ |
| | type: 'console-call', |
| | method: 'log', |
| | input: `${dimmed ? '[DIM]' : '[BRIGHT]'}: ${args.join(' ')}`, |
| | }) |
| | } |
| |
|
| | |
| | require('next/dist/server/node-environment-extensions/console-dim.external') |
| |
|
| | |
| | console.log('outside') |
| |
|
| | consoleAsyncStorage.run({ dim: true }, () => { |
| | console.log('inside') |
| | }) |
| | } |
| |
|
| | const { consoleCalls, exitCode } = await runWorkerCode(testForWorker) |
| |
|
| | expect(exitCode).toBe(0) |
| | |
| | expect(consoleCalls).toEqual([ |
| | { method: 'log', input: '[BRIGHT]: outside' }, |
| | |
| | { method: 'log', input: expect.stringMatching(/\[DIM\].*inside/) }, |
| | ]) |
| | }) |
| |
|
| | it('should not wrap console methods assigned after patching', async () => { |
| | async function testForWorker() { |
| | const { |
| | consoleAsyncStorage, |
| | } = require('next/dist/server/app-render/console-async-storage.external') |
| |
|
| | |
| | require('next/dist/server/node-environment-extensions/console-dim.external') |
| |
|
| | |
| | console.log = function (...args) { |
| | let dimmed = false |
| | if ( |
| | args.find((a) => |
| | typeof a === 'string' ? a.includes('color:') : false |
| | ) |
| | ) { |
| | dimmed = true |
| | } |
| | reportResult({ |
| | type: 'console-call', |
| | method: 'log', |
| | input: `${dimmed ? '[DIM]' : '[BRIGHT]'}: ${args.join(' ')}`, |
| | }) |
| | } |
| |
|
| | |
| | console.log('outside') |
| |
|
| | consoleAsyncStorage.run({ dim: true }, () => { |
| | console.log('inside') |
| | }) |
| | } |
| |
|
| | const { consoleCalls, exitCode } = await runWorkerCode(testForWorker) |
| |
|
| | expect(exitCode).toBe(0) |
| | |
| | expect(consoleCalls).toEqual([ |
| | { method: 'log', input: '[BRIGHT]: outside' }, |
| | { method: 'log', input: '[BRIGHT]: inside' }, |
| | ]) |
| | }) |
| |
|
| | it('should preserve function properties and behavior', async () => { |
| | async function testForWorker() { |
| | reportResult({ |
| | type: 'serialized', |
| | key: 'originalName', |
| | data: JSON.stringify(console.log.name), |
| | }) |
| |
|
| | reportResult({ |
| | type: 'serialized', |
| | key: 'originalLength', |
| | data: JSON.stringify(console.log.length), |
| | }) |
| |
|
| | |
| | require('next/dist/server/node-environment-extensions/console-dim.external') |
| |
|
| | |
| | reportResult({ |
| | type: 'serialized', |
| | key: 'patchedName', |
| | data: JSON.stringify(console.log.name), |
| | }) |
| |
|
| | reportResult({ |
| | type: 'serialized', |
| | key: 'patchedLength', |
| | data: JSON.stringify(console.log.length), |
| | }) |
| | } |
| |
|
| | const { data, exitCode } = await runWorkerCode(testForWorker) |
| |
|
| | expect(exitCode).toBe(0) |
| | expect(data.patchedName).toBe('log') |
| | expect(data.patchedName).toBe(data.originalName) |
| | expect(data.patchedLength).toBe(data.originalLength) |
| | }) |
| |
|
| | it('should enter a dimming context when a prerender store exists with an aborted renderSignal', async () => { |
| | async function testForWorker() { |
| | const { |
| | workUnitAsyncStorage, |
| | } = require('next/dist/server/app-render/work-unit-async-storage.external') |
| |
|
| | |
| | console.log = function (...args) { |
| | let dimmed = false |
| | if ( |
| | args.find((a) => |
| | typeof a === 'string' ? a.includes('color:') : false |
| | ) |
| | ) { |
| | dimmed = true |
| | } |
| | reportResult({ |
| | type: 'console-call', |
| | method: 'log', |
| | input: `${dimmed ? '[DIM]' : '[BRIGHT]'}: ${args.join(' ')}`, |
| | }) |
| | } |
| |
|
| | |
| | require('next/dist/server/node-environment-extensions/console-dim.external') |
| |
|
| | |
| | console.log('outside') |
| |
|
| | const controller = new AbortController() |
| |
|
| | workUnitAsyncStorage.run( |
| | { type: 'prerender', renderSignal: controller.signal }, |
| | () => { |
| | console.log('before abort') |
| | controller.abort() |
| | console.log('after abort') |
| | } |
| | ) |
| | } |
| |
|
| | const { consoleCalls, exitCode } = await runWorkerCode(testForWorker) |
| |
|
| | expect(exitCode).toBe(0) |
| | |
| | expect(consoleCalls).toEqual([ |
| | { method: 'log', input: '[BRIGHT]: outside' }, |
| | { method: 'log', input: '[BRIGHT]: before abort' }, |
| | { method: 'log', input: expect.stringMatching(/\[DIM\].*after abort/) }, |
| | ]) |
| | }) |
| |
|
| | it('should enter a dimming context when a react cacheSignal exists and is aborted', async () => { |
| | async function testForWorker() { |
| | const originalLog = console.log |
| | let controller: null | AbortController = new AbortController() |
| |
|
| | |
| | console.log = function (...args) { |
| | originalLog(...args) |
| | let dimmed = false |
| | if ( |
| | args.find((a) => |
| | typeof a === 'string' ? a.includes('color:') : false |
| | ) |
| | ) { |
| | dimmed = true |
| | } |
| | reportResult({ |
| | type: 'console-call', |
| | method: 'log', |
| | input: `${dimmed ? '[DIM]' : '[BRIGHT]'}: ${args.join(' ')}`, |
| | }) |
| | } |
| |
|
| | |
| | require('next/dist/server/node-environment-extensions/console-dim.external') |
| |
|
| | const { |
| | registerServerReact, |
| | registerClientReact, |
| | } = require('next/dist/server/runtime-reacts.external') |
| |
|
| | registerServerReact({ |
| | cacheSignal() { |
| | return controller?.signal |
| | }, |
| | }) |
| | registerClientReact({ |
| | cacheSignal() { |
| | return null |
| | }, |
| | }) |
| |
|
| | console.log('before abort') |
| | controller.abort() |
| | console.log('after abort') |
| |
|
| | controller = null |
| | console.log('with null signal') |
| | } |
| |
|
| | const { consoleCalls, exitCode } = await runWorkerCode(testForWorker) |
| |
|
| | expect(exitCode).toBe(0) |
| | expect(consoleCalls).toEqual([ |
| | { method: 'log', input: '[BRIGHT]: before abort' }, |
| | { method: 'log', input: expect.stringMatching(/\[DIM\].*after abort/) }, |
| | { method: 'log', input: '[BRIGHT]: with null signal' }, |
| | ]) |
| | }) |
| | }) |
| | }) |
| |
|