diff --git a/.changeset/stupid-walls-heal.md b/.changeset/stupid-walls-heal.md new file mode 100644 index 000000000..139e7de07 --- /dev/null +++ b/.changeset/stupid-walls-heal.md @@ -0,0 +1,5 @@ +--- +'houdini-react': patch +--- + +Fix invalid import during dev diff --git a/.prettierignore b/.prettierignore index de2379ec4..5ea4312f7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,4 @@ packages/*/package.json packages/houdini-svelte/src/runtime/index.ts packages/create-houdini/**/houdini.config.js +packages/houdini-react/src/runtime/server/**/* diff --git a/e2e/react/package.json b/e2e/react/package.json index 28aef5c0c..d0145947d 100644 --- a/e2e/react/package.json +++ b/e2e/react/package.json @@ -24,8 +24,7 @@ "houdini-adapter-cloudflare": "workspace:^", "houdini-react": "workspace:^", "react": "^18.3.0-canary-d6dcad6a8-20230914", - "react-dom": "^18.3.0-canary-d6dcad6a8-20230914", - "react-streaming": "^0.3.10" + "react-dom": "^18.3.0-canary-d6dcad6a8-20230914" }, "devDependencies": { "@types/react": "^18.0.27", diff --git a/packages/create-houdini/templates/react-typescript/package.json b/packages/create-houdini/templates/react-typescript/package.json index b3f23754f..b79fec5e4 100644 --- a/packages/create-houdini/templates/react-typescript/package.json +++ b/packages/create-houdini/templates/react-typescript/package.json @@ -13,7 +13,6 @@ "houdini-react": "^HOUDINI_VERSION", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-streaming": "^0.3.14", "graphql-yoga": "4.0.4", "graphql": "15.8.0", "@whatwg-node/server": "^0.9.14" diff --git a/packages/create-houdini/templates/react/package.json b/packages/create-houdini/templates/react/package.json index 5f393ca47..95f2439a1 100644 --- a/packages/create-houdini/templates/react/package.json +++ b/packages/create-houdini/templates/react/package.json @@ -13,7 +13,6 @@ "houdini-react": "^HOUDINI_VERSION", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-streaming": "^0.3.14", "graphql-yoga": "4.0.4", "graphql": "15.8.0", "@whatwg-node/server": "^0.9.14" diff --git a/packages/houdini-adapter-cloudflare/package.json b/packages/houdini-adapter-cloudflare/package.json index 184385be4..de0a545b8 100644 --- a/packages/houdini-adapter-cloudflare/package.json +++ b/packages/houdini-adapter-cloudflare/package.json @@ -37,8 +37,7 @@ "houdini": "workspace:^", "itty-router": "^4.0.23", "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-streaming": "^0.3.14" + "react-dom": "^18.2.0" }, "files": [ "build" diff --git a/packages/houdini-react/package.json b/packages/houdini-react/package.json index 8235287f8..1d10c9e64 100644 --- a/packages/houdini-react/package.json +++ b/packages/houdini-react/package.json @@ -25,7 +25,7 @@ "@types/cookies": "^0.7.7", "@types/estraverse": "^5.1.2", "@types/express": "^4.17.17", - "@types/react-dom": "^18.0.10", + "@types/react-dom": "^18.0.11", "next": "^13.0.1", "scripts": "workspace:^" }, @@ -43,7 +43,7 @@ "graphql-yoga": "^4.0.4", "houdini": "workspace:^", "react": "^18.2.0", - "react-streaming": "^0.3.9", + "react-dom": "^18.2.0", "recast": "^0.23.1", "rollup": "^3.7.4", "use-deep-compare-effect": "^1.8.1" @@ -64,4 +64,4 @@ }, "main": "./build/plugin-cjs/index.js", "types": "./build/plugin/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/houdini-react/src/plugin/codegen/render.ts b/packages/houdini-react/src/plugin/codegen/render.ts index 1badaaf66..293726a82 100644 --- a/packages/houdini-react/src/plugin/codegen/render.ts +++ b/packages/houdini-react/src/plugin/codegen/render.ts @@ -36,7 +36,7 @@ export default (props) => ( import { Cache } from '$houdini/runtime/cache/cache' import { serverAdapterFactory, _serverHandler } from '$houdini/runtime/router/server' import { HoudiniClient } from '$houdini/runtime/client' -import { renderToStream } from 'react-streaming/server' +import { renderToStream } from '$houdini/plugins/houdini-react/runtime/server' import React from 'react' import { router_cache } from '../../runtime/routing' diff --git a/packages/houdini-react/src/plugin/vite.tsx b/packages/houdini-react/src/plugin/vite.tsx index 89c22fb37..293a9b6e4 100644 --- a/packages/houdini-react/src/plugin/vite.tsx +++ b/packages/houdini-react/src/plugin/vite.tsx @@ -60,7 +60,6 @@ export default { assetFileNames: 'assets/[name].js', entryFileNames: '[name].js', }, - external: ['react-streaming/server'], }, } diff --git a/packages/houdini-react/src/runtime/index.tsx b/packages/houdini-react/src/runtime/index.tsx index 98dc43f80..4e9add957 100644 --- a/packages/houdini-react/src/runtime/index.tsx +++ b/packages/houdini-react/src/runtime/index.tsx @@ -17,11 +17,13 @@ export function Router({ last_variables, session, assetPrefix, + injectToStream, }: { initialURL: string cache: Cache session?: App.Session assetPrefix: string + injectToStream?: (chunk: string) => void } & RouterCache) { return ( - + ) } diff --git a/packages/houdini-react/src/runtime/routing/Router.tsx b/packages/houdini-react/src/runtime/routing/Router.tsx index ff86e849b..7cef3577d 100644 --- a/packages/houdini-react/src/runtime/routing/Router.tsx +++ b/packages/houdini-react/src/runtime/routing/Router.tsx @@ -7,7 +7,6 @@ import { QueryArtifact } from '$houdini/runtime/lib/types' import { find_match } from '$houdini/runtime/router/match' import type { RouterManifest, RouterPageManifest } from '$houdini/runtime/router/types' import React from 'react' -import { useStream } from 'react-streaming' import { useDocumentStore } from '../hooks/useDocumentStore' import { SuspenseCache, suspense_cache } from './cache' @@ -35,10 +34,12 @@ export function Router({ manifest, initialURL, assetPrefix, + injectToStream, }: { manifest: RouterManifest initialURL?: string assetPrefix: string + injectToStream?: undefined | ((chunk: string) => void) }) { // the current route is just a string in state. const [current, setCurrent] = React.useState(() => { @@ -58,6 +59,7 @@ export function Router({ page, variables, assetPrefix, + injectToStream, }) // if we get this far, it's safe to load the component const { component_cache } = useRouterContext() @@ -129,10 +131,12 @@ function usePageData({ page, variables, assetPrefix, + injectToStream, }: { page: RouterPageManifest variables: GraphQLVariables assetPrefix: string + injectToStream: undefined | ((chunk: string) => void) }): { loadData: (page: RouterPageManifest, variables: {} | null) => void loadComponent: (page: RouterPageManifest) => void @@ -148,9 +152,6 @@ function usePageData({ last_variables, } = useRouterContext() - // get a reference to the current stream - const stream = useStream() - // grab the current session value const session = useSession() @@ -188,7 +189,7 @@ function usePageData({ // if we are building up a stream (on the server), we want to add something // to the client that resolves the pending request with the // data that we just got - stream?.injectToStream(` + injectToStream?.(` `) diff --git a/packages/houdini-react/src/runtime/server/index.ts b/packages/houdini-react/src/runtime/server/index.ts new file mode 100644 index 000000000..51a90ef91 --- /dev/null +++ b/packages/houdini-react/src/runtime/server/index.ts @@ -0,0 +1,19 @@ +/** +This directory is largely copied from https://github.com/brillout/react-streaming and adapted to fit the needs of this project. It is subject to the MIT license, found [here](https://github.com/brillout/react-streaming/blob/main/LICENSE.md). Duplicated below for reference: + +MIT License + +Copyright (c) 2022-present Romuald Brillout + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +// We don't import from ./utils.ts because utils/debug.js contains a !isBrowser() assertion +import { renderToStream } from './renderToStream' +import type { InjectToStream } from './renderToStream/createBuffer' + +export { renderToStream } +export type { InjectToStream } diff --git a/packages/houdini-react/src/runtime/server/renderToStream.ts b/packages/houdini-react/src/runtime/server/renderToStream.ts new file mode 100644 index 000000000..565e24f2a --- /dev/null +++ b/packages/houdini-react/src/runtime/server/renderToStream.ts @@ -0,0 +1,228 @@ +import React from 'react' +import type { + renderToPipeableStream as RenderToPipeableStream, + renderToReadableStream as RenderToReadableStream, +} from 'react-dom/server' + +import { createPipeWrapper, Pipe } from './renderToStream/createPipeWrapper' +import { createReadableWrapper } from './renderToStream/createReadableWrapper' +import { nodeStreamModuleIsAvailable } from './renderToStream/loadNodeStreamModule' +import { resolveSeoStrategy, SeoStrategy } from './renderToStream/resolveSeoStrategy' +import { createDebugger } from './utils' + +export { renderToStream } +export { disable } + +const debug = createDebugger('react-streaming:flow') + +type Options = { + webStream?: boolean + disable?: boolean + seoStrategy?: SeoStrategy + userAgent?: string + onBoundaryError?: (err: unknown) => void + renderToReadableStream?: typeof RenderToReadableStream + renderToPipeableStream?: typeof RenderToPipeableStream +} +type Result = ( + | { + pipe: Pipe + readable: null + } + | { + pipe: null + readable: ReadableStream + } +) & { + streamEnd: Promise + disabled: boolean + injectToStream: (chunk: unknown) => void +} + +const globalConfig: { disable: boolean } = ((globalThis as any).__react_streaming = ( + globalThis as any +).__react_streaming || { + disable: false, +}) +function disable() { + globalConfig.disable = true +} + +async function renderToStream(element: React.ReactNode, options: Options = {}): Promise { + const buffer: unknown[] = [] + let injectToStream: (chunk: unknown) => void = (chunk) => buffer.push(chunk) + element = React.cloneElement(element as React.ReactElement, { injectToStream }) + + const disable = + globalConfig.disable || (options.disable ?? resolveSeoStrategy(options).disableStream) + const webStream = options.webStream ?? !(await nodeStreamModuleIsAvailable()) + debug(`disable === ${disable} && webStream === ${webStream}`) + + let result: Result + const resultPartial: Pick = { disabled: disable } + if (!webStream) { + result = { ...resultPartial, ...(await renderToNodeStream(element, disable, options)) } + } else { + result = { ...resultPartial, ...(await renderToWebStream(element, disable, options)) } + } + + injectToStream = result.injectToStream + buffer.forEach((chunk) => injectToStream(chunk)) + buffer.length = 0 + + debug('promise `await renderToStream()` resolved') + return result +} + +async function renderToNodeStream( + element: React.ReactNode, + disable: boolean, + options: { + debug?: boolean + onBoundaryError?: (err: unknown) => void + renderToPipeableStream?: typeof RenderToPipeableStream + } +) { + debug('creating Node.js Stream Pipe') + + let onAllReady!: () => void + const allReady = new Promise((r) => { + onAllReady = () => r() + }) + let onShellReady!: () => void + const shellReady = new Promise((r) => { + onShellReady = () => r() + }) + + let didError = false + let firstErr: unknown = null + let reactBug: unknown = null + const onError = (err: unknown) => { + debug('[react] onError() / onShellError()') + didError = true + firstErr ??= err + onShellReady() + afterReactBugCatch(() => { + // Is not a React internal error (i.e. a React bug) + if (err !== reactBug) { + options.onBoundaryError?.(err) + } + }) + } + const renderToPipeableStream = + options.renderToPipeableStream ?? + // @ts-ignore + // We don't directly use import() because it shouldn't be bundled for Cloudflare Workers: the module react-dom/server.node contains a require('stream') which fails on Cloudflare Workers + ((await import('react-dom/server.node')) + .renderToPipeableStream as typeof RenderToPipeableStream) + + const { pipe: pipeOriginal } = renderToPipeableStream(element, { + onShellReady() { + debug('[react] onShellReady()') + onShellReady() + }, + onAllReady() { + debug('[react] onAllReady()') + onShellReady() + onAllReady() + }, + onShellError: onError, + onError, + }) + let promiseResolved = false + const { pipeForUser, injectToStream, streamEnd } = await createPipeWrapper(pipeOriginal, { + onReactBug(err) { + debug('react bug') + didError = true + firstErr ??= err + reactBug = err + // Only log if it wasn't used as rejection for `await renderToStream()` + if (reactBug !== firstErr || promiseResolved) { + console.error(reactBug) + } + }, + }) + await shellReady + if (didError) throw firstErr + if (disable) await allReady + if (didError) throw firstErr + promiseResolved = true + return { + pipe: pipeForUser, + readable: null, + streamEnd: wrapStreamEnd(streamEnd, didError), + injectToStream, + } +} +async function renderToWebStream( + element: React.ReactNode, + disable: boolean, + options: { + debug?: boolean + onBoundaryError?: (err: unknown) => void + renderToReadableStream?: typeof RenderToReadableStream + } +) { + debug('creating Web Stream Pipe') + + let didError = false + let firstErr: unknown = null + let reactBug: unknown = null + const onError = (err: unknown) => { + didError = true + firstErr = firstErr || err + afterReactBugCatch(() => { + // Is not a React internal error (i.e. a React bug) + if (err !== reactBug) { + options.onBoundaryError?.(err) + } + }) + } + const renderToReadableStream = + options.renderToReadableStream ?? + // We directly use import() because it needs to be bundled for Cloudflare Workers + ((await import('react-dom/server.browser' as string)) + .renderToReadableStream as typeof RenderToReadableStream) + + const readableOriginal = await renderToReadableStream(element, { onError }) + const { allReady } = readableOriginal + let promiseResolved = false + // Upon React internal errors (i.e. React bugs), React rejects `allReady`. + // React doesn't reject `allReady` upon boundary errors. + allReady.catch((err) => { + debug('react bug') + didError = true + firstErr = firstErr || err + reactBug = err + // Only log if it wasn't used as rejection for `await renderToStream()` + if (reactBug !== firstErr || promiseResolved) { + console.error(reactBug) + } + }) + if (didError) throw firstErr + if (disable) await allReady + if (didError) throw firstErr + const { readableForUser, streamEnd, injectToStream } = createReadableWrapper(readableOriginal) + promiseResolved = true + return { + readable: readableForUser, + pipe: null, + streamEnd: wrapStreamEnd(streamEnd, didError), + injectToStream, + } +} + +// Needed for the hacky solution to workaround https://github.com/facebook/react/issues/24536 +function afterReactBugCatch(fn: Function) { + setTimeout(() => { + fn() + }, 0) +} +function wrapStreamEnd(streamEnd: Promise, didError: boolean): Promise { + return ( + streamEnd + // Needed because of the `afterReactBugCatch()` hack above, otherwise `onBoundaryError` triggers after `streamEnd` resolved + .then(() => new Promise((r) => setTimeout(r, 0))) + .then(() => !didError) + ) +} diff --git a/packages/houdini-react/src/runtime/server/renderToStream/createBuffer.ts b/packages/houdini-react/src/runtime/server/renderToStream/createBuffer.ts new file mode 100644 index 000000000..5476afec4 --- /dev/null +++ b/packages/houdini-react/src/runtime/server/renderToStream/createBuffer.ts @@ -0,0 +1,94 @@ +import { assert, assertUsage, createDebugger } from '../utils' + +export { createBuffer } +export type { InjectToStream } +export type { StreamOperations } + +const debug = createDebugger('react-streaming:buffer') + +type InjectToStream = (chunk: unknown, options?: { flush?: boolean }) => void +type StreamOperations = { + operations: null | { writeChunk: (chunk: unknown) => void; flush: null | (() => void) } +} + +function createBuffer(streamOperations: StreamOperations): { + injectToStream: InjectToStream + onBeforeWrite: (chunk: unknown) => void + onBeforeEnd: () => void +} { + const buffer: { chunk: unknown; flush: undefined | boolean }[] = [] + let state: 'UNSTARTED' | 'STREAMING' | 'ENDED' = 'UNSTARTED' + let writePermission: null | boolean = null // Set to `null` because React fails to hydrate if something is injected before the first react write + + return { injectToStream, onBeforeWrite, onBeforeEnd } + + function injectToStream(chunk: unknown, options?: { flush?: boolean }) { + assertUsage( + state !== 'ENDED', + `Cannot inject following chunk after stream has ended: \`${chunk}\`` + ) + if (debug.isEnabled) { + debug('injectToStream()', String(chunk)) + } + buffer.push({ chunk, flush: options?.flush }) + flushBuffer() + } + + function flushBuffer() { + if (!writePermission) { + return + } + if (buffer.length === 0) { + return + } + if (state !== 'STREAMING') { + assert(state === 'UNSTARTED') + return + } + let flushStream = false + buffer.forEach((bufferEntry) => { + assert(streamOperations.operations) + const { writeChunk } = streamOperations.operations + writeChunk(bufferEntry.chunk) + if (bufferEntry.flush) { + flushStream = true + } + }) + buffer.length = 0 + assert(streamOperations.operations) + if (flushStream && streamOperations.operations.flush !== null) { + streamOperations.operations.flush() + debug('stream flushed') + } + } + + function onBeforeWrite(chunk: unknown) { + state === 'UNSTARTED' && debug('>>> START') + if (debug.isEnabled) { + debug(`react write${!writePermission ? '' : ' (allowed)'}`, String(chunk)) + } + state = 'STREAMING' + if (writePermission) { + flushBuffer() + } + if (writePermission == true || writePermission === null) { + writePermission = false + debug('writePermission =', writePermission) + setTimeout(() => { + debug('>>> setTimeout()') + writePermission = true + debug('writePermission =', writePermission) + flushBuffer() + }) + } + } + + function onBeforeEnd() { + writePermission = true + debug('writePermission =', writePermission) + flushBuffer() + assert(buffer.length === 0) + state = 'ENDED' + debug('>>> END') + } +} diff --git a/packages/houdini-react/src/runtime/server/renderToStream/createPipeWrapper.ts b/packages/houdini-react/src/runtime/server/renderToStream/createPipeWrapper.ts new file mode 100644 index 000000000..75134eedc --- /dev/null +++ b/packages/houdini-react/src/runtime/server/renderToStream/createPipeWrapper.ts @@ -0,0 +1,83 @@ +import type { Writable as StreamNodeWritable } from 'stream' + +import { createDebugger } from '../utils' +import { createBuffer, StreamOperations } from './createBuffer' +import { loadNodeStreamModule } from './loadNodeStreamModule' + +export { createPipeWrapper } +export type { Pipe } + +const debug = createDebugger('react-streaming:createPipeWrapper') + +// `pipeFromReact` is the pipe provided by React +// `pipeForUser` is the pipe we give to the user will (the wrapper) +// `writableFromUser` is the writable provided by the user (i.e. `pipeForUser(writableFromUser)`), for example a Express.js's `res` writable stream. +// `writableForReact` is the writable that React directly writes to. +// Essentially: what React writes to `writableForReact` is piped to `writableFromUser` + +type Pipe = (writable: StreamNodeWritable) => void + +async function createPipeWrapper( + pipeFromReact: Pipe, + { onReactBug }: { onReactBug: (err: unknown) => void } +) { + const { Writable } = await loadNodeStreamModule() + const { pipeForUser, streamEnd } = createPipeForUser() + const streamOperations: StreamOperations = { + operations: null, + } + const { injectToStream, onBeforeWrite, onBeforeEnd } = createBuffer(streamOperations) + return { pipeForUser, streamEnd, injectToStream } + + function createPipeForUser(): { pipeForUser: Pipe; streamEnd: Promise } { + debug('createPipeForUser()') + let onEnded!: () => void + const streamEnd = new Promise((r) => { + onEnded = () => r() + }) + const pipeForUser: Pipe = (writableFromUser: StreamNodeWritable) => { + const writableForReact = new Writable({ + write(chunk: unknown, encoding, callback) { + debug('write') + onBeforeWrite(chunk) + if (!writableFromUser.destroyed) { + writableFromUser.write(chunk, encoding, callback) + } else { + // Destroying twice is fine: https://github.com/brillout/react-streaming/pull/21#issuecomment-1554517163 + writableForReact.destroy() + } + }, + final(callback) { + debug('final') + onBeforeEnd() + writableFromUser.end() + onEnded() + callback() + }, + destroy(err) { + debug(`destroy (\`!!err === ${!!err}\`)`) + // Upon React internal errors (i.e. React bugs), React destroys the stream. + if (err) onReactBug(err) + writableFromUser.destroy(err ?? undefined) + onEnded() + }, + }) + const flush = () => { + if (typeof (writableFromUser as any).flush === 'function') { + ;(writableFromUser as any).flush() + debug('stream flushed (Node.js Writable)') + } + } + streamOperations.operations = { + flush, + writeChunk(chunk: unknown) { + writableFromUser.write(chunk) + }, + } + // Forward the flush() call. E.g. used by React to flush GZIP buffers, see https://github.com/brillout/vite-plugin-ssr/issues/466#issuecomment-1269601710 + ;(writableForReact as any).flush = flush + pipeFromReact(writableForReact) + } + return { pipeForUser, streamEnd } + } +} diff --git a/packages/houdini-react/src/runtime/server/renderToStream/createReadableWrapper.ts b/packages/houdini-react/src/runtime/server/renderToStream/createReadableWrapper.ts new file mode 100644 index 000000000..40a9a2a7d --- /dev/null +++ b/packages/houdini-react/src/runtime/server/renderToStream/createReadableWrapper.ts @@ -0,0 +1,71 @@ +import { createBuffer, StreamOperations } from './createBuffer' + +export { createReadableWrapper } + +// `readableFromReact` is the readable stream provided by React +// `readableForUser` is the readable stream we give to the user (the wrapper) +// Essentially: what React writes to `readableFromReact` is forwarded to `readableForUser` + +function createReadableWrapper(readableFromReact: ReadableStream) { + const streamOperations: StreamOperations = { + operations: null, + } + let controllerOfUserStream: ReadableStreamController + let onEnded!: () => void + const streamEnd = new Promise((r) => { + onEnded = () => r() + }) + const readableForUser = new ReadableStream({ + start(controller) { + controllerOfUserStream = controller + onReady(onEnded) + }, + }) + const { injectToStream, onBeforeWrite, onBeforeEnd } = createBuffer(streamOperations) + return { readableForUser, streamEnd, injectToStream } + + async function onReady(onEnded: () => void) { + streamOperations.operations = { + writeChunk(chunk) { + controllerOfUserStream.enqueue(encodeForWebStream(chunk) as any) + }, + flush: null, + } + + const reader = readableFromReact.getReader() + + while (true) { + let result: ReadableStreamReadResult + try { + result = await reader.read() + } catch (err) { + controllerOfUserStream.close() + throw err + } + const { value, done } = result + if (done) { + break + } + onBeforeWrite(value) + streamOperations.operations.writeChunk(value) + } + + // Collect `injectToStream()` calls stuck in an async call + setTimeout(() => { + onBeforeEnd() + controllerOfUserStream.close() + onEnded() + }, 0) + } +} + +let encoder: TextEncoder +function encodeForWebStream(thing: unknown) { + if (!encoder) { + encoder = new TextEncoder() + } + if (typeof thing === 'string') { + return encoder.encode(thing) + } + return thing +} diff --git a/packages/houdini-react/src/runtime/server/renderToStream/loadNodeStreamModule.ts b/packages/houdini-react/src/runtime/server/renderToStream/loadNodeStreamModule.ts new file mode 100644 index 000000000..589bc8480 --- /dev/null +++ b/packages/houdini-react/src/runtime/server/renderToStream/loadNodeStreamModule.ts @@ -0,0 +1,26 @@ +import type { Readable as StreamNodeReadable, Writable as StreamNodeWritable } from 'stream' + +export { loadNodeStreamModule } +export { nodeStreamModuleIsAvailable } + +type StreamModule = { + Readable: typeof StreamNodeReadable + Writable: typeof StreamNodeWritable +} + +async function loadNodeStreamModule(): Promise { + const streamModule = await loadModule() + const { Readable, Writable } = streamModule + return { Readable, Writable } +} +async function nodeStreamModuleIsAvailable(): Promise { + try { + await loadModule() + return true + } catch (err) { + return false + } +} +function loadModule() { + return import('stream') as Promise +} diff --git a/packages/houdini-react/src/runtime/server/renderToStream/resolveSeoStrategy.ts b/packages/houdini-react/src/runtime/server/renderToStream/resolveSeoStrategy.ts new file mode 100644 index 000000000..321b16292 --- /dev/null +++ b/packages/houdini-react/src/runtime/server/renderToStream/resolveSeoStrategy.ts @@ -0,0 +1,27 @@ +// https://github.com/omrilotan/isbot +// https://stackoverflow.com/questions/34647657/how-to-detect-web-crawlers-for-seo-using-express/68869738#68869738 +import { assertWarning } from '../utils' + +export { resolveSeoStrategy } +export type { SeoStrategy } + +type SeoStrategy = 'conservative' | 'google-speed' +function resolveSeoStrategy(options: { seoStrategy?: SeoStrategy; userAgent?: string } = {}): { + disableStream: boolean +} { + const seoStrategy: SeoStrategy = options.seoStrategy || 'conservative' + + if (!options.userAgent) { + assertWarning( + false, + 'Streaming disabled. Provide `options.userAgent` to enable streaming. (react-streaming needs the User Agent string in order to be able to disable streaming for bots, e.g. for Google Bot.) Or set `options.disable` to `true` to get rid of this warning.', + { onlyOnce: true } + ) + return { disableStream: true } + } + const isGoogleBot = options.userAgent.toLowerCase().includes('googlebot') + if (seoStrategy === 'google-speed' && isGoogleBot) { + return { disableStream: false } + } + return { disableStream: true } +} diff --git a/packages/houdini-react/src/runtime/server/shared/initData.ts b/packages/houdini-react/src/runtime/server/shared/initData.ts new file mode 100644 index 000000000..7a3427aeb --- /dev/null +++ b/packages/houdini-react/src/runtime/server/shared/initData.ts @@ -0,0 +1,5 @@ +export type { InitData } +export { initDataHtmlClass } + +type InitData = { value: unknown; key: string; elementId: string } +const initDataHtmlClass = 'react-streaming_initData' diff --git a/packages/houdini-react/src/runtime/server/shared/key.ts b/packages/houdini-react/src/runtime/server/shared/key.ts new file mode 100644 index 000000000..5fde53bfc --- /dev/null +++ b/packages/houdini-react/src/runtime/server/shared/key.ts @@ -0,0 +1,21 @@ +import { isCallable } from '../utils/isCallable' +import { assertUsage } from './utils' + +export { stringifyKey } +export { assertKey } + +function stringifyKey(key: unknown): string { + const keyString = JSON.stringify(key) + return keyString +} + +function assertKey(keyValue: unknown) { + assertUsage( + keyValue, + `[useAsync(key, asyncFn)] You provided a \`key\` with the value \`${keyValue}\` which is forbidden.` + ) + assertUsage( + !isCallable(keyValue), + `[useAsync(key, asyncFn)] You provided a \`key\` that is a function which is forbidden.` + ) +} diff --git a/packages/houdini-react/src/runtime/server/shared/utils.ts b/packages/houdini-react/src/runtime/server/shared/utils.ts new file mode 100644 index 000000000..855a38371 --- /dev/null +++ b/packages/houdini-react/src/runtime/server/shared/utils.ts @@ -0,0 +1,3 @@ +export * from '../utils/assert' +export * from '../utils/getGlobalVariable' +export * from '../utils/isPromise' diff --git a/packages/houdini-react/src/runtime/server/useStream.ts b/packages/houdini-react/src/runtime/server/useStream.ts new file mode 100644 index 000000000..8cd03c9e6 --- /dev/null +++ b/packages/houdini-react/src/runtime/server/useStream.ts @@ -0,0 +1,19 @@ +import React, { useContext } from 'react' + +import { assert } from './utils' + +export { useStream } +export { StreamProvider } +export type { StreamUtils } + +type StreamUtils = { + injectToStream: (htmlChunk: string) => void +} +const StreamContext = React.createContext(null) +const StreamProvider = StreamContext.Provider + +function useStream(): StreamUtils | null { + const streamUtils = useContext(StreamContext) + assert(streamUtils) + return streamUtils +} diff --git a/packages/houdini-react/src/runtime/server/utils.ts b/packages/houdini-react/src/runtime/server/utils.ts new file mode 100644 index 000000000..64890929c --- /dev/null +++ b/packages/houdini-react/src/runtime/server/utils.ts @@ -0,0 +1,4 @@ +export * from './utils/assert' +export * from './utils/debug' +export * from './utils/isPromise' +export * from './utils/isBrowser' diff --git a/packages/houdini-react/src/runtime/server/utils/assert.ts b/packages/houdini-react/src/runtime/server/utils/assert.ts new file mode 100644 index 000000000..468b301ad --- /dev/null +++ b/packages/houdini-react/src/runtime/server/utils/assert.ts @@ -0,0 +1,109 @@ +import { createErrorWithCleanStackTrace } from './createErrorWithCleanStackTrace' +import { projectInfo } from './projectInfo' + +export { assert } +export { assertUsage } +export { assertWarning } +export { assertInfo } +export { getProjectError } + +const errorPrefix = `[${projectInfo.npmPackageName}@${projectInfo.projectVersion}]` +const internalErrorPrefix = `${errorPrefix}[Bug]` +const usageErrorPrefix = `${errorPrefix}[Wrong Usage]` +const warningPrefix = `${errorPrefix}[Warning]` +const infoPrefix = `${errorPrefix}[Info]` + +const numberOfStackTraceLinesToRemove = 2 + +function assert(condition: unknown, debugInfo?: unknown): asserts condition { + if (condition) { + return + } + + const debugStr = (() => { + if (!debugInfo) { + return '' + } + const debugInfoSerialized = + typeof debugInfo === 'string' ? debugInfo : '`' + JSON.stringify(debugInfo) + '`' + return `Debug info (this is for the ${projectInfo.projectName} maintainers; you can ignore this): ${debugInfoSerialized}.` + })() + + const internalError = createErrorWithCleanStackTrace( + [ + `${internalErrorPrefix} You stumbled upon a bug in ${projectInfo.projectName}'s source code.`, + `Reach out at ${projectInfo.githubRepository}/issues/new or ${projectInfo.discordInviteToolChannel} and include this error stack (the error stack is usually enough to fix the problem).`, + 'A maintainer will fix the bug (usually under 24 hours).', + `Do not hesitate to reach out as it makes ${projectInfo.projectName} more robust.`, + debugStr, + ].join(' '), + numberOfStackTraceLinesToRemove + ) + + throw internalError +} + +function assertUsage(condition: unknown, errorMessage: string): asserts condition { + if (condition) { + return + } + const whiteSpace = errorMessage.startsWith('[') ? '' : ' ' + const usageError = createErrorWithCleanStackTrace( + `${usageErrorPrefix}${whiteSpace}${errorMessage}`, + numberOfStackTraceLinesToRemove + ) + throw usageError +} + +function getProjectError(errorMessage: string) { + const pluginError = createErrorWithCleanStackTrace( + `${errorPrefix} ${errorMessage}`, + numberOfStackTraceLinesToRemove + ) + return pluginError +} + +let alreadyLogged: Set = new Set() +function assertWarning( + condition: unknown, + errorMessage: string, + { onlyOnce, showStackTrace }: { onlyOnce: boolean | string; showStackTrace?: true } +): void { + if (condition) { + return + } + const msg = `${warningPrefix} ${errorMessage}` + if (onlyOnce) { + const key = onlyOnce === true ? msg : onlyOnce + if (alreadyLogged.has(key)) { + return + } else { + alreadyLogged.add(key) + } + } + if (showStackTrace) { + console.warn(new Error(msg)) + } else { + console.warn(msg) + } +} + +function assertInfo( + condition: unknown, + errorMessage: string, + { onlyOnce }: { onlyOnce: boolean } +): void { + if (condition) { + return + } + const msg = `${infoPrefix} ${errorMessage}` + if (onlyOnce) { + const key = msg + if (alreadyLogged.has(key)) { + return + } else { + alreadyLogged.add(key) + } + } + console.log(msg) +} diff --git a/packages/houdini-react/src/runtime/server/utils/createErrorWithCleanStackTrace.ts b/packages/houdini-react/src/runtime/server/utils/createErrorWithCleanStackTrace.ts new file mode 100644 index 000000000..9069bc8d7 --- /dev/null +++ b/packages/houdini-react/src/runtime/server/utils/createErrorWithCleanStackTrace.ts @@ -0,0 +1,57 @@ +export { createErrorWithCleanStackTrace } + +function createErrorWithCleanStackTrace( + errorMessage: string, + numberOfStackTraceLinesToRemove: number +) { + let err + { + var stackTraceLimit__original = Error.stackTraceLimit + Error.stackTraceLimit = Infinity + err = new Error(errorMessage) + Error.stackTraceLimit = stackTraceLimit__original + } + + err.stack = clean(err.stack, numberOfStackTraceLinesToRemove) + + return err +} + +function clean( + errStack: string | undefined, + numberOfStackTraceLinesToRemove: number +): string | undefined { + if (!errStack) { + return errStack + } + + const stackLines = splitByLine(errStack) + + let linesRemoved = 0 + + const stackLine__cleaned = stackLines + .filter((line) => { + // Remove internal stack traces + if (line.includes(' (internal/') || line.includes(' (node:internal')) { + return false + } + if (linesRemoved < numberOfStackTraceLinesToRemove && isStackTraceLine(line)) { + linesRemoved++ + return false + } + + return true + }) + .join('\n') + + return stackLine__cleaned +} + +function isStackTraceLine(line: string): boolean { + return line.startsWith(' at ') +} + +function splitByLine(str: string): string[] { + // https://stackoverflow.com/questions/21895233/how-in-node-to-split-string-by-newline-n + return str.split(/\r?\n/) +} diff --git a/packages/houdini-react/src/runtime/server/utils/debug.ts b/packages/houdini-react/src/runtime/server/utils/debug.ts new file mode 100644 index 000000000..3c6ad03e2 --- /dev/null +++ b/packages/houdini-react/src/runtime/server/utils/debug.ts @@ -0,0 +1,107 @@ +import { assert } from './assert' +import { isBrowser } from './isBrowser' +import { isCallable } from './isCallable' +import { objectAssign } from './objectAssign' + +export { createDebugger } +export { isDebugEnabled } +export type { Debug } + +// Ensure that this is never loaded in the browser. (In order to avoid this file to be included in the client-side bundle.) +// For isomorphic code: use `globalThis.createDebugger()` instead of `import { createDebugger } from './utils'`. +assert(!isBrowser(), 'utils/debug.js loaded in browser') +;(globalThis as any).__brillout_debug_createDebugger = createDebugger + +type Debug = ReturnType + +type Options = { + serialization?: { + emptyArray?: string + } +} + +function createDebugger(namespace: string, optionsGlobal?: Options) { + const debugWithOptions = (options: Options) => { + return (msg: string, info?: unknown) => { + if (!isDebugEnabled(namespace)) return + if (info !== undefined) { + msg += strInfo(info, { ...optionsGlobal, ...options }) + } + console.log('\x1b[1m%s\x1b[0m', namespace, msg) + } + } + const debug = (msg: string, info?: unknown) => debugWithOptions({})(msg, info) + objectAssign(debug, { options: debugWithOptions, isEnabled: isDebugEnabled(namespace) }) + return debug +} + +function isDebugEnabled(namespace: string): boolean { + let DEBUG: undefined | string + // - `process` can be undefined in edge workers + // - We want bundlers to be able to statically replace `process.env.*` + try { + DEBUG = process.env.DEBUG + } catch {} + return DEBUG?.includes(namespace) ?? false +} + +function strInfo(info: unknown, options: Options): string | undefined { + if (info === undefined) { + return undefined + } + + let str = '\n' + + if (typeof info === 'string') { + str += info + } else if (Array.isArray(info)) { + if (info.length === 0) { + str += options.serialization?.emptyArray ?? '[]' + } else { + str += info.map(strUnknown).join('\n') + } + } else { + str += strUnknown(info) + } + + str = pad(str) + + return str +} + +function pad(str: string): string { + const PADDING = ' ' + const WIDTH = process.stdout.columns as number | undefined + const lines: string[] = [] + str.split('\n').forEach((line) => { + if (!WIDTH) { + lines.push(line) + } else { + chunk(line, WIDTH - PADDING.length).forEach((chunk) => { + lines.push(chunk) + }) + } + }) + return lines.join('\n' + PADDING) +} +function chunk(str: string, size: number): string[] { + if (str.length <= size) { + return [str] + } + const chunks = str.match(new RegExp('.{1,' + size + '}', 'g')) + assert(chunks) + return chunks +} + +function strUnknown(thing: unknown) { + return typeof thing === 'string' ? thing : strObj(thing) +} +function strObj(obj: unknown, newLines = false) { + return JSON.stringify(obj, replaceFunctionSerializer, newLines ? 2 : undefined) +} +function replaceFunctionSerializer(this: Record, _key: string, value: unknown) { + if (isCallable(value)) { + return value.toString().split(/\s+/).join(' ') + } + return value +} diff --git a/packages/houdini-react/src/runtime/server/utils/getGlobalVariable.ts b/packages/houdini-react/src/runtime/server/utils/getGlobalVariable.ts new file mode 100644 index 000000000..44e15402e --- /dev/null +++ b/packages/houdini-react/src/runtime/server/utils/getGlobalVariable.ts @@ -0,0 +1,8 @@ +export function getGlobalVariable(key: string, defaultValue: T): T { + globalThis.__react_streaming = globalThis.__react_streaming || {} + globalThis.__react_streaming[key] = globalThis.__react_streaming[key] || defaultValue + return globalThis.__react_streaming[key] as T +} +declare global { + var __react_streaming: Record +} diff --git a/packages/houdini-react/src/runtime/server/utils/isBrowser.ts b/packages/houdini-react/src/runtime/server/utils/isBrowser.ts new file mode 100644 index 000000000..7aaba0086 --- /dev/null +++ b/packages/houdini-react/src/runtime/server/utils/isBrowser.ts @@ -0,0 +1,6 @@ +export { isBrowser } + +function isBrowser() { + // Using `typeof window !== 'undefined'` alone is not enough because some users use https://www.npmjs.com/package/ssr-window + return typeof window !== 'undefined' && typeof window.scrollY === 'number' +} diff --git a/packages/houdini-react/src/runtime/server/utils/isCallable.ts b/packages/houdini-react/src/runtime/server/utils/isCallable.ts new file mode 100644 index 000000000..a2062b191 --- /dev/null +++ b/packages/houdini-react/src/runtime/server/utils/isCallable.ts @@ -0,0 +1,5 @@ +export { isCallable } + +function isCallable unknown>(thing: T | unknown): thing is T { + return thing instanceof Function || typeof thing === 'function' +} diff --git a/packages/houdini-react/src/runtime/server/utils/isClientSide.ts b/packages/houdini-react/src/runtime/server/utils/isClientSide.ts new file mode 100644 index 000000000..0a67da082 --- /dev/null +++ b/packages/houdini-react/src/runtime/server/utils/isClientSide.ts @@ -0,0 +1,5 @@ +export { isClientSide } + +function isClientSide() { + return typeof window !== 'undefined' && typeof window?.getComputedStyle === 'function' +} diff --git a/packages/houdini-react/src/runtime/server/utils/isPromise.ts b/packages/houdini-react/src/runtime/server/utils/isPromise.ts new file mode 100644 index 000000000..3800b2061 --- /dev/null +++ b/packages/houdini-react/src/runtime/server/utils/isPromise.ts @@ -0,0 +1,10 @@ +import { isCallable } from './isCallable' + +export function isPromise(val: unknown): val is Promise { + return ( + typeof val === 'object' && + val !== null && + 'then' in val && + isCallable((val as Record).then) + ) +} diff --git a/packages/houdini-react/src/runtime/server/utils/isServerSide.ts b/packages/houdini-react/src/runtime/server/utils/isServerSide.ts new file mode 100644 index 000000000..d4d3cf5b2 --- /dev/null +++ b/packages/houdini-react/src/runtime/server/utils/isServerSide.ts @@ -0,0 +1,7 @@ +import { isClientSide } from './isClientSide' + +export { isServerSide } + +function isServerSide() { + return !isClientSide() +} diff --git a/packages/houdini-react/src/runtime/server/utils/objectAssign.ts b/packages/houdini-react/src/runtime/server/utils/objectAssign.ts new file mode 100644 index 000000000..85ec60764 --- /dev/null +++ b/packages/houdini-react/src/runtime/server/utils/objectAssign.ts @@ -0,0 +1,9 @@ +export { objectAssign } + +// Same as `Object.assign()` but with type inference +function objectAssign( + obj: Obj, + objAddendum: ObjAddendum +): asserts obj is Obj & ObjAddendum { + Object.assign(obj, objAddendum) +} diff --git a/packages/houdini-react/src/runtime/server/utils/projectInfo.ts b/packages/houdini-react/src/runtime/server/utils/projectInfo.ts new file mode 100644 index 000000000..52d2d887f --- /dev/null +++ b/packages/houdini-react/src/runtime/server/utils/projectInfo.ts @@ -0,0 +1,16 @@ +const PROJECT_VERSION = '0.3.14' + +export const projectInfo = { + projectName: 'react-streaming' as const, + projectVersion: PROJECT_VERSION, + npmPackageName: 'react-streaming' as const, + githubRepository: 'https://github.com/brillout/react-streaming' as const, + discordInviteToolChannel: 'https://discord.com/invite/H23tjRxFvx' as const, +} + +// Trick: since `utils/asserts.ts` depends on this file (`utils/projectInfo.ts`), we can have confidence that this file is always instantiated. So that we don't have to initialize this code snippet at every possible entry. (There are a *lot* of entries: `client/router/`, `client/`, `node/`, `node/plugin/`, `node/cli`, etc.) +globalThis.__vite_plugin_ssr__instances = globalThis.__vite_plugin_ssr__instances || [] +globalThis.__vite_plugin_ssr__instances.push(projectInfo.projectVersion) +declare global { + var __vite_plugin_ssr__instances: undefined | string[] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9999c3c0c..188b81ee4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - overrides: graphql: 15.5.0 @@ -68,7 +64,7 @@ importers: version: 4.9.4 vite: specifier: ^4.1.4 - version: 4.1.4 + version: 4.1.4(@types/node@18.11.15) vitest: specifier: ^0.28.3 version: 0.28.3(@vitest/ui@0.28.3) @@ -177,7 +173,7 @@ importers: version: 4.9.4 vite: specifier: ^4.1.4 - version: 4.1.4 + version: 4.1.4(@types/node@18.11.15) vite-plugin-lib-reporter: specifier: ^0.0.6 version: 0.0.6 @@ -214,9 +210,6 @@ importers: react-dom: specifier: ^18.3.0-canary-d6dcad6a8-20230914 version: 18.3.0-canary-d6dcad6a8-20230914(react@18.3.0-canary-d7a98a5e9-20230517) - react-streaming: - specifier: ^0.3.10 - version: 0.3.10(react-dom@18.3.0-canary-d6dcad6a8-20230914)(react@18.3.0-canary-d7a98a5e9-20230517) devDependencies: '@types/react': specifier: ^18.0.27 @@ -244,7 +237,7 @@ importers: version: 4.9.4 vite: specifier: ^4.1.0 - version: 4.1.4 + version: 4.1.4(@types/node@18.11.15) wrangler: specifier: ^3.7.0 version: 3.7.0 @@ -484,9 +477,6 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) - react-streaming: - specifier: ^0.3.14 - version: 0.3.14(react-dom@18.2.0)(react@18.2.0) devDependencies: '@cloudflare/workers-types': specifier: ^4.20230904.0 @@ -570,9 +560,9 @@ importers: react: specifier: ^18.2.0 version: 18.2.0 - react-streaming: - specifier: ^0.3.9 - version: 0.3.9(react-dom@18.2.0)(react@18.2.0) + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) recast: specifier: ^0.23.1 version: 0.23.1 @@ -599,7 +589,7 @@ importers: specifier: ^4.17.17 version: 4.17.17 '@types/react-dom': - specifier: ^18.0.10 + specifier: ^18.0.11 version: 18.0.11 next: specifier: ^13.0.1 @@ -1285,14 +1275,6 @@ packages: resolution: {integrity: sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==} dev: true - /@brillout/import@0.2.3: - resolution: {integrity: sha512-1T8WlD75eeFSMrptGy8jiLHmfHgMmSjWvLOIUvHmSVZt+6k0eQqYUoK4KbmE4T9pVLIfxvZSOm2D68VEqKRHRw==} - dev: false - - /@brillout/json-serializer@0.5.3: - resolution: {integrity: sha512-IxlOMD5gOM0WfFGdeR98jHKiC82Ad1tUnSjvLS5jnRkfMEKBI+YzHA32Umw8W3Ccp5N4fNEX229BW6RaRpxRWQ==} - dev: false - /@changesets/apply-release-plan@6.1.3: resolution: {integrity: sha512-ECDNeoc3nfeAe1jqJb5aFQX7CqzQhD2klXRez2JDb/aVpGUbX673HgKrnrgJRuQR/9f2TtLoYIzrGB9qwD77mg==} dependencies: @@ -2272,7 +2254,7 @@ packages: resolution: {integrity: sha512-SKlIcMA71Dha5JnEWlw4XxcaJ+YupuXg0QCZgl2TOLFz4SkGCwU/geAsJvUJFwK2RbVLpQv/UMq67lOaBuwDtg==} engines: {node: '>=16.0.0'} peerDependencies: - graphql: 15.5.0 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: '@graphql-tools/utils': 10.0.6(graphql@15.5.0) '@graphql-typed-document-node/core': 3.2.0(graphql@15.5.0) @@ -2285,7 +2267,7 @@ packages: /@graphql-tools/merge@8.3.14(graphql@15.5.0): resolution: {integrity: sha512-zV0MU1DnxJLIB0wpL4N3u21agEiYFsjm6DI130jqHpwF0pR9HkF+Ni65BNfts4zQelP0GjkHltG+opaozAJ1NA==} peerDependencies: - graphql: 15.5.0 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: '@graphql-tools/utils': 9.1.3(graphql@15.5.0) graphql: 15.5.0 @@ -2296,7 +2278,7 @@ packages: resolution: {integrity: sha512-J7/xqjkGTTwOJmaJQJ2C+VDBDOWJL3lKrHJN4yMaRLAJH3PosB7GiPRaSDZdErs0+F77sH2MKs2haMMkywzx7Q==} engines: {node: '>=16.0.0'} peerDependencies: - graphql: 15.5.0 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: '@graphql-tools/utils': 10.0.6(graphql@15.5.0) graphql: 15.5.0 @@ -2307,7 +2289,7 @@ packages: resolution: {integrity: sha512-kf3qOXMFcMs2f/S8Y3A8fm/2w+GaHAkfr3Gnhh2LOug/JgpY/ywgFVxO3jOeSpSEdoYcDKLcXVjMigNbY4AdQg==} engines: {node: '>=16.0.0'} peerDependencies: - graphql: 15.5.0 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: '@graphql-tools/merge': 9.0.0(graphql@15.5.0) '@graphql-tools/utils': 10.0.6(graphql@15.5.0) @@ -2319,7 +2301,7 @@ packages: /@graphql-tools/schema@9.0.12(graphql@15.5.0): resolution: {integrity: sha512-DmezcEltQai0V1y96nwm0Kg11FDS/INEFekD4nnVgzBqawvznWqK6D6bujn+cw6kivoIr3Uq//QmU/hBlBzUlQ==} peerDependencies: - graphql: 15.5.0 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: '@graphql-tools/merge': 8.3.14(graphql@15.5.0) '@graphql-tools/utils': 9.1.3(graphql@15.5.0) @@ -2332,7 +2314,7 @@ packages: resolution: {integrity: sha512-hZMjl/BbX10iagovakgf3IiqArx8TPsotq5pwBld37uIX1JiZoSbgbCIFol7u55bh32o6cfDEiiJgfAD5fbeyQ==} engines: {node: '>=16.0.0'} peerDependencies: - graphql: 15.5.0 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@15.5.0) dset: 3.1.2 @@ -2343,7 +2325,7 @@ packages: /@graphql-tools/utils@9.1.3(graphql@15.5.0): resolution: {integrity: sha512-bbJyKhs6awp1/OmP+WKA1GOyu9UbgZGkhIj5srmiMGLHohEOKMjW784Sk0BZil1w2x95UPu0WHw6/d/HVCACCg==} peerDependencies: - graphql: 15.5.0 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: graphql: 15.5.0 tslib: 2.6.2 @@ -2352,7 +2334,7 @@ packages: /@graphql-tools/utils@9.2.1(graphql@15.5.0): resolution: {integrity: sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==} peerDependencies: - graphql: 15.5.0 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@15.5.0) graphql: 15.5.0 @@ -2362,7 +2344,7 @@ packages: /@graphql-typed-document-node/core@3.2.0(graphql@15.5.0): resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} peerDependencies: - graphql: 15.5.0 + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: graphql: 15.5.0 dev: false @@ -2378,7 +2360,7 @@ packages: resolution: {integrity: sha512-pjA7xLIYVCugxyM/FwG7SlbPVPxn8cr5AFT0I4EsgwK2z6D0oM+fN6guPam7twTVKr0BmA/EtVm2RWkik114Mw==} peerDependencies: '@graphql-tools/utils': ^9.2.1 - graphql: 15.5.0 + graphql: ^15.2.0 || ^16.0.0 graphql-yoga: ^3.9.1 dependencies: '@graphql-tools/utils': 9.2.1(graphql@15.5.0) @@ -2856,7 +2838,7 @@ packages: svelte: 3.57.0 tiny-glob: 0.2.9 undici: 5.20.0 - vite: 4.1.4 + vite: 4.1.4(@types/node@18.11.15) transitivePeerDependencies: - supports-color @@ -2910,7 +2892,7 @@ packages: magic-string: 0.27.0 svelte: 3.57.0 svelte-hmr: 0.15.1(svelte@3.57.0) - vite: 4.1.4 + vite: 4.1.4(@types/node@18.11.15) vitefu: 0.2.3(vite@4.1.4) transitivePeerDependencies: - supports-color @@ -3606,7 +3588,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.21.4) magic-string: 0.27.0 react-refresh: 0.14.0 - vite: 4.1.4 + vite: 4.1.4(@types/node@18.11.15) transitivePeerDependencies: - supports-color dev: true @@ -6933,7 +6915,7 @@ packages: resolution: {integrity: sha512-44yBuw2/DLNEiMypbNZBt1yMDbBmyVPVesPywnteGGALiBmdyy1JP8jSg8ClLePg8ZZxk0O4BLhd1a6U/1jDOQ==} engines: {node: ^12.20.0 || ^14.15.0 || >= 15.9.0} peerDependencies: - graphql: 15.5.0 + graphql: ^16.2.0 dependencies: graphql: 15.5.0 dev: false @@ -6942,7 +6924,7 @@ packages: resolution: {integrity: sha512-4EiZ3/UXYcjm+xFGP544/yW1+DVI8ZpKASFbzrV5EDTFWJp0ZvLl4Dy2fSZAzz9imKp5pZMIcjB0x/H69Pv/6w==} engines: {node: '>=10'} peerDependencies: - graphql: 15.5.0 + graphql: '>=0.11 <=16' dependencies: graphql: 15.5.0 dev: false @@ -6951,7 +6933,7 @@ packages: resolution: {integrity: sha512-MvCLhFecYNIKuxAZisPjpIL9lxRYbpgPSNKENDO/8CV3oiFlsLJHZb5dp2sVAeLafXHeZ9TgkijLthUBc1+Jag==} engines: {node: '>=16.0.0'} peerDependencies: - graphql: 15.5.0 + graphql: ^15.2.0 || ^16.0.0 dependencies: '@envelop/core': 4.0.1 '@graphql-tools/executor': 1.2.0(graphql@15.5.0) @@ -7451,11 +7433,6 @@ packages: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} dev: true - /isbot-fast@1.2.0: - resolution: {integrity: sha512-twjuQzy2gKMDVfKGQyQqrx6Uy4opu/fiVUTTpdqtFsd7OQijIp5oXvb27n5EemYXaijh5fomndJt/SPRLsEdSg==} - engines: {node: '>=6.0.0'} - dev: false - /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -9410,43 +9387,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /react-streaming@0.3.10(react-dom@18.3.0-canary-d6dcad6a8-20230914)(react@18.3.0-canary-d7a98a5e9-20230517): - resolution: {integrity: sha512-Cf/aritzKGDUEBo+jpsNKlzkNBTmBM5gYM9jArPuoBqmckBikALYu7YvQbTHSG4kbpSitnoBIUH2005LAxknBQ==} - peerDependencies: - react: '>=18' - react-dom: '>=18' - dependencies: - '@brillout/json-serializer': 0.5.3 - isbot-fast: 1.2.0 - react: 18.3.0-canary-d7a98a5e9-20230517 - react-dom: 18.3.0-canary-d6dcad6a8-20230914(react@18.3.0-canary-d7a98a5e9-20230517) - dev: false - - /react-streaming@0.3.14(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-nYTYNp1NjJbEDFiiN/NpuUOhDm94GszLD/aGxXRtbYO2z2GlvL3S/OHjku17+qFfP91IQTF1fcHcvjCKEd3Ong==} - peerDependencies: - react: '>=18' - react-dom: '>=18' - dependencies: - '@brillout/import': 0.2.3 - '@brillout/json-serializer': 0.5.3 - isbot-fast: 1.2.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /react-streaming@0.3.9(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-P0sn1dXtJfKkJMZZoPAu3YaNi6008a8sUg58XvYb21nDepysKWC7YzZCQdEhw/dp0r5s3nxDcrUmXFHFiGaf7w==} - peerDependencies: - react: '>=18' - react-dom: '>=18' - dependencies: - '@brillout/json-serializer': 0.5.3 - isbot-fast: 1.2.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - /react@16.14.0: resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==} engines: {node: '>=0.10.0'} @@ -11276,38 +11216,6 @@ packages: optionalDependencies: fsevents: 2.3.2 - /vite@4.1.4: - resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==} - engines: {node: ^14.18.0 || >=16.0.0} - hasBin: true - peerDependencies: - '@types/node': '>= 14' - less: '*' - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - esbuild: 0.16.17 - postcss: 8.4.21 - resolve: 1.22.1 - rollup: 3.14.0 - optionalDependencies: - fsevents: 2.3.2 - /vite@4.1.4(@types/node@18.11.15): resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==} engines: {node: ^14.18.0 || >=16.0.0} @@ -11340,7 +11248,6 @@ packages: rollup: 3.27.1 optionalDependencies: fsevents: 2.3.2 - dev: true /vite@4.4.8(@types/node@18.11.15): resolution: {integrity: sha512-LONawOUUjxQridNWGQlNizfKH89qPigK36XhMI7COMGztz8KNY0JHim7/xDd71CZwGT4HtSRgI7Hy+RlhG0Gvg==} @@ -11395,7 +11302,7 @@ packages: vite: optional: true dependencies: - vite: 4.1.4 + vite: 4.1.4(@types/node@18.11.15) /vitefu@0.2.3(vite@4.4.8): resolution: {integrity: sha512-75l7TTuU8isAhz1QFtNKjDkqjxvndfMC1AfIMjJ0ZQ59ZD0Ow9QOIsJJX16Wv9PS8f+zMzp6fHy5cCbKG/yVUQ==} @@ -11801,3 +11708,7 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: true + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false